summaryrefslogtreecommitdiff
path: root/src/mod_http_upload.erl
diff options
context:
space:
mode:
Diffstat (limited to 'src/mod_http_upload.erl')
-rw-r--r--src/mod_http_upload.erl1043
1 files changed, 1043 insertions, 0 deletions
diff --git a/src/mod_http_upload.erl b/src/mod_http_upload.erl
new file mode 100644
index 00000000..6c029c43
--- /dev/null
+++ b/src/mod_http_upload.erl
@@ -0,0 +1,1043 @@
+%%%----------------------------------------------------------------------
+%%% 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-2016 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(GEN_SERVER, gen_server).
+-define(SERVICE_REQUEST_TIMEOUT, 5000). % 5 seconds.
+-define(SLOT_TIMEOUT, 18000000). % 5 hours.
+-define(PROCNAME, ?MODULE).
+-define(FORMAT(Error), file:format_error(Error)).
+-define(URL_ENC(URL), binary_to_list(ejabberd_http:url_encode(URL))).
+-define(ADDR_TO_STR(IP), ejabberd_config:may_hide_data(jlib:ip_to_list(IP))).
+-define(STR_TO_INT(Str, B), jlib:binary_to_integer(iolist_to_binary(Str), B)).
+-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">>},
+ {<<".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_link/3,
+ start/2,
+ stop/1,
+ mod_opt_type/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.hrl").
+-include("ejabberd_http.hrl").
+-include("jlib.hrl").
+-include("logger.hrl").
+
+-record(state,
+ {server_host :: binary(),
+ host :: 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(),
+ slots = #{} :: map()}).
+
+-record(media_info,
+ {type :: binary(),
+ height :: integer(),
+ width :: integer()}).
+
+-type state() :: #state{}.
+-type slot() :: [binary(), ...].
+-type media_info() :: #media_info{}.
+
+%%--------------------------------------------------------------------
+%% gen_mod/supervisor callbacks.
+%%--------------------------------------------------------------------
+
+-spec start_link(binary(), atom(), gen_mod:opts())
+ -> {ok, pid()} | ignore | {error, _}.
+
+start_link(ServerHost, Proc, Opts) ->
+ ?GEN_SERVER:start_link({local, Proc}, ?MODULE, {ServerHost, Opts}, []).
+
+-spec start(binary(), gen_mod:opts()) -> {ok, _} | {ok, _, _} | {error, _}.
+
+start(ServerHost, Opts) ->
+ case gen_mod:get_opt(rm_on_unregister, Opts,
+ fun(B) when is_boolean(B) -> B end,
+ true) of
+ true ->
+ ejabberd_hooks:add(remove_user, ServerHost, ?MODULE,
+ remove_user, 50),
+ ejabberd_hooks:add(anonymous_purge_hook, ServerHost, ?MODULE,
+ remove_user, 50);
+ false ->
+ ok
+ end,
+ Proc = get_proc_name(ServerHost, ?PROCNAME),
+ Spec = {Proc,
+ {?MODULE, start_link, [ServerHost, Proc, Opts]},
+ permanent,
+ 3000,
+ worker,
+ [?MODULE]},
+ supervisor:start_child(ejabberd_sup, Spec).
+
+-spec stop(binary()) -> ok.
+
+stop(ServerHost) ->
+ case gen_mod:get_module_opt(ServerHost, ?MODULE, rm_on_unregister,
+ fun(B) when is_boolean(B) -> B end,
+ true) of
+ true ->
+ ejabberd_hooks:delete(remove_user, ServerHost, ?MODULE,
+ remove_user, 50),
+ ejabberd_hooks:delete(anonymous_purge_hook, ServerHost, ?MODULE,
+ remove_user, 50);
+ false ->
+ ok
+ end,
+ Proc = get_proc_name(ServerHost, ?PROCNAME),
+ supervisor:terminate_child(ejabberd_sup, Proc),
+ supervisor:delete_child(ejabberd_sup, Proc).
+
+-spec mod_opt_type(atom()) -> fun((term()) -> term()) | [atom()].
+
+mod_opt_type(host) ->
+ fun iolist_to_binary/1;
+mod_opt_type(name) ->
+ fun iolist_to_binary/1;
+mod_opt_type(access) ->
+ fun(A) when is_atom(A) -> A end;
+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(Mode) -> ?STR_TO_INT(Mode, 8) end;
+mod_opt_type(dir_mode) ->
+ fun(Mode) -> ?STR_TO_INT(Mode, 8) end;
+mod_opt_type(docroot) ->
+ fun iolist_to_binary/1;
+mod_opt_type(put_url) ->
+ fun(<<"http://", _/binary>> = URL) -> URL;
+ (<<"https://", _/binary>> = URL) -> URL
+ end;
+mod_opt_type(get_url) ->
+ fun(<<"http://", _/binary>> = URL) -> URL;
+ (<<"https://", _/binary>> = URL) -> URL
+ end;
+mod_opt_type(service_url) ->
+ fun(<<"http://", _/binary>> = URL) -> URL;
+ (<<"https://", _/binary>> = 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(B) when is_boolean(B) -> B end;
+mod_opt_type(_) ->
+ [host, name, access, max_size, secret_length, jid_in_url, file_mode,
+ dir_mode, docroot, put_url, get_url, service_url, custom_headers,
+ rm_on_unregister, thumbnail].
+
+%%--------------------------------------------------------------------
+%% gen_server callbacks.
+%%--------------------------------------------------------------------
+
+-spec init({binary(), gen_mod:opts()}) -> {ok, state()}.
+
+init({ServerHost, Opts}) ->
+ process_flag(trap_exit, true),
+ Host = gen_mod:get_opt_host(ServerHost, Opts, <<"upload.@HOST@">>),
+ Name = gen_mod:get_opt(name, Opts,
+ fun iolist_to_binary/1,
+ <<"HTTP File Upload">>),
+ Access = gen_mod:get_opt(access, Opts,
+ fun(A) when is_atom(A) -> A end,
+ local),
+ MaxSize = gen_mod:get_opt(max_size, Opts,
+ fun(I) when is_integer(I), I > 0 -> I;
+ (infinity) -> infinity
+ end,
+ 104857600),
+ SecretLength = gen_mod:get_opt(secret_length, Opts,
+ fun(I) when is_integer(I), I >= 8 -> I end,
+ 40),
+ JIDinURL = gen_mod:get_opt(jid_in_url, Opts,
+ fun(sha1) -> sha1;
+ (node) -> node
+ end,
+ sha1),
+ DocRoot = gen_mod:get_opt(docroot, Opts,
+ fun iolist_to_binary/1,
+ <<"@HOME@/upload">>),
+ FileMode = gen_mod:get_opt(file_mode, Opts,
+ fun(Mode) -> ?STR_TO_INT(Mode, 8) end),
+ DirMode = gen_mod:get_opt(dir_mode, Opts,
+ fun(Mode) -> ?STR_TO_INT(Mode, 8) end),
+ PutURL = gen_mod:get_opt(put_url, Opts,
+ fun(<<"http://", _/binary>> = URL) -> URL;
+ (<<"https://", _/binary>> = URL) -> URL
+ end,
+ <<"http://@HOST@:5444">>),
+ GetURL = gen_mod:get_opt(get_url, Opts,
+ fun(<<"http://", _/binary>> = URL) -> URL;
+ (<<"https://", _/binary>> = URL) -> URL
+ end,
+ PutURL),
+ ServiceURL = gen_mod:get_opt(service_url, Opts,
+ fun(<<"http://", _/binary>> = URL) -> URL;
+ (<<"https://", _/binary>> = URL) -> URL
+ end),
+ Thumbnail = gen_mod:get_opt(thumbnail, Opts,
+ fun(B) when is_boolean(B) -> B end,
+ true),
+ DocRoot1 = expand_home(str:strip(DocRoot, right, $/)),
+ DocRoot2 = expand_host(DocRoot1, ServerHost),
+ case ServiceURL of
+ undefined ->
+ ok;
+ <<"http://", _/binary>> ->
+ application:start(inets);
+ <<"https://", _/binary>> ->
+ application:start(inets),
+ application:start(crypto),
+ application:start(asn1),
+ application:start(public_key),
+ application:start(ssl)
+ end,
+ case DirMode of
+ undefined ->
+ ok;
+ Mode ->
+ file:change_mode(DocRoot2, Mode)
+ end,
+ case Thumbnail of
+ true ->
+ case string:str(os:cmd("identify"), "Magick") of
+ 0 ->
+ ?ERROR_MSG("Cannot find 'identify' command, please install "
+ "ImageMagick or disable thumbnail creation", []);
+ _ ->
+ ok
+ end;
+ false ->
+ ok
+ end,
+ ejabberd_router:register_route(Host, ServerHost),
+ {ok, #state{server_host = ServerHost, host = Host, 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}}.
+
+-spec handle_call(_, {pid(), _}, state())
+ -> {reply, {ok, pos_integer(), binary(),
+ pos_integer() | undefined,
+ pos_integer() | undefined}, state()} |
+ {reply, {error, binary()}, state()} | {noreply, state()}.
+
+handle_call({use_slot, Slot}, _From, #state{file_mode = FileMode,
+ dir_mode = DirMode,
+ get_url = GetPrefix,
+ thumbnail = Thumbnail,
+ docroot = DocRoot} = State) ->
+ case get_slot(Slot, State) of
+ {ok, {Size, Timer}} ->
+ timer:cancel(Timer),
+ NewState = del_slot(Slot, State),
+ Path = str:join([DocRoot | Slot], <<$/>>),
+ {reply, {ok, Size, Path, FileMode, DirMode, GetPrefix, Thumbnail},
+ NewState};
+ error ->
+ {reply, {error, <<"Invalid slot">>}, State}
+ end;
+handle_call(get_docroot, _From, #state{docroot = DocRoot} = State) ->
+ {reply, {ok, DocRoot}, 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, From, To, #xmlel{name = <<"iq">>} = Stanza}, State) ->
+ Request = jlib:iq_query_info(Stanza),
+ {Reply, NewState} = case process_iq(From, Request, 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(To, From, jlib:iq_to_xml(Reply));
+ true ->
+ ok
+ end,
+ {noreply, NewState};
+handle_info({slot_timed_out, 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, _} | _, _) -> ok.
+
+terminate(Reason, #state{server_host = ServerHost, host = Host}) ->
+ ?DEBUG("Stopping HTTP upload process for ~s: ~p", [ServerHost, Reason]),
+ ejabberd_router:unregister_route(Host),
+ ok.
+
+-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, ?ADDR_TO_STR(IP), Host]),
+ http_response(Host, 404);
+process(_LocalPath, #request{method = 'PUT', host = Host, ip = IP,
+ data = Data} = Request) ->
+ {Proc, Slot} = parse_http_request(Request),
+ case catch gen_server:call(Proc, {use_slot, Slot}) of
+ {ok, Size, Path, FileMode, DirMode, GetPrefix, Thumbnail}
+ when byte_size(Data) == Size ->
+ ?DEBUG("Storing file from ~s for ~s: ~s",
+ [?ADDR_TO_STR(IP), Host, Path]),
+ case store_file(Path, Data, FileMode, DirMode,
+ GetPrefix, Slot, Thumbnail) of
+ ok ->
+ http_response(Host, 201);
+ {ok, Headers, OutData} ->
+ http_response(Host, 201, Headers, OutData);
+ {error, Error} ->
+ ?ERROR_MSG("Cannot store file ~s from ~s for ~s: ~p",
+ [Path, ?ADDR_TO_STR(IP), Host, ?FORMAT(Error)]),
+ http_response(Host, 500)
+ end;
+ {ok, Size, Path, _FileMode, _DirMode, _GetPrefix, _Thumbnail} ->
+ ?INFO_MSG("Rejecting file ~s from ~s for ~s: Size is ~B, not ~B",
+ [Path, ?ADDR_TO_STR(IP), Host, byte_size(Data), Size]),
+ http_response(Host, 413);
+ {error, Error} ->
+ ?INFO_MSG("Rejecting file from ~s for ~s: ~p",
+ [?ADDR_TO_STR(IP), Host, Error]),
+ http_response(Host, 403);
+ Error ->
+ ?ERROR_MSG("Cannot handle PUT request from ~s for ~s: ~p",
+ [?ADDR_TO_STR(IP), Host, Error]),
+ http_response(Host, 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_docroot) of
+ {ok, DocRoot} ->
+ Path = str:join([DocRoot | Slot], <<$/>>),
+ case file:read_file(Path) of
+ {ok, Data} ->
+ ?INFO_MSG("Serving ~s to ~s", [Path, ?ADDR_TO_STR(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],
+ http_response(Host, 200, Headers2, Data);
+ {error, eacces} ->
+ ?INFO_MSG("Cannot serve ~s to ~s: Permission denied",
+ [Path, ?ADDR_TO_STR(IP)]),
+ http_response(Host, 403);
+ {error, enoent} ->
+ ?INFO_MSG("Cannot serve ~s to ~s: No such file",
+ [Path, ?ADDR_TO_STR(IP)]),
+ http_response(Host, 404);
+ {error, eisdir} ->
+ ?INFO_MSG("Cannot serve ~s to ~s: Is a directory",
+ [Path, ?ADDR_TO_STR(IP)]),
+ http_response(Host, 404);
+ {error, Error} ->
+ ?INFO_MSG("Cannot serve ~s to ~s: ~s",
+ [Path, ?ADDR_TO_STR(IP), ?FORMAT(Error)]),
+ http_response(Host, 500)
+ end;
+ Error ->
+ ?ERROR_MSG("Cannot handle ~s request from ~s for ~s: ~p",
+ [Method, ?ADDR_TO_STR(IP), Host, Error]),
+ http_response(Host, 500)
+ end;
+process(_LocalPath, #request{method = 'OPTIONS', host = Host, ip = IP}) ->
+ ?DEBUG("Responding to OPTIONS request from ~s for ~s",
+ [?ADDR_TO_STR(IP), Host]),
+ http_response(Host, 200);
+process(_LocalPath, #request{method = Method, host = Host, ip = IP}) ->
+ ?DEBUG("Rejecting ~s request from ~s for ~s",
+ [Method, ?ADDR_TO_STR(IP), Host]),
+ http_response(Host, 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,
+ fun(<<"http://", _/binary>> = URL) -> URL;
+ (<<"https://", _/binary>> = URL) -> URL;
+ (_) -> <<"http://@HOST@">>
+ end,
+ <<"http://@HOST@">>),
+ {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(Subject) ->
+ {ok, [[Home]]} = init:get_argument(home),
+ Parts = binary:split(Subject, <<"@HOME@">>, [global]),
+ str:join(Parts, list_to_binary(Home)).
+
+-spec expand_host(binary(), binary()) -> binary().
+
+expand_host(Subject, Host) ->
+ Parts = binary:split(Subject, <<"@HOST@">>, [global]),
+ str:join(Parts, Host).
+
+%%--------------------------------------------------------------------
+%% Internal functions.
+%%--------------------------------------------------------------------
+
+%% XMPP request handling.
+
+-spec process_iq(jid(), iq_request() | reply | invalid, state())
+ -> {iq_reply(), state()} | iq_reply() | not_request.
+
+process_iq(_From,
+ #iq{type = get, xmlns = ?NS_DISCO_INFO, lang = Lang} = IQ,
+ #state{server_host = ServerHost, name = Name}) ->
+ AddInfo = ejabberd_hooks:run_fold(disco_info, ServerHost, [],
+ [ServerHost, ?MODULE, <<"">>, <<"">>]),
+ IQ#iq{type = result,
+ sub_el = [#xmlel{name = <<"query">>,
+ attrs = [{<<"xmlns">>, ?NS_DISCO_INFO}],
+ children = iq_disco_info(ServerHost, Lang, Name)
+ ++ AddInfo}]};
+process_iq(From,
+ #iq{type = get, xmlns = XMLNS, lang = Lang, sub_el = SubEl} = IQ,
+ #state{server_host = ServerHost, access = Access} = State)
+ when XMLNS == ?NS_HTTP_UPLOAD;
+ XMLNS == ?NS_HTTP_UPLOAD_OLD ->
+ case acl:match_rule(ServerHost, Access, From) of
+ allow ->
+ case parse_request(SubEl, Lang) of
+ {ok, File, Size, ContentType} ->
+ case create_slot(State, From, File, Size, ContentType,
+ Lang) of
+ {ok, Slot} ->
+ {ok, Timer} = timer:send_after(?SLOT_TIMEOUT,
+ {slot_timed_out,
+ Slot}),
+ NewState = add_slot(Slot, Size, Timer, State),
+ SlotEl = slot_el(Slot, State, XMLNS),
+ {IQ#iq{type = result, sub_el = [SlotEl]}, NewState};
+ {ok, PutURL, GetURL} ->
+ SlotEl = slot_el(PutURL, GetURL, XMLNS),
+ IQ#iq{type = result, sub_el = [SlotEl]};
+ {error, Error} ->
+ IQ#iq{type = error, sub_el = [SubEl, Error]}
+ end;
+ {error, Error} ->
+ ?DEBUG("Cannot parse request from ~s",
+ [jid:to_string(From)]),
+ IQ#iq{type = error, sub_el = [SubEl, Error]}
+ end;
+ deny ->
+ ?DEBUG("Denying HTTP upload slot request from ~s",
+ [jid:to_string(From)]),
+ IQ#iq{type = error, sub_el = [SubEl, ?ERR_FORBIDDEN]}
+ end;
+process_iq(_From, #iq{sub_el = SubEl} = IQ, _State) ->
+ IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]};
+process_iq(_From, reply, _State) ->
+ not_request;
+process_iq(_From, invalid, _State) ->
+ not_request.
+
+-spec parse_request(xmlel(), binary())
+ -> {ok, binary(), pos_integer(), binary()} | {error, xmlel()}.
+
+parse_request(#xmlel{name = <<"request">>, attrs = Attrs} = Request, Lang) ->
+ case fxml:get_attr(<<"xmlns">>, Attrs) of
+ {value, XMLNS} when XMLNS == ?NS_HTTP_UPLOAD;
+ XMLNS == ?NS_HTTP_UPLOAD_OLD ->
+ case {fxml:get_subtag_cdata(Request, <<"filename">>),
+ fxml:get_subtag_cdata(Request, <<"size">>),
+ fxml:get_subtag_cdata(Request, <<"content-type">>)} of
+ {File, SizeStr, ContentType} when byte_size(File) > 0 ->
+ case catch jlib:binary_to_integer(SizeStr) of
+ Size when is_integer(Size), Size > 0 ->
+ {ok, File, Size, yield_content_type(ContentType)};
+ _ ->
+ Text = <<"Please specify file size.">>,
+ {error, ?ERRT_BAD_REQUEST(Lang, Text)}
+ end;
+ _ ->
+ Text = <<"Please specify file name.">>,
+ {error, ?ERRT_BAD_REQUEST(Lang, Text)}
+ end;
+ _ ->
+ {error, ?ERR_BAD_REQUEST}
+ end;
+parse_request(_El, _Lang) -> {error, ?ERR_BAD_REQUEST}.
+
+-spec create_slot(state(), jid(), binary(), pos_integer(), binary(), binary())
+ -> {ok, slot()} | {ok, binary(), binary()} | {error, xmlel()}.
+
+create_slot(#state{service_url = undefined, max_size = MaxSize},
+ JID, File, Size, _ContentType, Lang) when MaxSize /= infinity,
+ Size > MaxSize ->
+ Text = <<"File larger than ", (jlib:integer_to_binary(MaxSize))/binary,
+ " Bytes.">>,
+ ?INFO_MSG("Rejecting file ~s from ~s (too large: ~B bytes)",
+ [File, jid:to_string(JID), Size]),
+ {error, ?ERRT_NOT_ACCEPTABLE(Lang, Text)};
+create_slot(#state{service_url = undefined,
+ jid_in_url = JIDinURL,
+ secret_length = SecretLength,
+ server_host = ServerHost,
+ docroot = DocRoot},
+ JID, File, Size, _ContentType, Lang) ->
+ UserStr = make_user_string(JID, JIDinURL),
+ UserDir = <<DocRoot/binary, $/, UserStr/binary>>,
+ case ejabberd_hooks:run_fold(http_upload_slot_request, ServerHost, allow,
+ [JID, UserDir, Size, Lang]) of
+ allow ->
+ RandStr = make_rand_string(SecretLength),
+ FileStr = make_file_string(File),
+ ?INFO_MSG("Got HTTP upload slot for ~s (file: ~s)",
+ [jid:to_string(JID), File]),
+ {ok, [UserStr, RandStr, FileStr]};
+ deny ->
+ {error, ?ERR_SERVICE_UNAVAILABLE};
+ #xmlel{} = Error ->
+ {error, Error}
+ end;
+create_slot(#state{service_url = ServiceURL},
+ #jid{luser = U, lserver = S} = JID, File, Size, ContentType,
+ _Lang) ->
+ Options = [{body_format, binary}, {full_result, false}],
+ HttpOptions = [{timeout, ?SERVICE_REQUEST_TIMEOUT}],
+ SizeStr = jlib:integer_to_binary(Size),
+ GetRequest = binary_to_list(ServiceURL) ++
+ "?jid=" ++ ?URL_ENC(jid:to_string({U, S, <<"">>})) ++
+ "&name=" ++ ?URL_ENC(File) ++
+ "&size=" ++ ?URL_ENC(SizeStr) ++
+ "&content_type=" ++ ?URL_ENC(ContentType),
+ case httpc:request(get, {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)",
+ [jid:to_string(JID), File]),
+ {ok, PutURL, GetURL};
+ Lines ->
+ ?ERROR_MSG("Can't parse data received for ~s from <~s>: ~p",
+ [jid:to_string(JID), ServiceURL, Lines]),
+ {error, ?ERR_SERVICE_UNAVAILABLE}
+ end;
+ {ok, {402, _Body}} ->
+ ?INFO_MSG("Got status code 402 for ~s from <~s>",
+ [jid:to_string(JID), ServiceURL]),
+ {error, ?ERR_RESOURCE_CONSTRAINT};
+ {ok, {403, _Body}} ->
+ ?INFO_MSG("Got status code 403 for ~s from <~s>",
+ [jid:to_string(JID), ServiceURL]),
+ {error, ?ERR_NOT_ALLOWED};
+ {ok, {413, _Body}} ->
+ ?INFO_MSG("Got status code 413 for ~s from <~s>",
+ [jid:to_string(JID), ServiceURL]),
+ {error, ?ERR_NOT_ACCEPTABLE};
+ {ok, {Code, _Body}} ->
+ ?ERROR_MSG("Got unexpected status code for ~s from <~s>: ~B",
+ [jid:to_string(JID), ServiceURL, Code]),
+ {error, ?ERR_SERVICE_UNAVAILABLE};
+ {error, Reason} ->
+ ?ERROR_MSG("Error requesting upload slot for ~s from <~s>: ~p",
+ [jid:to_string(JID), ServiceURL, Reason]),
+ {error, ?ERR_SERVICE_UNAVAILABLE}
+ end.
+
+-spec add_slot(slot(), pos_integer(), timer:tref(), state()) -> state().
+
+add_slot(Slot, Size, Timer, #state{slots = Slots} = State) ->
+ NewSlots = maps:put(Slot, {Size, Timer}, Slots),
+ State#state{slots = NewSlots}.
+
+-spec get_slot(slot(), state()) -> {ok, {pos_integer(), timer:tref()}} | 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 slot_el(slot() | binary(), state() | binary(), binary()) -> xmlel().
+
+slot_el(Slot, #state{put_url = PutPrefix, get_url = GetPrefix}, XMLNS) ->
+ PutURL = str:join([PutPrefix | Slot], <<$/>>),
+ GetURL = str:join([GetPrefix | Slot], <<$/>>),
+ slot_el(PutURL, GetURL, XMLNS);
+slot_el(PutURL, GetURL, XMLNS) ->
+ #xmlel{name = <<"slot">>,
+ attrs = [{<<"xmlns">>, XMLNS}],
+ children = [#xmlel{name = <<"put">>,
+ children = [{xmlcdata, PutURL}]},
+ #xmlel{name = <<"get">>,
+ children = [{xmlcdata, GetURL}]}]}.
+
+-spec make_user_string(jid(), sha1 | node) -> binary().
+
+make_user_string(#jid{luser = U, lserver = S}, sha1) ->
+ p1_sha:sha(<<U/binary, $@, S/binary>>);
+make_user_string(#jid{luser = U}, node) ->
+ re:replace(U, <<"[^a-zA-Z0-9_.-]">>, <<$_>>, [global, {return, binary}]).
+
+-spec make_file_string(binary()) -> binary().
+
+make_file_string(File) ->
+ re:replace(File, <<"[^a-zA-Z0-9_.-]">>, <<$_>>, [global, {return, binary}]).
+
+-spec make_rand_string(non_neg_integer()) -> binary().
+
+make_rand_string(Length) ->
+ list_to_binary(make_rand_string([], Length)).
+
+-spec make_rand_string(string(), non_neg_integer()) -> string().
+
+make_rand_string(S, 0) -> S;
+make_rand_string(S, N) -> make_rand_string([make_rand_char() | S], N - 1).
+
+-spec make_rand_char() -> char().
+
+make_rand_char() ->
+ map_int_to_char(crypto:rand_uniform(0, 62)).
+
+-spec map_int_to_char(0..61) -> char().
+
+map_int_to_char(N) when N =< 9 -> N + 48; % Digit.
+map_int_to_char(N) when N =< 35 -> N + 55; % Upper-case character.
+map_int_to_char(N) when N =< 61 -> N + 61. % Lower-case character.
+
+-spec yield_content_type(binary()) -> binary().
+
+yield_content_type(<<"">>) -> ?DEFAULT_CONTENT_TYPE;
+yield_content_type(Type) -> Type.
+
+-spec iq_disco_info(binary(), binary(), binary()) -> [xmlel()].
+
+iq_disco_info(Host, Lang, Name) ->
+ Form = case gen_mod:get_module_opt(Host, ?MODULE, max_size,
+ fun(I) when is_integer(I), I > 0 -> I;
+ (infinity) -> infinity
+ end,
+ 104857600) of
+ infinity ->
+ [];
+ MaxSize ->
+ MaxSizeStr = jlib:integer_to_binary(MaxSize),
+ Fields = [#xmlel{name = <<"field">>,
+ attrs = [{<<"type">>, <<"hidden">>},
+ {<<"var">>, <<"FORM_TYPE">>}],
+ children = [#xmlel{name = <<"value">>,
+ children =
+ [{xmlcdata,
+ ?NS_HTTP_UPLOAD}]}]},
+ #xmlel{name = <<"field">>,
+ attrs = [{<<"var">>, <<"max-file-size">>}],
+ children = [#xmlel{name = <<"value">>,
+ children =
+ [{xmlcdata,
+ MaxSizeStr}]}]}],
+ [#xmlel{name = <<"x">>,
+ attrs = [{<<"xmlns">>, ?NS_XDATA},
+ {<<"type">>, <<"result">>}],
+ children = Fields}]
+ end,
+ [#xmlel{name = <<"identity">>,
+ attrs = [{<<"category">>, <<"store">>},
+ {<<"type">>, <<"file">>},
+ {<<"name">>, translate:translate(Lang, Name)}]},
+ #xmlel{name = <<"feature">>,
+ attrs = [{<<"var">>, ?NS_HTTP_UPLOAD}]},
+ #xmlel{name = <<"feature">>,
+ attrs = [{<<"var">>, ?NS_HTTP_UPLOAD_OLD}]} | 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, ?PROCNAME), Slot}.
+
+-spec store_file(binary(), binary(),
+ integer() | undefined,
+ integer() | undefined,
+ binary(), slot(), boolean())
+ -> ok | {ok, [{binary(), binary()}], binary()} | {error, term()}.
+
+store_file(Path, Data, FileMode, DirMode, GetPrefix, Slot, Thumbnail) ->
+ case do_store_file(Path, Data, FileMode, DirMode) of
+ ok when Thumbnail ->
+ case identify(Path) of
+ {ok, MediaInfo} ->
+ case convert(Path, MediaInfo) of
+ {ok, OutPath} ->
+ [UserDir, RandDir | _] = Slot,
+ FileName = filename:basename(OutPath),
+ URL = str:join([GetPrefix, UserDir,
+ RandDir, FileName], <<$/>>),
+ ThumbEl = thumb_el(OutPath, 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(), binary(),
+ integer() | undefined,
+ integer() | undefined)
+ -> ok | {error, term()}.
+
+do_store_file(Path, Data, FileMode, DirMode) ->
+ try
+ ok = filelib:ensure_dir(Path),
+ {ok, Io} = file:open(Path, [write, exclusive, raw]),
+ Ok = file:write(Io, Data),
+ ok = file:close(Io),
+ 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,
+ ok = Ok % Raise an exception if file:write/2 failed.
+ 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(binary(), 100..599)
+ -> {pos_integer(), [{binary(), binary()}], binary()}.
+
+http_response(Host, Code) ->
+ http_response(Host, Code, []).
+
+-spec http_response(binary(), 100..599, [{binary(), binary()}])
+ -> {pos_integer(), [{binary(), binary()}], binary()}.
+
+http_response(Host, Code, ExtraHeaders) ->
+ Message = <<(code_to_message(Code))/binary, $\n>>,
+ http_response(Host, Code, ExtraHeaders, Message).
+
+-spec http_response(binary(), 100..599, [{binary(), binary()}], binary())
+ -> {pos_integer(), [{binary(), binary()}], binary()}.
+
+http_response(Host, Code, ExtraHeaders, Body) ->
+ ServerHeader = {<<"Server">>, <<"ejabberd ", (?VERSION)/binary>>},
+ CustomHeaders =
+ gen_mod:get_module_opt(Host, ?MODULE, custom_headers,
+ fun(Headers) ->
+ lists:map(fun({K, V}) ->
+ {iolist_to_binary(K),
+ iolist_to_binary(V)}
+ end, Headers)
+ end,
+ []),
+ Headers = case proplists:is_defined(<<"Content-Type">>, ExtraHeaders) of
+ true ->
+ [ServerHeader | ExtraHeaders];
+ false ->
+ [ServerHeader, {<<"Content-Type">>, <<"text/plain">>} |
+ ExtraHeaders]
+ end ++ CustomHeaders,
+ {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) -> <<"">>.
+
+%%--------------------------------------------------------------------
+%% Image manipulation stuff.
+%%--------------------------------------------------------------------
+
+-spec identify(binary()) -> {ok, media_info()} | pass.
+
+identify(Path) ->
+ Cmd = io_lib:format("identify -format 'ok %m %h %w' ~s", [Path]),
+ Res = string:strip(os:cmd(Cmd), right, $\n),
+ case string:tokens(Res, " ") of
+ ["ok", T, H, W] ->
+ {ok, #media_info{type = list_to_binary(string:to_lower(T)),
+ height = list_to_integer(H),
+ width = list_to_integer(W)}};
+ _ ->
+ ?DEBUG("Cannot identify type of ~s: ~s", [Path, Res]),
+ pass
+ end.
+
+-spec convert(binary(), media_info()) -> {ok, binary()} | pass.
+
+convert(Path, #media_info{type = T, width = W, height = H}) ->
+ if W * H >= 25000000 ->
+ ?DEBUG("The image ~s is more than 25 Mpix", [Path]),
+ pass;
+ W =< 300, H =< 300 ->
+ {ok, Path};
+ T == <<"gif">>; T == <<"jpeg">>; T == <<"png">>; T == <<"webp">> ->
+ Dir = filename:dirname(Path),
+ FileName = <<(randoms:get_string())/binary, $., T/binary>>,
+ OutPath = filename:join(Dir, FileName),
+ Cmd = io_lib:format("convert -resize 300 ~s ~s", [Path, OutPath]),
+ case os:cmd(Cmd) of
+ "" ->
+ {ok, OutPath};
+ Err ->
+ ?ERROR_MSG("Failed to convert ~s to ~s: ~s",
+ [Path, OutPath, string:strip(Err, right, $\n)]),
+ pass
+ end;
+ true ->
+ ?DEBUG("Won't call 'convert' for unknown type ~s", [T]),
+ pass
+ end.
+
+-spec thumb_el(binary(), binary()) -> xmlel().
+
+thumb_el(Path, URI) ->
+ ContentType = guess_content_type(Path),
+ case identify(Path) of
+ {ok, #media_info{height = H, width = W}} ->
+ #xmlel{name = <<"thumbnail">>,
+ attrs = [{<<"xmlns">>, ?NS_THUMBS_1},
+ {<<"media-type">>, ContentType},
+ {<<"uri">>, URI},
+ {<<"height">>, jlib:integer_to_binary(H)},
+ {<<"width">>, jlib:integer_to_binary(W)}]};
+ pass ->
+ #xmlel{name = <<"thumbnail">>,
+ attrs = [{<<"xmlns">>, ?NS_THUMBS_1},
+ {<<"uri">>, URI},
+ {<<"media-type">>, ContentType}]}
+ end.
+
+%%--------------------------------------------------------------------
+%% 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,
+ fun iolist_to_binary/1,
+ <<"@HOME@/upload">>),
+ JIDinURL = gen_mod:get_module_opt(ServerHost, ?MODULE, jid_in_url,
+ fun(sha1) -> sha1;
+ (node) -> node
+ end,
+ sha1),
+ 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: ~p",
+ [User, Server, ?FORMAT(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.