diff options
Diffstat (limited to 'src/mod_http_fileserver.erl')
-rw-r--r-- | src/mod_http_fileserver.erl | 379 |
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). |