24
1
mirror of https://github.com/processone/ejabberd.git synced 2024-06-02 21:17:12 +02:00
xmpp.chapril.org-ejabberd/src/mod_http_upload.erl
Evgeny Khramtsov 08f3d066b1 Switch more log message to warning level
The commit is supposed to improve logging at loglevel 3, which
is the recommended level for high loaded ejabberd servers
2018-09-19 23:12:14 +03:00

986 lines
34 KiB
Erlang

%%%----------------------------------------------------------------------
%%% File : mod_http_upload.erl
%%% Author : Holger Weiss <holger@zedat.fu-berlin.de>
%%% Purpose : HTTP File Upload (XEP-0363)
%%% Created : 20 Aug 2015 by Holger Weiss <holger@zedat.fu-berlin.de>
%%%
%%%
%%% ejabberd, Copyright (C) 2015-2018 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.,
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
%%%
%%%----------------------------------------------------------------------
-module(mod_http_upload).
-author('holger@zedat.fu-berlin.de').
-protocol({xep, 363, '0.1'}).
-define(SERVICE_REQUEST_TIMEOUT, 5000). % 5 seconds.
-define(CALL_TIMEOUT, 60000). % 1 minute.
-define(SLOT_TIMEOUT, timer:hours(5)).
-define(DEFAULT_CONTENT_TYPE, <<"application/octet-stream">>).
-define(CONTENT_TYPES,
[{<<".avi">>, <<"video/avi">>},
{<<".bmp">>, <<"image/bmp">>},
{<<".bz2">>, <<"application/x-bzip2">>},
{<<".gif">>, <<"image/gif">>},
{<<".gz">>, <<"application/x-gzip">>},
{<<".jpeg">>, <<"image/jpeg">>},
{<<".jpg">>, <<"image/jpeg">>},
{<<".m4a">>, <<"audio/mp4">>},
{<<".mp3">>, <<"audio/mpeg">>},
{<<".mp4">>, <<"video/mp4">>},
{<<".mpeg">>, <<"video/mpeg">>},
{<<".mpg">>, <<"video/mpeg">>},
{<<".ogg">>, <<"application/ogg">>},
{<<".pdf">>, <<"application/pdf">>},
{<<".png">>, <<"image/png">>},
{<<".rtf">>, <<"application/rtf">>},
{<<".svg">>, <<"image/svg+xml">>},
{<<".tiff">>, <<"image/tiff">>},
{<<".txt">>, <<"text/plain">>},
{<<".wav">>, <<"audio/wav">>},
{<<".webp">>, <<"image/webp">>},
{<<".xz">>, <<"application/x-xz">>},
{<<".zip">>, <<"application/zip">>}]).
-behaviour(gen_server).
-behaviour(gen_mod).
%% gen_mod/supervisor callbacks.
-export([start/2,
stop/1,
depends/2,
mod_opt_type/1,
mod_options/1]).
%% gen_server callbacks.
-export([init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3]).
%% ejabberd_http callback.
-export([process/2]).
%% ejabberd_hooks callback.
-export([remove_user/2]).
%% Utility functions.
-export([get_proc_name/2,
expand_home/1,
expand_host/2]).
-include("ejabberd_http.hrl").
-include("xmpp.hrl").
-include("logger.hrl").
-include("translate.hrl").
-record(state,
{server_host :: binary(),
hosts :: [binary()],
name :: binary(),
access :: atom(),
max_size :: pos_integer() | infinity,
secret_length :: pos_integer(),
jid_in_url :: sha1 | node,
file_mode :: integer() | undefined,
dir_mode :: integer() | undefined,
docroot :: binary(),
put_url :: binary(),
get_url :: binary(),
service_url :: binary() | undefined,
thumbnail :: boolean(),
custom_headers :: [{binary(), binary()}],
slots = #{} :: map(),
external_secret :: binary()}).
-record(media_info,
{type :: atom(),
height :: integer(),
width :: integer()}).
-type state() :: #state{}.
-type slot() :: [binary(), ...].
-type media_info() :: #media_info{}.
%%--------------------------------------------------------------------
%% gen_mod/supervisor callbacks.
%%--------------------------------------------------------------------
-spec start(binary(), gen_mod:opts()) -> {ok, pid()} | {error, already_started}.
start(ServerHost, Opts) ->
case gen_mod:get_opt(rm_on_unregister, Opts) of
true ->
ejabberd_hooks:add(remove_user, ServerHost, ?MODULE,
remove_user, 50);
false ->
ok
end,
Proc = get_proc_name(ServerHost, ?MODULE),
case whereis(Proc) of
undefined ->
gen_mod:start_child(?MODULE, ServerHost, Opts, Proc);
_Pid ->
?ERROR_MSG("Multiple virtual hosts can't use a single 'put_url' "
"without the @HOST@ keyword", []),
{error, already_started}
end.
-spec stop(binary()) -> ok | {error, any()}.
stop(ServerHost) ->
case gen_mod:get_module_opt(ServerHost, ?MODULE, rm_on_unregister) of
true ->
ejabberd_hooks:delete(remove_user, ServerHost, ?MODULE,
remove_user, 50);
false ->
ok
end,
Proc = get_proc_name(ServerHost, ?MODULE),
gen_mod:stop_child(Proc).
-spec mod_opt_type(atom()) -> fun((term()) -> term()) | [atom()].
mod_opt_type(host) ->
fun ejabberd_config:v_host/1;
mod_opt_type(hosts) ->
fun ejabberd_config:v_hosts/1;
mod_opt_type(name) ->
fun iolist_to_binary/1;
mod_opt_type(access) ->
fun acl:access_rules_validator/1;
mod_opt_type(max_size) ->
fun(I) when is_integer(I), I > 0 -> I;
(infinity) -> infinity
end;
mod_opt_type(secret_length) ->
fun(I) when is_integer(I), I >= 8 -> I end;
mod_opt_type(jid_in_url) ->
fun(sha1) -> sha1;
(node) -> node
end;
mod_opt_type(file_mode) ->
fun(undefined) -> undefined;
(Mode) -> binary_to_integer(iolist_to_binary(Mode), 8)
end;
mod_opt_type(dir_mode) ->
fun(undefined) -> undefined;
(Mode) -> binary_to_integer(iolist_to_binary(Mode), 8)
end;
mod_opt_type(docroot) ->
fun iolist_to_binary/1;
mod_opt_type(put_url) ->
fun misc:try_url/1;
mod_opt_type(get_url) ->
fun(undefined) -> undefined;
(URL) -> misc:try_url(URL)
end;
mod_opt_type(service_url) ->
fun(undefined) -> undefined;
(URL) ->
?WARNING_MSG("option 'service_url' is deprecated, consider unsing "
"the 'external_secret' interface instead", []),
misc:try_url(URL)
end;
mod_opt_type(custom_headers) ->
fun(Headers) ->
lists:map(fun({K, V}) ->
{iolist_to_binary(K), iolist_to_binary(V)}
end, Headers)
end;
mod_opt_type(rm_on_unregister) ->
fun(B) when is_boolean(B) -> B end;
mod_opt_type(thumbnail) ->
fun(true) ->
case eimp:supported_formats() of
[] ->
?WARNING_MSG("ejabberd is built without image converter "
"support, option '~s' is ignored",
[thumbnail]),
erlang:error(badarg);
_ ->
true
end;
(false) ->
false
end;
mod_opt_type(external_secret) ->
fun iolist_to_binary/1.
-spec mod_options(binary()) -> [{atom(), any()}].
mod_options(_Host) ->
[{host, <<"upload.@HOST@">>},
{hosts, []},
{name, ?T("HTTP File Upload")},
{access, local},
{max_size, 104857600},
{secret_length, 40},
{jid_in_url, sha1},
{file_mode, undefined},
{dir_mode, undefined},
{docroot, <<"@HOME@/upload">>},
{put_url, <<"https://@HOST@:5443/upload">>},
{get_url, undefined},
{service_url, undefined},
{external_secret, <<"">>},
{custom_headers, []},
{rm_on_unregister, true},
{thumbnail, false}].
-spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}].
depends(_Host, _Opts) ->
[].
%%--------------------------------------------------------------------
%% gen_server callbacks.
%%--------------------------------------------------------------------
-spec init(list()) -> {ok, state()}.
init([ServerHost, Opts]) ->
process_flag(trap_exit, true),
Hosts = gen_mod:get_opt_hosts(ServerHost, Opts),
Name = gen_mod:get_opt(name, Opts),
Access = gen_mod:get_opt(access, Opts),
MaxSize = gen_mod:get_opt(max_size, Opts),
SecretLength = gen_mod:get_opt(secret_length, Opts),
JIDinURL = gen_mod:get_opt(jid_in_url, Opts),
DocRoot = gen_mod:get_opt(docroot, Opts),
FileMode = gen_mod:get_opt(file_mode, Opts),
DirMode = gen_mod:get_opt(dir_mode, Opts),
PutURL = gen_mod:get_opt(put_url, Opts),
GetURL = case gen_mod:get_opt(get_url, Opts) of
undefined -> PutURL;
URL -> URL
end,
ServiceURL = gen_mod:get_opt(service_url, Opts),
Thumbnail = gen_mod:get_opt(thumbnail, Opts),
ExternalSecret = gen_mod:get_opt(external_secret, Opts),
CustomHeaders = gen_mod:get_opt(custom_headers, Opts),
DocRoot1 = expand_home(str:strip(DocRoot, right, $/)),
DocRoot2 = expand_host(DocRoot1, ServerHost),
case DirMode of
undefined ->
ok;
Mode ->
file:change_mode(DocRoot2, Mode)
end,
lists:foreach(
fun(Host) ->
ejabberd_router:register_route(Host, ServerHost)
end, Hosts),
{ok, #state{server_host = ServerHost, hosts = Hosts, name = Name,
access = Access, max_size = MaxSize,
secret_length = SecretLength, jid_in_url = JIDinURL,
file_mode = FileMode, dir_mode = DirMode,
thumbnail = Thumbnail,
docroot = DocRoot2,
put_url = expand_host(str:strip(PutURL, right, $/), ServerHost),
get_url = expand_host(str:strip(GetURL, right, $/), ServerHost),
service_url = ServiceURL,
external_secret = ExternalSecret,
custom_headers = CustomHeaders}}.
-spec handle_call(_, {pid(), _}, state())
-> {reply, {ok, pos_integer(), binary(),
pos_integer() | undefined,
pos_integer() | undefined}, state()} |
{reply, {error, atom()}, state()} | {noreply, state()}.
handle_call({use_slot, Slot, Size}, _From,
#state{file_mode = FileMode,
dir_mode = DirMode,
get_url = GetPrefix,
thumbnail = Thumbnail,
custom_headers = CustomHeaders,
docroot = DocRoot} = State) ->
case get_slot(Slot, State) of
{ok, {Size, TRef}} ->
misc:cancel_timer(TRef),
NewState = del_slot(Slot, State),
Path = str:join([DocRoot | Slot], <<$/>>),
{reply,
{ok, Path, FileMode, DirMode, GetPrefix, Thumbnail, CustomHeaders},
NewState};
{ok, {_WrongSize, _TRef}} ->
{reply, {error, size_mismatch}, State};
error ->
{reply, {error, invalid_slot}, State}
end;
handle_call(get_conf, _From,
#state{docroot = DocRoot,
custom_headers = CustomHeaders} = State) ->
{reply, {ok, DocRoot, CustomHeaders}, State};
handle_call(Request, From, State) ->
?ERROR_MSG("Got unexpected request from ~p: ~p", [From, Request]),
{noreply, State}.
-spec handle_cast(_, state()) -> {noreply, state()}.
handle_cast(Request, State) ->
?ERROR_MSG("Got unexpected request: ~p", [Request]),
{noreply, State}.
-spec handle_info(timeout | _, state()) -> {noreply, state()}.
handle_info({route, #iq{lang = Lang} = Packet}, State) ->
try xmpp:decode_els(Packet) of
IQ ->
{Reply, NewState} = case process_iq(IQ, State) of
R when is_record(R, iq) ->
{R, State};
{R, S} ->
{R, S};
not_request ->
{none, State}
end,
if Reply /= none ->
ejabberd_router:route(Reply);
true ->
ok
end,
{noreply, NewState}
catch _:{xmpp_codec, Why} ->
Txt = xmpp:io_format_error(Why),
Err = xmpp:err_bad_request(Txt, Lang),
ejabberd_router:route_error(Packet, Err),
{noreply, State}
end;
handle_info({timeout, _TRef, Slot}, State) ->
NewState = del_slot(Slot, State),
{noreply, NewState};
handle_info(Info, State) ->
?ERROR_MSG("Got unexpected info: ~p", [Info]),
{noreply, State}.
-spec terminate(normal | shutdown | {shutdown, _} | _, state()) -> ok.
terminate(Reason, #state{server_host = ServerHost, hosts = Hosts}) ->
?DEBUG("Stopping HTTP upload process for ~s: ~p", [ServerHost, Reason]),
lists:foreach(fun ejabberd_router:unregister_route/1, Hosts).
-spec code_change({down, _} | _, state(), _) -> {ok, state()}.
code_change(_OldVsn, #state{server_host = ServerHost} = State, _Extra) ->
?DEBUG("Updating HTTP upload process for ~s", [ServerHost]),
{ok, State}.
%%--------------------------------------------------------------------
%% ejabberd_http callback.
%%--------------------------------------------------------------------
-spec process([binary()], #request{})
-> {pos_integer(), [{binary(), binary()}], binary()}.
process(LocalPath, #request{method = Method, host = Host, ip = IP})
when length(LocalPath) < 3,
Method == 'PUT' orelse
Method == 'GET' orelse
Method == 'HEAD' ->
?DEBUG("Rejecting ~s request from ~s for ~s: Too few path components",
[Method, encode_addr(IP), Host]),
http_response(404);
process(_LocalPath, #request{method = 'PUT', host = Host, ip = IP,
length = Length} = Request) ->
{Proc, Slot} = parse_http_request(Request),
case catch gen_server:call(Proc, {use_slot, Slot, Length}, ?CALL_TIMEOUT) of
{ok, Path, FileMode, DirMode, GetPrefix, Thumbnail, CustomHeaders} ->
?DEBUG("Storing file from ~s for ~s: ~s",
[encode_addr(IP), Host, Path]),
case store_file(Path, Request, FileMode, DirMode,
GetPrefix, Slot, Thumbnail) of
ok ->
http_response(201, CustomHeaders);
{ok, Headers, OutData} ->
http_response(201, Headers ++ CustomHeaders, OutData);
{error, closed} ->
?DEBUG("Cannot store file ~s from ~s for ~s: connection closed",
[Path, encode_addr(IP), Host]),
http_response(404);
{error, Error} ->
?ERROR_MSG("Cannot store file ~s from ~s for ~s: ~s",
[Path, encode_addr(IP), Host, format_error(Error)]),
http_response(500)
end;
{error, size_mismatch} ->
?WARNING_MSG("Rejecting file ~s from ~s for ~s: Unexpected size (~B)",
[lists:last(Slot), encode_addr(IP), Host, Length]),
http_response(413);
{error, invalid_slot} ->
?WARNING_MSG("Rejecting file ~s from ~s for ~s: Invalid slot",
[lists:last(Slot), encode_addr(IP), Host]),
http_response(403);
Error ->
?ERROR_MSG("Cannot handle PUT request from ~s for ~s: ~p",
[encode_addr(IP), Host, Error]),
http_response(500)
end;
process(_LocalPath, #request{method = Method, host = Host, ip = IP} = Request)
when Method == 'GET';
Method == 'HEAD' ->
{Proc, [_UserDir, _RandDir, FileName] = Slot} = parse_http_request(Request),
case catch gen_server:call(Proc, get_conf, ?CALL_TIMEOUT) of
{ok, DocRoot, CustomHeaders} ->
Path = str:join([DocRoot | Slot], <<$/>>),
case file:open(Path, [read]) of
{ok, Fd} ->
file:close(Fd),
?INFO_MSG("Serving ~s to ~s", [Path, encode_addr(IP)]),
ContentType = guess_content_type(FileName),
Headers1 = case ContentType of
<<"image/", _SubType/binary>> -> [];
<<"text/", _SubType/binary>> -> [];
_ ->
[{<<"Content-Disposition">>,
<<"attachment; filename=",
$", FileName/binary, $">>}]
end,
Headers2 = [{<<"Content-Type">>, ContentType} | Headers1],
Headers3 = Headers2 ++ CustomHeaders,
http_response(200, Headers3, {file, Path});
{error, eacces} ->
?WARNING_MSG("Cannot serve ~s to ~s: Permission denied",
[Path, encode_addr(IP)]),
http_response(403);
{error, enoent} ->
?WARNING_MSG("Cannot serve ~s to ~s: No such file",
[Path, encode_addr(IP)]),
http_response(404);
{error, eisdir} ->
?WARNING_MSG("Cannot serve ~s to ~s: Is a directory",
[Path, encode_addr(IP)]),
http_response(404);
{error, Error} ->
?WARNING_MSG("Cannot serve ~s to ~s: ~s",
[Path, encode_addr(IP), format_error(Error)]),
http_response(500)
end;
Error ->
?ERROR_MSG("Cannot handle ~s request from ~s for ~s: ~p",
[Method, encode_addr(IP), Host, Error]),
http_response(500)
end;
process(_LocalPath, #request{method = 'OPTIONS', host = Host,
ip = IP} = Request) ->
?DEBUG("Responding to OPTIONS request from ~s for ~s",
[encode_addr(IP), Host]),
{Proc, _Slot} = parse_http_request(Request),
case catch gen_server:call(Proc, get_conf, ?CALL_TIMEOUT) of
{ok, _DocRoot, CustomHeaders} ->
AllowHeader = {<<"Allow">>, <<"OPTIONS, HEAD, GET, PUT">>},
http_response(200, [AllowHeader | CustomHeaders]);
Error ->
?ERROR_MSG("Cannot handle OPTIONS request from ~s for ~s: ~p",
[encode_addr(IP), Host, Error]),
http_response(500)
end;
process(_LocalPath, #request{method = Method, host = Host, ip = IP}) ->
?DEBUG("Rejecting ~s request from ~s for ~s",
[Method, encode_addr(IP), Host]),
http_response(405, [{<<"Allow">>, <<"OPTIONS, HEAD, GET, PUT">>}]).
%%--------------------------------------------------------------------
%% Exported utility functions.
%%--------------------------------------------------------------------
-spec get_proc_name(binary(), atom()) -> atom().
get_proc_name(ServerHost, ModuleName) ->
PutURL = gen_mod:get_module_opt(ServerHost, ?MODULE, put_url),
{ok, {_Scheme, _UserInfo, Host, _Port, Path, _Query}} =
http_uri:parse(binary_to_list(expand_host(PutURL, ServerHost))),
ProcPrefix = list_to_binary(string:strip(Host ++ Path, right, $/)),
gen_mod:get_module_proc(ProcPrefix, ModuleName).
-spec expand_home(binary()) -> binary().
expand_home(Input) ->
{ok, [[Home]]} = init:get_argument(home),
misc:expand_keyword(<<"@HOME@">>, Input, Home).
-spec expand_host(binary(), binary()) -> binary().
expand_host(Input, Host) ->
misc:expand_keyword(<<"@HOST@">>, Input, Host).
%%--------------------------------------------------------------------
%% Internal functions.
%%--------------------------------------------------------------------
%% XMPP request handling.
-spec process_iq(iq(), state()) -> {iq(), state()} | iq() | not_request.
process_iq(#iq{type = get, lang = Lang, sub_els = [#disco_info{}]} = IQ,
#state{server_host = ServerHost, name = Name}) ->
AddInfo = ejabberd_hooks:run_fold(disco_info, ServerHost, [],
[ServerHost, ?MODULE, <<"">>, <<"">>]),
xmpp:make_iq_result(IQ, iq_disco_info(ServerHost, Lang, Name, AddInfo));
process_iq(#iq{type = get, sub_els = [#disco_items{}]} = IQ, _State) ->
xmpp:make_iq_result(IQ, #disco_items{});
process_iq(#iq{type = get, sub_els = [#upload_request{filename = File,
size = Size,
'content-type' = CType,
xmlns = XMLNS}]} = IQ,
State) ->
process_slot_request(IQ, File, Size, CType, XMLNS, State);
process_iq(#iq{type = get, sub_els = [#upload_request_0{filename = File,
size = Size,
'content-type' = CType,
xmlns = XMLNS}]} = IQ,
State) ->
process_slot_request(IQ, File, Size, CType, XMLNS, State);
process_iq(#iq{type = T, lang = Lang} = IQ, _State) when T == get; T == set ->
Txt = <<"No module is handling this query">>,
xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang));
process_iq(#iq{}, _State) ->
not_request.
-spec process_slot_request(iq(), binary(), pos_integer(), binary(), binary(),
state()) -> {iq(), state()} | iq().
process_slot_request(#iq{lang = Lang, from = From} = IQ,
File, Size, CType, XMLNS,
#state{server_host = ServerHost,
access = Access} = State) ->
case acl:match_rule(ServerHost, Access, From) of
allow ->
ContentType = yield_content_type(CType),
case create_slot(State, From, File, Size, ContentType, XMLNS,
Lang) of
{ok, Slot} ->
Query = make_query_string(Slot, Size, State),
NewState = add_slot(Slot, Size, State),
NewSlot = mk_slot(Slot, State, XMLNS, Query),
{xmpp:make_iq_result(IQ, NewSlot), NewState};
{ok, PutURL, GetURL} ->
Slot = mk_slot(PutURL, GetURL, XMLNS, <<"">>),
xmpp:make_iq_result(IQ, Slot);
{error, Error} ->
xmpp:make_error(IQ, Error)
end;
deny ->
?DEBUG("Denying HTTP upload slot request from ~s",
[jid:encode(From)]),
Txt = <<"Access denied by service policy">>,
xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang))
end.
-spec create_slot(state(), jid(), binary(), pos_integer(), binary(), binary(),
binary())
-> {ok, slot()} | {ok, binary(), binary()} | {error, xmlel()}.
create_slot(#state{service_url = undefined, max_size = MaxSize},
JID, File, Size, _ContentType, XMLNS, Lang)
when MaxSize /= infinity,
Size > MaxSize ->
Text = {<<"File larger than ~w bytes">>, [MaxSize]},
?WARNING_MSG("Rejecting file ~s from ~s (too large: ~B bytes)",
[File, jid:encode(JID), Size]),
Error = xmpp:err_not_acceptable(Text, Lang),
Els = xmpp:get_els(Error),
Els1 = [#upload_file_too_large{'max-file-size' = MaxSize,
xmlns = XMLNS} | Els],
Error1 = xmpp:set_els(Error, Els1),
{error, Error1};
create_slot(#state{service_url = undefined,
jid_in_url = JIDinURL,
secret_length = SecretLength,
server_host = ServerHost,
docroot = DocRoot},
JID, File, Size, _ContentType, _XMLNS, Lang) ->
UserStr = make_user_string(JID, JIDinURL),
UserDir = <<DocRoot/binary, $/, UserStr/binary>>,
case ejabberd_hooks:run_fold(http_upload_slot_request, ServerHost, allow,
[ServerHost, JID, UserDir, Size, Lang]) of
allow ->
RandStr = p1_rand:get_alphanum_string(SecretLength),
FileStr = make_file_string(File),
?INFO_MSG("Got HTTP upload slot for ~s (file: ~s, size: ~B)",
[jid:encode(JID), File, Size]),
{ok, [UserStr, RandStr, FileStr]};
deny ->
{error, xmpp:err_service_unavailable()};
#stanza_error{} = Error ->
{error, Error}
end;
create_slot(#state{service_url = ServiceURL},
#jid{luser = U, lserver = S} = JID,
File, Size, ContentType, _XMLNS, Lang) ->
Options = [{body_format, binary}, {full_result, false}],
HttpOptions = [{timeout, ?SERVICE_REQUEST_TIMEOUT}],
SizeStr = integer_to_binary(Size),
JidStr = jid:encode({U, S, <<"">>}),
GetRequest = <<ServiceURL/binary,
"?jid=", (misc:url_encode(JidStr))/binary,
"&name=", (misc:url_encode(File))/binary,
"&size=", (misc:url_encode(SizeStr))/binary,
"&content_type=", (misc:url_encode(ContentType))/binary>>,
case httpc:request(get, {binary_to_list(GetRequest), []},
HttpOptions, Options) of
{ok, {Code, Body}} when Code >= 200, Code =< 299 ->
case binary:split(Body, <<$\n>>, [global, trim]) of
[<<"http", _/binary>> = PutURL,
<<"http", _/binary>> = GetURL] ->
?INFO_MSG("Got HTTP upload slot for ~s (file: ~s, size: ~B)",
[jid:encode(JID), File, Size]),
{ok, PutURL, GetURL};
Lines ->
?ERROR_MSG("Can't parse data received for ~s from <~s>: ~p",
[jid:encode(JID), ServiceURL, Lines]),
Txt = <<"Failed to parse HTTP response">>,
{error, xmpp:err_service_unavailable(Txt, Lang)}
end;
{ok, {402, _Body}} ->
?WARNING_MSG("Got status code 402 for ~s from <~s>",
[jid:encode(JID), ServiceURL]),
{error, xmpp:err_resource_constraint()};
{ok, {403, _Body}} ->
?WARNING_MSG("Got status code 403 for ~s from <~s>",
[jid:encode(JID), ServiceURL]),
{error, xmpp:err_not_allowed()};
{ok, {413, _Body}} ->
?WARNING_MSG("Got status code 413 for ~s from <~s>",
[jid:encode(JID), ServiceURL]),
{error, xmpp:err_not_acceptable()};
{ok, {Code, _Body}} ->
?ERROR_MSG("Got unexpected status code for ~s from <~s>: ~B",
[jid:encode(JID), ServiceURL, Code]),
{error, xmpp:err_service_unavailable()};
{error, Reason} ->
?ERROR_MSG("Error requesting upload slot for ~s from <~s>: ~p",
[jid:encode(JID), ServiceURL, Reason]),
{error, xmpp:err_service_unavailable()}
end.
-spec add_slot(slot(), pos_integer(), state()) -> state().
add_slot(Slot, Size, #state{external_secret = <<>>, slots = Slots} = State) ->
TRef = erlang:start_timer(?SLOT_TIMEOUT, self(), Slot),
NewSlots = maps:put(Slot, {Size, TRef}, Slots),
State#state{slots = NewSlots};
add_slot(_Slot, _Size, State) ->
State.
-spec get_slot(slot(), state()) -> {ok, {pos_integer(), reference()}} | error.
get_slot(Slot, #state{slots = Slots}) ->
maps:find(Slot, Slots).
-spec del_slot(slot(), state()) -> state().
del_slot(Slot, #state{slots = Slots} = State) ->
NewSlots = maps:remove(Slot, Slots),
State#state{slots = NewSlots}.
-spec mk_slot(slot(), state(), binary(), binary()) -> upload_slot();
(binary(), binary(), binary(), binary()) -> upload_slot().
mk_slot(Slot, #state{put_url = PutPrefix, get_url = GetPrefix}, XMLNS, Query) ->
PutURL = str:join([PutPrefix | Slot], <<$/>>),
GetURL = str:join([GetPrefix | Slot], <<$/>>),
mk_slot(PutURL, GetURL, XMLNS, Query);
mk_slot(PutURL, GetURL, XMLNS, Query) ->
PutURL1 = <<(misc:url_encode(PutURL))/binary, Query/binary>>,
GetURL1 = misc:url_encode(GetURL),
case XMLNS of
?NS_HTTP_UPLOAD_0 ->
#upload_slot_0{get = GetURL1, put = PutURL1, xmlns = XMLNS};
_ ->
#upload_slot{get = GetURL1, put = PutURL1, xmlns = XMLNS}
end.
-spec make_user_string(jid(), sha1 | node) -> binary().
make_user_string(#jid{luser = U, lserver = S}, sha1) ->
str:sha(<<U/binary, $@, S/binary>>);
make_user_string(#jid{luser = U}, node) ->
replace_special_chars(U).
-spec make_file_string(binary()) -> binary().
make_file_string(File) ->
replace_special_chars(File).
-spec make_query_string(slot(), non_neg_integer(), state()) -> binary().
make_query_string(Slot, Size, #state{external_secret = Key}) when Key /= <<>> ->
UrlPath = str:join(Slot, <<$/>>),
SizeStr = integer_to_binary(Size),
Data = <<UrlPath/binary, " ", SizeStr/binary>>,
HMAC = str:to_hexlist(crypto:hmac(sha256, Key, Data)),
<<"?v=", HMAC/binary>>;
make_query_string(_Slot, _Size, _State) ->
<<>>.
-spec replace_special_chars(binary()) -> binary().
replace_special_chars(S) ->
re:replace(S, <<"[^\\p{Xan}_.-]">>, <<$_>>,
[unicode, global, {return, binary}]).
-spec yield_content_type(binary()) -> binary().
yield_content_type(<<"">>) -> ?DEFAULT_CONTENT_TYPE;
yield_content_type(Type) -> Type.
-spec encode_addr(inet:ip_address() | {inet:ip_address(), inet:port_number()} |
undefined) -> binary().
encode_addr(IP) ->
ejabberd_config:may_hide_data(misc:ip_to_list(IP)).
-spec iq_disco_info(binary(), binary(), binary(), [xdata()]) -> disco_info().
iq_disco_info(Host, Lang, Name, AddInfo) ->
Form = case gen_mod:get_module_opt(Host, ?MODULE, max_size) of
infinity ->
AddInfo;
MaxSize ->
lists:foldl(
fun(NS, Acc) ->
Fs = http_upload:encode(
[{'max-file-size', MaxSize}], NS, Lang),
[#xdata{type = result, fields = Fs}|Acc]
end, AddInfo, [?NS_HTTP_UPLOAD_0, ?NS_HTTP_UPLOAD])
end,
#disco_info{identities = [#identity{category = <<"store">>,
type = <<"file">>,
name = translate:translate(Lang, Name)}],
features = [?NS_HTTP_UPLOAD,
?NS_HTTP_UPLOAD_0,
?NS_HTTP_UPLOAD_OLD,
?NS_DISCO_INFO,
?NS_DISCO_ITEMS],
xdata = Form}.
%% HTTP request handling.
-spec parse_http_request(#request{}) -> {atom(), slot()}.
parse_http_request(#request{host = Host, path = Path}) ->
PrefixLength = length(Path) - 3,
{ProcURL, Slot} = if PrefixLength > 0 ->
Prefix = lists:sublist(Path, PrefixLength),
{str:join([Host | Prefix], $/),
lists:nthtail(PrefixLength, Path)};
true ->
{Host, Path}
end,
{gen_mod:get_module_proc(ProcURL, ?MODULE), Slot}.
-spec store_file(binary(), http_request(),
integer() | undefined,
integer() | undefined,
binary(), slot(), boolean())
-> ok | {ok, [{binary(), binary()}], binary()} | {error, term()}.
store_file(Path, Request, FileMode, DirMode, GetPrefix, Slot, Thumbnail) ->
case do_store_file(Path, Request, FileMode, DirMode) of
ok when Thumbnail ->
case identify(Path) of
{ok, MediaInfo} ->
case convert(Path, MediaInfo) of
{ok, OutPath, OutMediaInfo} ->
[UserDir, RandDir | _] = Slot,
FileName = filename:basename(OutPath),
URL = str:join([GetPrefix, UserDir,
RandDir, FileName], <<$/>>),
ThumbEl = thumb_el(OutMediaInfo, URL),
{ok,
[{<<"Content-Type">>,
<<"text/xml; charset=utf-8">>}],
fxml:element_to_binary(ThumbEl)};
pass ->
ok
end;
pass ->
ok
end;
ok ->
ok;
Err ->
Err
end.
-spec do_store_file(file:filename_all(), http_request(),
integer() | undefined,
integer() | undefined)
-> ok | {error, term()}.
do_store_file(Path, Request, FileMode, DirMode) ->
try
ok = filelib:ensure_dir(Path),
ok = ejabberd_http:recv_file(Request, Path),
if is_integer(FileMode) ->
ok = file:change_mode(Path, FileMode);
FileMode == undefined ->
ok
end,
if is_integer(DirMode) ->
RandDir = filename:dirname(Path),
UserDir = filename:dirname(RandDir),
ok = file:change_mode(RandDir, DirMode),
ok = file:change_mode(UserDir, DirMode);
DirMode == undefined ->
ok
end
catch
_:{badmatch, {error, Error}} ->
{error, Error};
_:Error ->
{error, Error}
end.
-spec guess_content_type(binary()) -> binary().
guess_content_type(FileName) ->
mod_http_fileserver:content_type(FileName,
?DEFAULT_CONTENT_TYPE,
?CONTENT_TYPES).
-spec http_response(100..599)
-> {pos_integer(), [{binary(), binary()}], binary()}.
http_response(Code) ->
http_response(Code, []).
-spec http_response(100..599, [{binary(), binary()}])
-> {pos_integer(), [{binary(), binary()}], binary()}.
http_response(Code, ExtraHeaders) ->
Message = <<(code_to_message(Code))/binary, $\n>>,
http_response(Code, ExtraHeaders, Message).
-type http_body() :: binary() | {file, file:filename()}.
-spec http_response(100..599, [{binary(), binary()}], http_body())
-> {pos_integer(), [{binary(), binary()}], binary()}.
http_response(Code, ExtraHeaders, Body) ->
Headers = case proplists:is_defined(<<"Content-Type">>, ExtraHeaders) of
true ->
ExtraHeaders;
false ->
[{<<"Content-Type">>, <<"text/plain">>} | ExtraHeaders]
end,
{Code, Headers, Body}.
-spec code_to_message(100..599) -> binary().
code_to_message(201) -> <<"Upload successful.">>;
code_to_message(403) -> <<"Forbidden.">>;
code_to_message(404) -> <<"Not found.">>;
code_to_message(405) -> <<"Method not allowed.">>;
code_to_message(413) -> <<"File size doesn't match requested size.">>;
code_to_message(500) -> <<"Internal server error.">>;
code_to_message(_Code) -> <<"">>.
-spec format_error(atom()) -> string().
format_error(Reason) ->
case file:format_error(Reason) of
"unknown POSIX error" ->
case inet:format_error(Reason) of
"unknown POSIX error" ->
atom_to_list(Reason);
Txt ->
Txt
end;
Txt ->
Txt
end.
%%--------------------------------------------------------------------
%% Image manipulation stuff.
%%--------------------------------------------------------------------
-spec identify(binary()) -> {ok, media_info()} | pass.
identify(Path) ->
try
{ok, Fd} = file:open(Path, [read, raw]),
{ok, Data} = file:read(Fd, 1024),
case eimp:identify(Data) of
{ok, Info} ->
{ok, #media_info{
type = proplists:get_value(type, Info),
width = proplists:get_value(width, Info),
height = proplists:get_value(height, Info)}};
{error, Why} ->
?DEBUG("Cannot identify type of ~s: ~s",
[Path, eimp:format_error(Why)]),
pass
end
catch _:{badmatch, {error, Reason}} ->
?DEBUG("Failed to read file ~s: ~s",
[Path, format_error(Reason)]),
pass
end.
-spec convert(binary(), media_info()) -> {ok, binary(), media_info()} | pass.
convert(Path, #media_info{type = T, width = W, height = H} = Info) ->
if W * H >= 25000000 ->
?DEBUG("The image ~s is more than 25 Mpix", [Path]),
pass;
W =< 300, H =< 300 ->
{ok, Path, Info};
true ->
Dir = filename:dirname(Path),
Ext = atom_to_binary(T, latin1),
FileName = <<(p1_rand:get_string())/binary, $., Ext/binary>>,
OutPath = filename:join(Dir, FileName),
{W1, H1} = if W > H -> {300, round(H*300/W)};
H > W -> {round(W*300/H), 300};
true -> {300, 300}
end,
OutInfo = #media_info{type = T, width = W1, height = H1},
case file:read_file(Path) of
{ok, Data} ->
case eimp:convert(Data, T, [{scale, {W1, H1}}]) of
{ok, OutData} ->
case file:write_file(OutPath, OutData) of
ok ->
{ok, OutPath, OutInfo};
{error, Why} ->
?ERROR_MSG("Failed to write to ~s: ~s",
[OutPath, format_error(Why)]),
pass
end;
{error, Why} ->
?ERROR_MSG("Failed to convert ~s to ~s: ~s",
[Path, OutPath, eimp:format_error(Why)]),
pass
end;
{error, Why} ->
?ERROR_MSG("Failed to read file ~s: ~s",
[Path, format_error(Why)]),
pass
end
end.
-spec thumb_el(media_info(), binary()) -> xmlel().
thumb_el(#media_info{type = T, height = H, width = W}, URI) ->
MimeType = <<"image/", (atom_to_binary(T, latin1))/binary>>,
Thumb = #thumbnail{'media-type' = MimeType, uri = URI,
height = H, width = W},
xmpp:encode(Thumb).
%%--------------------------------------------------------------------
%% Remove user.
%%--------------------------------------------------------------------
-spec remove_user(binary(), binary()) -> ok.
remove_user(User, Server) ->
ServerHost = jid:nameprep(Server),
DocRoot = gen_mod:get_module_opt(ServerHost, ?MODULE, docroot),
JIDinURL = gen_mod:get_module_opt(ServerHost, ?MODULE, jid_in_url),
DocRoot1 = expand_host(expand_home(DocRoot), ServerHost),
UserStr = make_user_string(jid:make(User, Server), JIDinURL),
UserDir = str:join([DocRoot1, UserStr], <<$/>>),
case del_tree(UserDir) of
ok ->
?INFO_MSG("Removed HTTP upload directory of ~s@~s", [User, Server]);
{error, enoent} ->
?DEBUG("Found no HTTP upload directory of ~s@~s", [User, Server]);
{error, Error} ->
?ERROR_MSG("Cannot remove HTTP upload directory of ~s@~s: ~s",
[User, Server, format_error(Error)])
end,
ok.
-spec del_tree(file:filename_all()) -> ok | {error, term()}.
del_tree(Dir) when is_binary(Dir) ->
del_tree(binary_to_list(Dir));
del_tree(Dir) ->
try
{ok, Entries} = file:list_dir(Dir),
lists:foreach(fun(Path) ->
case filelib:is_dir(Path) of
true ->
ok = del_tree(Path);
false ->
ok = file:delete(Path)
end
end, [Dir ++ "/" ++ Entry || Entry <- Entries]),
ok = file:del_dir(Dir)
catch
_:{badmatch, {error, Error}} ->
{error, Error};
_:Error ->
{error, Error}
end.