aboutsummaryrefslogtreecommitdiff
path: root/src/mod_http_fileserver.erl
diff options
context:
space:
mode:
Diffstat (limited to 'src/mod_http_fileserver.erl')
-rw-r--r--src/mod_http_fileserver.erl379
1 files changed, 379 insertions, 0 deletions
diff --git a/src/mod_http_fileserver.erl b/src/mod_http_fileserver.erl
new file mode 100644
index 000000000..300bb26eb
--- /dev/null
+++ b/src/mod_http_fileserver.erl
@@ -0,0 +1,379 @@
+%%%-------------------------------------------------------------------
+%%% 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').
+
+-author('ecestari@process-one.net').
+
+-behaviour(gen_mod).
+
+%% gen_mod callbacks
+-export([start/2, stop/1]).
+
+%% request_handlers callbacks
+-export([process/2]).
+
+-include("ejabberd.hrl").
+-include("logger.hrl").
+
+-include("jlib.hrl").
+
+-include_lib("kernel/include/file.hrl").
+
+-include("ejabberd_http.hrl").
+
+-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">>}]).
+
+start(Host, Opts) ->
+ DocRoot = gen_mod:get_opt(docroot, Opts,
+ fun iolist_to_binary/1,
+ undefined),
+ set_default_host(Host, Opts),
+ conf_store(Host, docroot, DocRoot),
+ 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),
+ start_log(Host, AccessLog),
+ DirectoryIndices = gen_mod:get_opt(directory_indices, Opts,
+ fun(L) when is_list(L) -> L end,
+ []),
+ conf_store(Host, directory_indices, DirectoryIndices),
+ ServeStaticGzip = gen_mod:get_opt(serve_gzip, Opts,
+ fun(B) when is_boolean(B) -> B end,
+ false),
+ conf_store(Host, serve_gzip, ServeStaticGzip),
+ CustomHeaders = gen_mod:get_opt(custom_headers, Opts,
+ fun(L) when is_list(L) -> L end,
+ []),
+ conf_store(Host, custom_headers, CustomHeaders),
+ DefaultContentType =
+ gen_mod:get_opt(default_content_type, Opts,
+ fun iolist_to_binary/1,
+ ?DEFAULT_CONTENT_TYPE),
+ conf_store(Host, default_content_type,
+ DefaultContentType),
+ ContentTypes = build_list_content_types(
+ gen_mod:get_opt(content_types, Opts,
+ fun(L) when is_list(L) -> L end,
+ []),
+ ?DEFAULT_CONTENT_TYPES),
+ conf_store(Host, content_types, ContentTypes),
+ ?INFO_MSG("initialize: ~n ~p", [ContentTypes]),
+ ok.
+
+set_default_host(Host, _Opts) ->
+ case mochiglobal:get(http_default_host) of
+ undefined ->
+ ?DEBUG("Setting default host to ~p", [Host]),
+ mochiglobal:put(http_default_host, Host);
+ _ -> ok
+ end.
+
+get_host(Host) ->
+ DCT = mochiglobal:get(default_content_type),
+ case lists:keymember(Host, 1, DCT) of
+ true -> Host;
+ false -> mochiglobal:get(http_default_host)
+ end.
+
+conf_store(Host, Key, Value) ->
+ R = case mochiglobal:get(Key) of
+ undefined -> [{Host, Value}];
+ A ->
+ case lists:keymember(Host, 1, A) of
+ true -> lists:keyreplace(Host, 1, A, {Host, Value});
+ false -> [{Host, Value} | A]
+ end
+ end,
+ mochiglobal:put(Key, R).
+
+conf_get(Host, Key) ->
+ case mochiglobal:get(Key) of
+ undefined -> undefined;
+ A ->
+ case lists:keyfind(Host, 1, A) of
+ {Host, Val} -> Val;
+ false ->
+ case mochiglobal:get(http_default_host) of
+ Host -> % stop recursion here
+ undefined;
+ DefaultHost -> conf_get(DefaultHost, Key)
+ end
+ end
+ end.
+
+%% @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.
+
+start_log(_Host, undefined) -> ok;
+start_log(Host, FileName) ->
+ mod_http_fileserver_log:start(Host, FileName).
+
+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.
+
+stop(_Host) -> ok.
+
+%%====================================================================
+%% 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]),
+ Host = get_host(Request#request.host),
+ ClientHeaders = Request#request.headers,
+ DirectoryIndices = conf_get(Host, directory_indices),
+ CustomHeaders = conf_get(Host, custom_headers),
+ DefaultContentType = conf_get(Host,
+ default_content_type),
+ ContentTypes = conf_get(Host, content_types),
+ Encoding = conf_get(Host, serve_gzip),
+ Static = select_encoding(ClientHeaders, Encoding),
+ DocRoot = conf_get(Host, docroot),
+ FileName = filename:join(filename:split(DocRoot) ++
+ LocalPath),
+ {FileSize, Code, Headers, Contents} = 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,
+ Static);
+ {ok, FileInfo} ->
+ case should_serve(FileInfo,
+ ClientHeaders)
+ of
+ true ->
+ serve_file(FileInfo,
+ FileName,
+ CustomHeaders,
+ DefaultContentType,
+ ContentTypes,
+ Static);
+ false -> {0, 304, [], []}
+ end
+ end,
+ mod_http_fileserver_log:add_to_log(Host, FileSize, Code,
+ Request),
+ {Code, Headers, Contents}.
+
+should_serve(FileInfo, Headers) ->
+ lists:foldl(fun ({Header, Fun}, Acc) ->
+ case lists:keyfind(Header, 1, Headers) of
+ {_, Val} -> Fun(FileInfo, Val);
+ _O -> Acc
+ end
+ end,
+ true, [{'If-None-Match', fun etag/2}]).
+
+etag(FileInfo, Etag) ->
+ case httpd_util:create_etag(FileInfo) of
+ Etag -> false;
+ _ -> true
+ end.
+
+select_encoding(_Headers, false) -> false;
+select_encoding(Headers, Configuration) ->
+ Value = find_header('Accept-Encoding', Headers, <<"">>),
+ Schemes = str:tokens(Value, <<",">>),
+ case lists:member(<<"gzip">>, Schemes) of
+ true -> Configuration;
+ false -> false
+ end.
+
+serve_index(_FileName, [], _CH, _DefaultContentType,
+ _ContentTypes, _Static) ->
+ ?HTTP_ERR_FILE_NOT_FOUND;
+serve_index(FileName, [Index | T], CH,
+ DefaultContentType, ContentTypes, Static) ->
+ IndexFileName = filename:join([FileName] ++ [Index]),
+ case file:read_file_info(IndexFileName) of
+ {error, _Error} ->
+ serve_index(FileName, T, CH, DefaultContentType,
+ ContentTypes, Static);
+ {ok, #file_info{type = directory}} ->
+ serve_index(FileName, T, CH, DefaultContentType,
+ ContentTypes, Static);
+ {ok, FileInfo} ->
+ serve_file(FileInfo, IndexFileName, CH,
+ DefaultContentType, ContentTypes, Static)
+ 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, false) ->
+ ?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};
+serve_file(FileInfo, FileName, CustomHeaders,
+ DefaultContentType, ContentTypes, Gzip) ->
+ ?DEBUG("Delivering: ~s", [FileName]),
+ ContentType = content_type(FileName, DefaultContentType,
+ ContentTypes),
+ CompressedFileName = <<FileName/binary, ".gz">>,
+ case file:read_file_info(CompressedFileName) of
+ {ok, FileInfoCompressed} ->
+ ?INFO_MSG("Found compressed: ~s", [FileName]),
+ {ok, FileContents} = file:read_file(CompressedFileName),
+ {FileInfoCompressed#file_info.size, 200,
+ [{<<"Server">>, <<"ejabberd">>},
+ {<<"Last-Modified">>,
+ last_modified(FileInfoCompressed)},
+ {<<"Content-Type">>, ContentType},
+ {<<"Etag">>,
+ httpd_util:create_etag(FileInfoCompressed)},
+ {<<"Content-Encoding">>, <<"gzip">>}
+ | CustomHeaders],
+ FileContents};
+ {error, _} ->
+ {FileContents, Size} = case Gzip of
+ static ->
+ {ok, Content} = file:read_file(FileName),
+ {Content, FileInfo#file_info.size};
+ always ->
+ {ok, Content} = file:read_file(FileName),
+ Compressed = zlib:gzip(Content),
+ {Compressed, byte_size(Compressed)}
+ end,
+ {Size, 200,
+ [{<<"Server">>, <<"ejabberd">>},
+ {<<"Last-Modified">>, last_modified(FileInfo)},
+ {<<"Etag">>, httpd_util:create_etag(FileInfo)},
+ {<<"Content-Type">>, ContentType},
+ {<<"Content-Encoding">>, <<"gzip">>}
+ | CustomHeaders],
+ FileContents}
+ end.
+
+find_header(Header, Headers, Default) ->
+ case lists:keysearch(Header, 1, Headers) of
+ {value, {_, Value}} -> Value;
+ false -> Default
+ end.
+
+%%----------------------------------------------------------------------
+%% Utilities
+%%----------------------------------------------------------------------
+
+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).