aboutsummaryrefslogtreecommitdiff
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.erl1265
1 files changed, 614 insertions, 651 deletions
diff --git a/src/mod_http_upload.erl b/src/mod_http_upload.erl
index b166f2b66..035aa3982 100644
--- a/src/mod_http_upload.erl
+++ b/src/mod_http_upload.erl
@@ -5,7 +5,7 @@
%%% Created : 20 Aug 2015 by Holger Weiss <holger@zedat.fu-berlin.de>
%%%
%%%
-%%% ejabberd, Copyright (C) 2015-2016 ProcessOne
+%%% ejabberd, Copyright (C) 2015-2019 ProcessOne
%%%
%%% This program is free software; you can redistribute it and/or
%%% modify it under the terms of the GNU General Public License as
@@ -25,17 +25,13 @@
-module(mod_http_upload).
-author('holger@zedat.fu-berlin.de').
-
+-behaviour(gen_server).
+-behaviour(gen_mod).
-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(CALL_TIMEOUT, 60000). % 1 minute.
+-define(SLOT_TIMEOUT, timer:hours(5)).
-define(DEFAULT_CONTENT_TYPE, <<"application/octet-stream">>).
-define(CONTENT_TYPES,
[{<<".avi">>, <<"video/avi">>},
@@ -45,6 +41,7 @@
{<<".gz">>, <<"application/x-gzip">>},
{<<".jpeg">>, <<"image/jpeg">>},
{<<".jpg">>, <<"image/jpeg">>},
+ {<<".m4a">>, <<"audio/mp4">>},
{<<".mp3">>, <<"audio/mpeg">>},
{<<".mp4">>, <<"video/mp4">>},
{<<".mpeg">>, <<"video/mpeg">>},
@@ -61,15 +58,13 @@
{<<".xz">>, <<"application/x-xz">>},
{<<".zip">>, <<"application/zip">>}]).
--behaviour(?GEN_SERVER).
--behaviour(gen_mod).
-
%% gen_mod/supervisor callbacks.
--export([start_link/3,
- start/2,
+-export([start/2,
stop/1,
+ reload/3,
depends/2,
- mod_opt_type/1]).
+ mod_opt_type/1,
+ mod_options/1]).
%% gen_server callbacks.
-export([init/1,
@@ -90,368 +85,329 @@
expand_home/1,
expand_host/2]).
--include("ejabberd.hrl").
-include("ejabberd_http.hrl").
--include("jlib.hrl").
+-include("xmpp.hrl").
-include("logger.hrl").
+-include("translate.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,
+ {server_host = <<>> :: binary(),
+ hosts = [] :: [binary()],
+ name = <<>> :: binary(),
+ access = none :: atom(),
+ max_size = infinity :: pos_integer() | infinity,
+ secret_length = 40 :: pos_integer(),
+ jid_in_url = sha1 :: sha1 | node,
file_mode :: integer() | undefined,
dir_mode :: integer() | undefined,
- docroot :: binary(),
- put_url :: binary(),
- get_url :: binary(),
+ docroot = <<>> :: binary(),
+ put_url = <<>> :: binary(),
+ get_url = <<>> :: binary(),
service_url :: binary() | undefined,
- thumbnail :: boolean(),
- slots = #{} :: map()}).
+ thumbnail = false :: boolean(),
+ custom_headers = [] :: [{binary(), binary()}],
+ slots = #{} :: slots(),
+ external_secret = <<>> :: binary()}).
-record(media_info,
- {type :: binary(),
+ {path :: binary(),
+ type :: atom(),
height :: integer(),
width :: integer()}).
-type state() :: #state{}.
-type slot() :: [binary(), ...].
+-type slots() :: #{slot() => {pos_integer(), reference()}}.
-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, _}.
-
+-spec start(binary(), gen_mod:opts()) -> {ok, pid()} | {error, term()}.
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.
+ Proc = get_proc_name(ServerHost, ?MODULE),
+ case gen_mod:start_child(?MODULE, ServerHost, Opts, Proc) of
+ {ok, _} = Ret -> Ret;
+ {error, {already_started, _}} = Err ->
+ ?ERROR_MSG("Multiple virtual hosts can't use a single 'put_url' "
+ "without the @HOST@ keyword", []),
+ Err;
+ Err ->
+ Err
+ end.
+-spec stop(binary()) -> ok | {error, any()}.
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()].
+ Proc = get_proc_name(ServerHost, ?MODULE),
+ gen_mod:stop_child(Proc).
+
+-spec reload(binary(), gen_mod:opts(), gen_mod:opts()) -> ok | {ok, pid()} | {error, term()}.
+reload(ServerHost, NewOpts, OldOpts) ->
+ NewURL = mod_http_upload_opt:put_url(NewOpts),
+ OldURL = mod_http_upload_opt:put_url(OldOpts),
+ OldProc = get_proc_name(ServerHost, ?MODULE, OldURL),
+ NewProc = get_proc_name(ServerHost, ?MODULE, NewURL),
+ if OldProc /= NewProc ->
+ gen_mod:stop_child(OldProc),
+ start(ServerHost, NewOpts);
+ true ->
+ gen_server:cast(NewProc, {reload, NewOpts, OldOpts})
+ end.
-mod_opt_type(host) ->
- fun iolist_to_binary/1;
+-spec mod_opt_type(atom()) -> econf:validator().
mod_opt_type(name) ->
- fun iolist_to_binary/1;
+ econf:binary();
mod_opt_type(access) ->
- fun acl:access_rules_validator/1;
+ econf:acl();
mod_opt_type(max_size) ->
- fun(I) when is_integer(I), I > 0 -> I;
- (infinity) -> infinity
- end;
+ econf:pos_int(infinity);
mod_opt_type(secret_length) ->
- fun(I) when is_integer(I), I >= 8 -> I end;
+ econf:int(8, 1000);
mod_opt_type(jid_in_url) ->
- fun(sha1) -> sha1;
- (node) -> node
- end;
+ econf:enum([sha1, node]);
mod_opt_type(file_mode) ->
- fun(Mode) -> ?STR_TO_INT(Mode, 8) end;
+ econf:octal();
mod_opt_type(dir_mode) ->
- fun(Mode) -> ?STR_TO_INT(Mode, 8) end;
+ econf:octal();
mod_opt_type(docroot) ->
- fun iolist_to_binary/1;
+ econf:binary();
mod_opt_type(put_url) ->
- fun(<<"http://", _/binary>> = URL) -> URL;
- (<<"https://", _/binary>> = URL) -> URL
- end;
+ econf:url();
mod_opt_type(get_url) ->
- fun(<<"http://", _/binary>> = URL) -> URL;
- (<<"https://", _/binary>> = URL) -> URL
- end;
+ econf:url();
mod_opt_type(service_url) ->
- fun(<<"http://", _/binary>> = URL) -> URL;
- (<<"https://", _/binary>> = URL) -> URL
- end;
+ econf:url();
mod_opt_type(custom_headers) ->
- fun(Headers) ->
- lists:map(fun({K, V}) ->
- {iolist_to_binary(K), iolist_to_binary(V)}
- end, Headers)
- end;
+ econf:map(econf:binary(), econf:binary());
mod_opt_type(rm_on_unregister) ->
- fun(B) when is_boolean(B) -> B end;
+ econf:bool();
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].
+ econf:and_then(
+ econf:bool(),
+ fun(true) ->
+ case eimp:supported_formats() of
+ [] -> econf:fail(eimp_error);
+ [_|_] -> true
+ end;
+ (false) ->
+ false
+ end);
+mod_opt_type(external_secret) ->
+ econf:binary();
+mod_opt_type(host) ->
+ econf:host();
+mod_opt_type(hosts) ->
+ econf:hosts();
+mod_opt_type(vcard) ->
+ econf:vcard_temp().
+
+-spec mod_options(binary()) -> [{thumbnail, boolean()} |
+ {atom(), any()}].
+mod_options(Host) ->
+ [{host, <<"upload.", Host/binary>>},
+ {hosts, []},
+ {name, ?T("HTTP File Upload")},
+ {vcard, undefined},
+ {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/binary, ":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({binary(), gen_mod:opts()}) -> {ok, state()}.
-
-init({ServerHost, Opts}) ->
+-spec init(list()) -> {ok, state()}.
+init([ServerHost|_]) ->
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 acl:access_rules_validator/1,
- 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
+ Opts = gen_mod:get_module_opts(ServerHost, ?MODULE),
+ Hosts = gen_mod:get_opt_hosts(Opts),
+ case mod_http_upload_opt:rm_on_unregister(Opts) 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;
+ ejabberd_hooks:add(remove_user, ServerHost, ?MODULE,
+ remove_user, 50);
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}}.
+ State = init_state(ServerHost, Hosts, Opts),
+ {ok, State}.
-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,
- docroot = DocRoot} = 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, Timer}} ->
- timer:cancel(Timer),
+ {ok, {Size, TRef}} ->
+ misc:cancel_timer(TRef),
NewState = del_slot(Slot, State),
Path = str:join([DocRoot | Slot], <<$/>>),
- {reply, {ok, Path, FileMode, DirMode, GetPrefix, Thumbnail},
+ {reply,
+ {ok, Path, FileMode, DirMode, GetPrefix, Thumbnail, CustomHeaders},
NewState};
- {ok, {_WrongSize, _Timer}} ->
+ {ok, {_WrongSize, _TRef}} ->
{reply, {error, size_mismatch}, State};
error ->
{reply, {error, invalid_slot}, State}
end;
-handle_call(get_docroot, _From, #state{docroot = DocRoot} = State) ->
- {reply, {ok, DocRoot}, State};
+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]),
+ ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
{noreply, State}.
-spec handle_cast(_, state()) -> {noreply, state()}.
-
+handle_cast({reload, NewOpts, OldOpts},
+ #state{server_host = ServerHost} = State) ->
+ case {mod_http_upload_opt:rm_on_unregister(NewOpts),
+ mod_http_upload_opt:rm_on_unregister(OldOpts)} of
+ {true, false} ->
+ ejabberd_hooks:add(remove_user, ServerHost, ?MODULE,
+ remove_user, 50);
+ {false, true} ->
+ ejabberd_hooks:delete(remove_user, ServerHost, ?MODULE,
+ remove_user, 50);
+ _ ->
+ ok
+ end,
+ NewHosts = gen_mod:get_opt_hosts(NewOpts),
+ OldHosts = gen_mod:get_opt_hosts(OldOpts),
+ lists:foreach(fun ejabberd_router:unregister_route/1, OldHosts -- NewHosts),
+ NewState = init_state(State#state{hosts = NewHosts -- OldHosts}, NewOpts),
+ {noreply, NewState};
handle_cast(Request, State) ->
- ?ERROR_MSG("Got unexpected request: ~p", [Request]),
+ ?WARNING_MSG("Unexpected cast: ~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) ->
+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]),
+ ?WARNING_MSG("Unexpected info: ~p", [Info]),
{noreply, State}.
-spec terminate(normal | shutdown | {shutdown, _} | _, state()) -> 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.
+terminate(Reason, #state{server_host = ServerHost, hosts = Hosts}) ->
+ ?DEBUG("Stopping HTTP upload process for ~ts: ~p", [ServerHost, Reason]),
+ ejabberd_hooks:delete(remove_user, ServerHost, ?MODULE, remove_user, 50),
+ 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]),
+ ?DEBUG("Updating HTTP upload process for ~ts", [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);
+ ?DEBUG("Rejecting ~ts request from ~ts for ~ts: Too few path components",
+ [Method, encode_addr(IP), Host]),
+ http_response(404);
process(_LocalPath, #request{method = 'PUT', host = Host, ip = IP,
- data = Data} = Request) ->
+ length = Length} = Request) ->
{Proc, Slot} = parse_http_request(Request),
- case catch gen_server:call(Proc, {use_slot, Slot, byte_size(Data)}) of
- {ok, Path, FileMode, DirMode, GetPrefix, Thumbnail} ->
- ?DEBUG("Storing file from ~s for ~s: ~s",
- [?ADDR_TO_STR(IP), Host, Path]),
- case store_file(Path, Data, FileMode, DirMode,
+ try gen_server:call(Proc, {use_slot, Slot, Length}, ?CALL_TIMEOUT) of
+ {ok, Path, FileMode, DirMode, GetPrefix, Thumbnail, CustomHeaders} ->
+ ?DEBUG("Storing file from ~ts for ~ts: ~ts",
+ [encode_addr(IP), Host, Path]),
+ case store_file(Path, Request, FileMode, DirMode,
GetPrefix, Slot, Thumbnail) of
ok ->
- http_response(Host, 201);
+ http_response(201, CustomHeaders);
{ok, Headers, OutData} ->
- http_response(Host, 201, Headers, OutData);
+ http_response(201, Headers ++ CustomHeaders, OutData);
+ {error, closed} ->
+ ?DEBUG("Cannot store file ~ts from ~ts for ~ts: connection closed",
+ [Path, encode_addr(IP), Host]),
+ http_response(404);
{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)
+ ?ERROR_MSG("Cannot store file ~ts from ~ts for ~ts: ~ts",
+ [Path, encode_addr(IP), Host, format_error(Error)]),
+ http_response(500)
end;
{error, size_mismatch} ->
- ?INFO_MSG("Rejecting file from ~s for ~s: Unexpected size (~B)",
- [?ADDR_TO_STR(IP), Host, byte_size(Data)]),
- http_response(Host, 413);
+ ?WARNING_MSG("Rejecting file ~ts from ~ts for ~ts: Unexpected size (~B)",
+ [lists:last(Slot), encode_addr(IP), Host, Length]),
+ http_response(413);
{error, invalid_slot} ->
- ?INFO_MSG("Rejecting file from ~s for ~s: Invalid slot",
- [?ADDR_TO_STR(IP), Host]),
- 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)
+ ?WARNING_MSG("Rejecting file ~ts from ~ts for ~ts: Invalid slot",
+ [lists:last(Slot), encode_addr(IP), Host]),
+ http_response(403)
+ catch
+ exit:{noproc, _} ->
+ ?WARNING_MSG("Cannot handle PUT request from ~ts for ~ts: "
+ "Upload not configured for this host",
+ [encode_addr(IP), Host]),
+ http_response(404);
+ _:Error ->
+ ?ERROR_MSG("Cannot handle PUT request from ~ts for ~ts: ~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_docroot) of
- {ok, DocRoot} ->
+ try gen_server:call(Proc, get_conf, ?CALL_TIMEOUT) of
+ {ok, DocRoot, CustomHeaders} ->
Path = str:join([DocRoot | Slot], <<$/>>),
- case file:read_file(Path) of
- {ok, Data} ->
- ?INFO_MSG("Serving ~s to ~s", [Path, ?ADDR_TO_STR(IP)]),
+ case file:open(Path, [read]) of
+ {ok, Fd} ->
+ file:close(Fd),
+ ?INFO_MSG("Serving ~ts to ~ts", [Path, encode_addr(IP)]),
ContentType = guess_content_type(FileName),
Headers1 = case ContentType of
<<"image/", _SubType/binary>> -> [];
@@ -462,68 +418,137 @@ process(_LocalPath, #request{method = Method, host = Host, ip = IP} = Request)
$", FileName/binary, $">>}]
end,
Headers2 = [{<<"Content-Type">>, ContentType} | Headers1],
- http_response(Host, 200, Headers2, Data);
+ Headers3 = Headers2 ++ CustomHeaders,
+ http_response(200, Headers3, {file, Path});
{error, eacces} ->
- ?INFO_MSG("Cannot serve ~s to ~s: Permission denied",
- [Path, ?ADDR_TO_STR(IP)]),
- http_response(Host, 403);
+ ?WARNING_MSG("Cannot serve ~ts to ~ts: Permission denied",
+ [Path, encode_addr(IP)]),
+ http_response(403);
{error, enoent} ->
- ?INFO_MSG("Cannot serve ~s to ~s: No such file",
- [Path, ?ADDR_TO_STR(IP)]),
- http_response(Host, 404);
+ ?WARNING_MSG("Cannot serve ~ts to ~ts: No such file",
+ [Path, encode_addr(IP)]),
+ http_response(404);
{error, eisdir} ->
- ?INFO_MSG("Cannot serve ~s to ~s: Is a directory",
- [Path, ?ADDR_TO_STR(IP)]),
- http_response(Host, 404);
+ ?WARNING_MSG("Cannot serve ~ts to ~ts: Is a directory",
+ [Path, encode_addr(IP)]),
+ http_response(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)
+ ?WARNING_MSG("Cannot serve ~ts to ~ts: ~ts",
+ [Path, encode_addr(IP), format_error(Error)]),
+ http_response(500)
+ end
+ catch
+ exit:{noproc, _} ->
+ ?WARNING_MSG("Cannot handle ~ts request from ~ts for ~ts: "
+ "Upload not configured for this host",
+ [Method, encode_addr(IP), Host]),
+ http_response(404);
+ _:Error ->
+ ?ERROR_MSG("Cannot handle ~ts request from ~ts for ~ts: ~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 ~ts for ~ts",
+ [encode_addr(IP), Host]),
+ {Proc, _Slot} = parse_http_request(Request),
+ try gen_server:call(Proc, get_conf, ?CALL_TIMEOUT) of
+ {ok, _DocRoot, CustomHeaders} ->
+ AllowHeader = {<<"Allow">>, <<"OPTIONS, HEAD, GET, PUT">>},
+ http_response(200, [AllowHeader | CustomHeaders])
+ catch
+ exit:{noproc, _} ->
+ ?WARNING_MSG("Cannot handle OPTIONS request from ~ts for ~ts: "
+ "Upload not configured for this host",
+ [encode_addr(IP), Host]),
+ http_response(404);
+ _:Error ->
+ ?ERROR_MSG("Cannot handle OPTIONS request from ~ts for ~ts: ~p",
+ [encode_addr(IP), Host, Error]),
+ http_response(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">>}]).
+ ?DEBUG("Rejecting ~ts request from ~ts for ~ts",
+ [Method, encode_addr(IP), Host]),
+ http_response(405, [{<<"Allow">>, <<"OPTIONS, HEAD, GET, PUT">>}]).
%%--------------------------------------------------------------------
-%% Exported utility functions.
+%% State initialization
%%--------------------------------------------------------------------
+-spec init_state(binary(), [binary()], gen_mod:opts()) -> state().
+init_state(ServerHost, Hosts, Opts) ->
+ init_state(#state{server_host = ServerHost, hosts = Hosts}, Opts).
+
+-spec init_state(state(), gen_mod:opts()) -> state().
+init_state(#state{server_host = ServerHost, hosts = Hosts} = State, Opts) ->
+ Name = mod_http_upload_opt:name(Opts),
+ Access = mod_http_upload_opt:access(Opts),
+ MaxSize = mod_http_upload_opt:max_size(Opts),
+ SecretLength = mod_http_upload_opt:secret_length(Opts),
+ JIDinURL = mod_http_upload_opt:jid_in_url(Opts),
+ DocRoot = mod_http_upload_opt:docroot(Opts),
+ FileMode = mod_http_upload_opt:file_mode(Opts),
+ DirMode = mod_http_upload_opt:dir_mode(Opts),
+ PutURL = mod_http_upload_opt:put_url(Opts),
+ GetURL = case mod_http_upload_opt:get_url(Opts) of
+ undefined -> PutURL;
+ URL -> URL
+ end,
+ ServiceURL = mod_http_upload_opt:service_url(Opts),
+ Thumbnail = mod_http_upload_opt:thumbnail(Opts),
+ ExternalSecret = mod_http_upload_opt:external_secret(Opts),
+ CustomHeaders = mod_http_upload_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),
+ State#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}.
+%%--------------------------------------------------------------------
+%% 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}} =
+ PutURL = mod_http_upload_opt:put_url(ServerHost),
+ get_proc_name(ServerHost, ModuleName, PutURL).
+
+-spec get_proc_name(binary(), atom(), binary()) -> atom().
+get_proc_name(ServerHost, ModuleName, PutURL) ->
+ %% Once we depend on OTP >= 20.0, we can use binaries with http_uri.
+ {ok, {_Scheme, _UserInfo, Host0, _Port, Path0, _Query}} =
http_uri:parse(binary_to_list(expand_host(PutURL, ServerHost))),
- ProcPrefix = list_to_binary(string:strip(Host ++ Path, right, $/)),
+ Host = jid:nameprep(iolist_to_binary(Host0)),
+ Path = str:strip(iolist_to_binary(Path0), right, $/),
+ ProcPrefix = <<Host/binary, Path/binary>>,
gen_mod:get_module_proc(ProcPrefix, ModuleName).
-spec expand_home(binary()) -> binary().
-
-expand_home(Subject) ->
+expand_home(Input) ->
{ok, [[Home]]} = init:get_argument(home),
- Parts = binary:split(Subject, <<"@HOME@">>, [global]),
- str:join(Parts, list_to_binary(Home)).
+ misc:expand_keyword(<<"@HOME@">>, Input, Home).
-spec expand_host(binary(), binary()) -> binary().
-
-expand_host(Subject, Host) ->
- Parts = binary:split(Subject, <<"@HOST@">>, [global]),
- str:join(Parts, Host).
+expand_host(Input, Host) ->
+ misc:expand_keyword(<<"@HOST@">>, Input, Host).
%%--------------------------------------------------------------------
%% Internal functions.
@@ -531,281 +556,255 @@ expand_host(Subject, Host) ->
%% 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,
+-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, <<"">>, <<"">>]),
- 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 ->
+ 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 = [#vcard_temp{}], lang = Lang} = IQ,
+ #state{server_host = ServerHost}) ->
+ VCard = case mod_http_upload_opt:vcard(ServerHost) of
+ undefined ->
+ #vcard_temp{fn = <<"ejabberd/mod_http_upload">>,
+ url = ejabberd_config:get_uri(),
+ desc = misc:get_descr(
+ Lang, ?T("ejabberd HTTP Upload service"))};
+ V ->
+ V
+ end,
+ xmpp:make_iq_result(IQ, VCard);
+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 = ?T("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 ->
- 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;
+ 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} ->
- ?DEBUG("Cannot parse request from ~s",
- [jid:to_string(From)]),
- IQ#iq{type = error, sub_el = [SubEl, Error]}
+ xmpp:make_error(IQ, Error)
end;
deny ->
- ?DEBUG("Denying HTTP upload slot request from ~s",
- [jid:to_string(From)]),
- Txt = <<"Denied by ACL">>,
- IQ#iq{type = error, sub_el = [SubEl, ?ERRT_FORBIDDEN(Lang, Txt)]}
- 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;
- _ ->
- Text = <<"No or invalid XML namespace">>,
- {error, ?ERRT_BAD_REQUEST(Lang, Text)}
- 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()}.
+ ?DEBUG("Denying HTTP upload slot request from ~ts",
+ [jid:encode(From)]),
+ Txt = ?T("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, xmpp_element()}.
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)};
+ JID, File, Size, _ContentType, XMLNS, Lang)
+ when MaxSize /= infinity,
+ Size > MaxSize ->
+ Text = {?T("File larger than ~w bytes"), [MaxSize]},
+ ?WARNING_MSG("Rejecting file ~ts from ~ts (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, Lang) ->
+ 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,
- [JID, UserDir, Size, Lang]) of
+ [ServerHost, JID, UserDir, Size, Lang]) of
allow ->
- RandStr = make_rand_string(SecretLength),
+ RandStr = p1_rand:get_alphanum_string(SecretLength),
FileStr = make_file_string(File),
- ?INFO_MSG("Got HTTP upload slot for ~s (file: ~s)",
- [jid:to_string(JID), File]),
+ ?INFO_MSG("Got HTTP upload slot for ~ts (file: ~ts, size: ~B)",
+ [jid:encode(JID), File, Size]),
{ok, [UserStr, RandStr, FileStr]};
deny ->
- {error, ?ERR_SERVICE_UNAVAILABLE};
- #xmlel{} = Error ->
+ {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,
- Lang) ->
+ #jid{luser = U, lserver = S} = JID,
+ File, Size, ContentType, _XMLNS, 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
+ 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)",
- [jid:to_string(JID), File]),
+ ?INFO_MSG("Got HTTP upload slot for ~ts (file: ~ts, size: ~B)",
+ [jid:encode(JID), File, Size]),
{ok, PutURL, GetURL};
Lines ->
- ?ERROR_MSG("Can't parse data received for ~s from <~s>: ~p",
- [jid:to_string(JID), ServiceURL, Lines]),
- Txt = <<"Failed to parse HTTP response">>,
- {error, ?ERRT_SERVICE_UNAVAILABLE(Lang, Txt)}
+ ?ERROR_MSG("Can't parse data received for ~ts from <~ts>: ~p",
+ [jid:encode(JID), ServiceURL, Lines]),
+ Txt = ?T("Failed to parse HTTP response"),
+ {error, xmpp:err_service_unavailable(Txt, Lang)}
end;
{ok, {402, _Body}} ->
- ?INFO_MSG("Got status code 402 for ~s from <~s>",
- [jid:to_string(JID), ServiceURL]),
- {error, ?ERR_RESOURCE_CONSTRAINT};
+ ?WARNING_MSG("Got status code 402 for ~ts from <~ts>",
+ [jid:encode(JID), ServiceURL]),
+ {error, xmpp: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};
+ ?WARNING_MSG("Got status code 403 for ~ts from <~ts>",
+ [jid:encode(JID), ServiceURL]),
+ {error, xmpp: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};
+ ?WARNING_MSG("Got status code 413 for ~ts from <~ts>",
+ [jid:encode(JID), ServiceURL]),
+ {error, xmpp: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_MSG("Unexpected status code for ~ts from <~ts>: ~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:to_string(JID), ServiceURL, Reason]),
- {error, ?ERR_SERVICE_UNAVAILABLE}
+ ?ERROR_MSG("Error requesting upload slot for ~ts from <~ts>: ~p",
+ [jid:encode(JID), ServiceURL, Reason]),
+ {error, xmpp: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.
+-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 slot_el(slot() | binary(), state() | binary(), binary()) -> xmlel().
-
-slot_el(Slot, #state{put_url = PutPrefix, get_url = GetPrefix}, XMLNS) ->
+-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], <<$/>>),
- 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}]}]}.
+ 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) ->
- p1_sha:sha(<<U/binary, $@, S/binary>>);
+ str:sha(<<U/binary, $@, S/binary>>);
make_user_string(#jid{luser = U}, node) ->
- re:replace(U, <<"[^a-zA-Z0-9_.-]">>, <<$_>>, [global, {return, binary}]).
+ replace_special_chars(U).
-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.
+ 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 iq_disco_info(binary(), binary(), binary()) -> [xmlel()].
+-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)).
-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
+-spec iq_disco_info(binary(), binary(), binary(), [xdata()]) -> disco_info().
+iq_disco_info(Host, Lang, Name, AddInfo) ->
+ Form = case mod_http_upload_opt:max_size(Host) of
infinity ->
- [];
+ AddInfo;
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}]
+ 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,
- [#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].
+ #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_VCARD,
+ ?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}) ->
+parse_http_request(#request{host = Host0, path = Path}) ->
+ Host = jid:nameprep(Host0),
PrefixLength = length(Path) - 3,
{ProcURL, Slot} = if PrefixLength > 0 ->
Prefix = lists:sublist(Path, PrefixLength),
@@ -814,26 +813,25 @@ parse_http_request(#request{host = Host, path = Path}) ->
true ->
{Host, Path}
end,
- {gen_mod:get_module_proc(ProcURL, ?PROCNAME), Slot}.
+ {gen_mod:get_module_proc(ProcURL, ?MODULE), Slot}.
--spec store_file(binary(), binary(),
+-spec store_file(binary(), http_request(),
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
+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} ->
+ case read_image(Path) of
+ {ok, Data, MediaInfo} ->
+ case convert(Data, MediaInfo) of
+ {ok, #media_info{path = OutPath} = OutMediaInfo} ->
[UserDir, RandDir | _] = Slot,
FileName = filename:basename(OutPath),
URL = str:join([GetPrefix, UserDir,
RandDir, FileName], <<$/>>),
- ThumbEl = thumb_el(OutPath, URL),
+ ThumbEl = thumb_el(OutMediaInfo, URL),
{ok,
[{<<"Content-Type">>,
<<"text/xml; charset=utf-8">>}],
@@ -850,17 +848,14 @@ store_file(Path, Data, FileMode, DirMode, GetPrefix, Slot, Thumbnail) ->
Err
end.
--spec do_store_file(file:filename_all(), binary(),
+-spec do_store_file(file:filename_all(), http_request(),
integer() | undefined,
integer() | undefined)
-> ok | {error, term()}.
-
-do_store_file(Path, Data, FileMode, DirMode) ->
+do_store_file(Path, Request, 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),
+ ok = ejabberd_http:recv_file(Request, Path),
if is_integer(FileMode) ->
ok = file:change_mode(Path, FileMode);
FileMode == undefined ->
@@ -873,60 +868,42 @@ do_store_file(Path, Data, FileMode, DirMode) ->
ok = file:change_mode(UserDir, DirMode);
DirMode == undefined ->
ok
- end,
- ok = Ok % Raise an exception if file:write/2 failed.
+ 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(binary(), 100..599)
+-spec http_response(100..599)
-> {pos_integer(), [{binary(), binary()}], binary()}.
+http_response(Code) ->
+ http_response(Code, []).
-http_response(Host, Code) ->
- http_response(Host, Code, []).
-
--spec http_response(binary(), 100..599, [{binary(), binary()}])
+-spec http_response(100..599, [{binary(), binary()}])
-> {pos_integer(), [{binary(), binary()}], binary()}.
-
-http_response(Host, Code, ExtraHeaders) ->
+http_response(Code, ExtraHeaders) ->
Message = <<(code_to_message(Code))/binary, $\n>>,
- http_response(Host, Code, ExtraHeaders, Message).
+ http_response(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,
- []),
+-type http_body() :: binary() | {file, file:filename_all()}.
+-spec http_response(100..599, [{binary(), binary()}], http_body())
+ -> {pos_integer(), [{binary(), binary()}], http_body()}.
+http_response(Code, ExtraHeaders, Body) ->
Headers = case proplists:is_defined(<<"Content-Type">>, ExtraHeaders) of
true ->
- [ServerHeader | ExtraHeaders];
+ ExtraHeaders;
false ->
- [ServerHeader, {<<"Content-Type">>, <<"text/plain">>} |
- ExtraHeaders]
- end ++ CustomHeaders,
+ [{<<"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.">>;
@@ -935,119 +912,105 @@ 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) ->
- 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]),
+-spec read_image(binary()) -> {ok, binary(), media_info()} | pass.
+read_image(Path) ->
+ case file:read_file(Path) of
+ {ok, Data} ->
+ case eimp:identify(Data) of
+ {ok, Info} ->
+ {ok, Data,
+ #media_info{
+ path = Path,
+ 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 ~ts: ~ts",
+ [Path, eimp:format_error(Why)]),
+ pass
+ end;
+ {error, Reason} ->
+ ?DEBUG("Failed to read file ~ts: ~ts",
+ [Path, format_error(Reason)]),
pass
end.
--spec convert(binary(), media_info()) -> {ok, binary()} | pass.
-
-convert(Path, #media_info{type = T, width = W, height = H}) ->
+-spec convert(binary(), media_info()) -> {ok, media_info()} | pass.
+convert(InData, #media_info{path = Path, type = T, width = W, height = H} = Info) ->
if W * H >= 25000000 ->
- ?DEBUG("The image ~s is more than 25 Mpix", [Path]),
+ ?DEBUG("The image ~ts is more than 25 Mpix", [Path]),
pass;
W =< 300, H =< 300 ->
- {ok, Path};
- T == <<"gif">>; T == <<"jpeg">>; T == <<"png">>; T == <<"webp">> ->
+ {ok, Info};
+ true ->
Dir = filename:dirname(Path),
- FileName = <<(randoms:get_string())/binary, $., T/binary>>,
+ Ext = atom_to_binary(T, latin1),
+ FileName = <<(p1_rand:get_string())/binary, $., Ext/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)]),
+ {W1, H1} = if W > H -> {300, round(H*300/W)};
+ H > W -> {round(W*300/H), 300};
+ true -> {300, 300}
+ end,
+ OutInfo = #media_info{path = OutPath, type = T, width = W1, height = H1},
+ case eimp:convert(InData, T, [{scale, {W1, H1}}]) of
+ {ok, OutData} ->
+ case file:write_file(OutPath, OutData) of
+ ok ->
+ {ok, OutInfo};
+ {error, Why} ->
+ ?ERROR_MSG("Failed to write to ~ts: ~ts",
+ [OutPath, format_error(Why)]),
+ pass
+ end;
+ {error, Why} ->
+ ?ERROR_MSG("Failed to convert ~ts to ~ts: ~ts",
+ [Path, OutPath, eimp:format_error(Why)]),
pass
- end;
- true ->
- ?DEBUG("Won't call 'convert' for unknown type ~s", [T]),
- pass
+ end
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.
+-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,
- 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),
+ DocRoot = mod_http_upload_opt:docroot(ServerHost),
+ JIDinURL = mod_http_upload_opt:jid_in_url(ServerHost),
DocRoot1 = expand_host(expand_home(DocRoot), ServerHost),
- UserStr = make_user_string(jid:make(User, Server, <<"">>), JIDinURL),
+ UserStr = make_user_string(jid:make(User, Server), JIDinURL),
UserDir = str:join([DocRoot1, UserStr], <<$/>>),
- case del_tree(UserDir) of
+ case misc:delete_dir(UserDir) of
ok ->
- ?INFO_MSG("Removed HTTP upload directory of ~s@~s", [User, Server]);
+ ?INFO_MSG("Removed HTTP upload directory of ~ts@~ts", [User, Server]);
{error, enoent} ->
- ?DEBUG("Found no HTTP upload directory of ~s@~s", [User, Server]);
+ ?DEBUG("Found no HTTP upload directory of ~ts@~ts", [User, Server]);
{error, Error} ->
- ?ERROR_MSG("Cannot remove HTTP upload directory of ~s@~s: ~p",
- [User, Server, ?FORMAT(Error)])
+ ?ERROR_MSG("Cannot remove HTTP upload directory of ~ts@~ts: ~ts",
+ [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.