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