./ct_report/coverage/mod_http_upload.COVER.html

1 %%==============================================================================
2 %% Copyright 2016 Erlang Solutions Ltd.
3 %%
4 %% Licensed under the Apache License, Version 2.0 (the "License");
5 %% you may not use this file except in compliance with the License.
6 %% You may obtain a copy of the License at
7 %%
8 %% http://www.apache.org/licenses/LICENSE-2.0
9 %%
10 %% Unless required by applicable law or agreed to in writing, software
11 %% distributed under the License is distributed on an "AS IS" BASIS,
12 %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 %% See the License for the specific language governing permissions and
14 %% limitations under the License.
15 %%==============================================================================
16
17 -module(mod_http_upload).
18 -author('konrad.zemek@erlang-solutions.com').
19 -behaviour(gen_mod).
20 -behaviour(mongoose_module_metrics).
21
22 -xep([{xep, 363}, {version, "1.1.0"}]).
23
24 -include("jlib.hrl").
25 -include("mongoose.hrl").
26 -include("mongoose_config_spec.hrl").
27
28 -define(DEFAULT_TOKEN_BYTES, 32).
29 -define(DEFAULT_MAX_FILE_SIZE, 10 * 1024 * 1024). % 10 MB
30 -define(DEFAULT_SUBHOST, <<"upload.@HOST@">>).
31
32 %% gen_mod callbacks
33 -export([start/2,
34 stop/1,
35 hooks/1,
36 config_spec/0,
37 supported_features/0]).
38
39 %% IQ and hooks handlers
40 -export([process_iq/5,
41 process_disco_iq/5,
42 disco_local_items/3]).
43
44 %% API
45 -export([get_urls/5]).
46
47 %% mongoose_module_metrics callbacks
48 -export([config_metrics/1]).
49
50 %%--------------------------------------------------------------------
51 %% API
52 %%--------------------------------------------------------------------
53
54 -spec start(HostType :: mongooseim:host_type(), Opts :: gen_mod:module_opts()) -> ok.
55 start(HostType, Opts = #{iqdisc := IQDisc}) ->
56 6 SubdomainPattern = subdomain_pattern(HostType),
57 6 PacketHandler = mongoose_packet_handler:new(ejabberd_local),
58
59 6 mongoose_domain_api:register_subdomain(HostType, SubdomainPattern, PacketHandler),
60 6 [gen_iq_handler:add_iq_handler_for_subdomain(HostType, SubdomainPattern, Namespace,
61 Component, Fn, #{}, IQDisc) ||
62 6 {Component, Namespace, Fn} <- iq_handlers()],
63 6 mod_http_upload_backend:init(HostType, Opts),
64 6 ok.
65
66 -spec stop(HostType :: mongooseim:host_type()) -> ok.
67 stop(HostType) ->
68 6 SubdomainPattern = subdomain_pattern(HostType),
69
70 6 [gen_iq_handler:remove_iq_handler_for_subdomain(HostType, SubdomainPattern, Namespace,
71 Component) ||
72 6 {Component, Namespace, _Fn} <- iq_handlers()],
73
74 6 mongoose_domain_api:unregister_subdomain(HostType, SubdomainPattern),
75 6 ok.
76
77 iq_handlers() ->
78 12 [{ejabberd_local, ?NS_HTTP_UPLOAD_030, fun ?MODULE:process_iq/5},
79 {ejabberd_local, ?NS_DISCO_INFO, fun ?MODULE:process_disco_iq/5}].
80
81 hooks(HostType) ->
82 12 [{disco_local_items, HostType, fun ?MODULE:disco_local_items/3, #{}, 90}].
83
84 %%--------------------------------------------------------------------
85 %% config_spec
86 %%--------------------------------------------------------------------
87
88 -spec config_spec() -> mongoose_config_spec:config_section().
89 config_spec() ->
90 106 #section{
91 items = #{<<"iqdisc">> => mongoose_config_spec:iqdisc(),
92 <<"host">> => #option{type = binary,
93 validate = subdomain_template,
94 process = fun mongoose_subdomain_utils:make_subdomain_pattern/1},
95 <<"backend">> => #option{type = atom,
96 validate = {module, mod_http_upload}},
97 <<"expiration_time">> => #option{type = integer,
98 validate = positive},
99 <<"token_bytes">> => #option{type = integer,
100 validate = positive},
101 <<"max_file_size">> => #option{type = integer,
102 validate = positive},
103 <<"s3">> => s3_spec()
104 },
105 defaults = #{<<"iqdisc">> => one_queue,
106 <<"host">> => <<"upload.@HOST@">>,
107 <<"backend">> => s3,
108 <<"expiration_time">> => 60,
109 <<"token_bytes">> => 32,
110 <<"max_file_size">> => ?DEFAULT_MAX_FILE_SIZE
111 },
112 required = [<<"s3">>]
113 }.
114
115 s3_spec() ->
116 106 #section{
117 items = #{<<"bucket_url">> => #option{type = binary,
118 validate = url},
119 <<"add_acl">> => #option{type = boolean},
120 <<"region">> => #option{type = binary},
121 <<"access_key_id">> => #option{type = binary},
122 <<"secret_access_key">> => #option{type = binary}
123 },
124 defaults = #{<<"add_acl">> => false},
125 required = [<<"bucket_url">>, <<"region">>, <<"access_key_id">>, <<"secret_access_key">>]
126 }.
127
128 -spec supported_features() -> [atom()].
129 supported_features() ->
130
:-(
[dynamic_domains].
131
132 %%--------------------------------------------------------------------
133 %% IQ and hook handlers
134 %%--------------------------------------------------------------------
135
136 -spec process_iq(Acc :: mongoose_acc:t(), From :: jid:jid(), To :: jid:jid(),
137 IQ :: jlib:iq(), map()) ->
138 {mongoose_acc:t(), jlib:iq() | ignore}.
139 process_iq(Acc, _From, _To, IQ = #iq{type = set, lang = Lang, sub_el = SubEl}, _Extra) ->
140 1 Error = mongoose_xmpp_errors:not_allowed(Lang, <<"IQ set is not allowed for HTTP upload">>),
141 1 {Acc, IQ#iq{type = error, sub_el = [SubEl, Error]}};
142 process_iq(Acc, _From, _To, IQ = #iq{type = get, sub_el = Request}, _Extra) ->
143 10 HostType = mongoose_acc:host_type(Acc),
144 10 Res = case parse_request(Request) of
145 {Filename, Size, ContentType} ->
146 7 Opts = module_opts(HostType),
147 7 case get_urls_helper(HostType, Filename, Size, ContentType, Opts) of
148 {PutUrl, GetUrl, Headers} ->
149 6 compose_iq_reply(IQ, PutUrl, GetUrl, Headers);
150 file_too_large_error ->
151 1 IQ#iq{type = error, sub_el = [file_too_large_error(max_file_size(HostType))]}
152 end;
153
154 bad_request ->
155 3 IQ#iq{type = error, sub_el = [Request, mongoose_xmpp_errors:bad_request()]}
156 end,
157 10 {Acc, Res}.
158
159 -spec process_disco_iq(Acc :: mongoose_acc:t(), From :: jid:jid(), To :: jid:jid(),
160 IQ :: jlib:iq(), map()) ->
161 {mongoose_acc:t(), jlib:iq()}.
162 process_disco_iq(Acc, _From, _To, #iq{type = set, lang = Lang, sub_el = SubEl} = IQ, _Extra) ->
163 1 ErrorMsg = <<"IQ set is not allowed for service discovery">>,
164 1 Error = mongoose_xmpp_errors:not_allowed(Lang, ErrorMsg),
165 1 {Acc, IQ#iq{type = error, sub_el = [SubEl, Error]}};
166 process_disco_iq(Acc, _From, _To, #iq{type = get, lang = Lang, sub_el = SubEl} = IQ, _Extra) ->
167 3 Node = xml:get_tag_attr_s(<<"node">>, SubEl),
168 3 case Node of
169 <<>> ->
170 2 Identity = mongoose_disco:identities_to_xml(disco_identity(Lang)),
171 2 Info = disco_info(mongoose_acc:host_type(Acc)),
172 2 Features = mongoose_disco:features_to_xml([?NS_HTTP_UPLOAD_030]),
173 2 {Acc, IQ#iq{type = result,
174 sub_el = [#xmlel{name = <<"query">>,
175 attrs = [{<<"xmlns">>, ?NS_DISCO_INFO}],
176 children = Identity ++ Info ++ Features}]}};
177 _ ->
178 1 ErrorMsg = <<"Node is not supported by HTTP upload">>,
179 1 Error = mongoose_xmpp_errors:item_not_found(Lang, ErrorMsg),
180 1 {Acc, IQ#iq{type = error, sub_el = [SubEl, Error]}}
181 end.
182
183 -spec disco_local_items(Acc, Params, Extra) -> {ok, Acc} when
184 Acc :: mongoose_disco:item_acc(),
185 Params :: map(),
186 Extra :: gen_hook:extra().
187 disco_local_items(Acc = #{host_type := HostType, to_jid := #jid{lserver = Domain}, node := <<>>, lang := Lang}, _, _) ->
188 1 {ok, mongoose_disco:add_items([#{jid => subdomain(HostType, Domain), name => my_disco_name(Lang)}], Acc)};
189 disco_local_items(Acc, _, _) ->
190
:-(
{ok, Acc}.
191
192 -spec get_urls(HostType :: mongooseim:host_type(), Filename :: binary(), Size :: pos_integer(),
193 ContentType :: binary() | undefined, Timeout :: pos_integer()) ->
194 file_too_large_error | {PutURL :: binary(), GetURL :: binary(),
195 Headers :: #{binary() => binary()}}.
196 get_urls(HostType, Filename, Size, ContentType, Timeout) ->
197 19 Opts = module_opts(HostType),
198 19 get_urls_helper(HostType, Filename, Size, ContentType, Opts#{expiration_time := Timeout}).
199
200 %%--------------------------------------------------------------------
201 %% Helpers
202 %%--------------------------------------------------------------------
203
204 get_urls_helper(HostType, Filename, Size, ContentType, Opts) ->
205 26 MaxFileSize = max_file_size(HostType),
206 26 case Size =< MaxFileSize of
207 true ->
208 21 UTCDateTime = calendar:universal_time(),
209 21 Token = generate_token(HostType),
210 21 mod_http_upload_backend:create_slot(HostType, UTCDateTime, Token, Filename,
211 ContentType, Size, Opts);
212 false ->
213 5 file_too_large_error
214 end.
215
216 -spec disco_identity(ejabberd:lang()) -> [mongoose_disco:identity()].
217 disco_identity(Lang) ->
218 2 [#{category => <<"store">>,
219 type => <<"file">>,
220 name => my_disco_name(Lang)}].
221
222 -spec disco_info(mongooseim:host_type()) -> [exml:element()].
223 disco_info(HostType) ->
224 2 MaxFileSize = max_file_size(HostType),
225 2 MaxFileSizeBin = integer_to_binary(MaxFileSize),
226 2 [get_disco_info_form(MaxFileSizeBin)].
227
228 -spec subdomain(mongooseim:host_type(), mongooseim:domain_name()) -> mongooseim:domain_name().
229 subdomain(HostType, Domain) ->
230 1 SubdomainPattern = subdomain_pattern(HostType),
231 1 mongoose_subdomain_utils:get_fqdn(SubdomainPattern, Domain).
232
233 -spec subdomain_pattern(mongooseim:host_type()) ->
234 mongoose_subdomain_utils:subdomain_pattern().
235 subdomain_pattern(HostType) ->
236 13 gen_mod:get_module_opt(HostType, ?MODULE, host).
237
238 -spec my_disco_name(ejabberd:lang()) -> binary().
239 my_disco_name(Lang) ->
240 3 translate:translate(Lang, <<"HTTP File Upload">>).
241
242
243 -spec compose_iq_reply(IQ :: jlib:iq(),
244 PutUrl :: binary(),
245 GetUrl :: binary(),
246 Headers :: #{binary() => binary()}) ->
247 Reply :: jlib:iq().
248 compose_iq_reply(IQ, PutUrl, GetUrl, Headers) ->
249 6 Slot = #xmlel{
250 name = <<"slot">>,
251 attrs = [{<<"xmlns">>, ?NS_HTTP_UPLOAD_030}],
252 children = [create_url_xmlel(<<"put">>, PutUrl, Headers),
253 create_url_xmlel(<<"get">>, GetUrl, #{})]},
254 6 IQ#iq{type = result, sub_el =[Slot]}.
255
256
257 -spec token_bytes(mongooseim:host_type()) -> pos_integer().
258 token_bytes(HostType) ->
259 21 gen_mod:get_module_opt(HostType, ?MODULE, token_bytes).
260
261
262 -spec max_file_size(mongooseim:host_type()) -> pos_integer() | undefined.
263 max_file_size(HostType) ->
264 29 gen_mod:get_module_opt(HostType, ?MODULE, max_file_size).
265
266 -spec module_opts(mongooseim:host_type())-> gen_mod:module_opts().
267 module_opts(HostType) ->
268 26 gen_mod:get_module_opts(HostType, ?MODULE).
269
270
271 -spec generate_token(mongooseim:host_type()) -> binary().
272 generate_token(HostType) ->
273 21 base16:encode(crypto:strong_rand_bytes(token_bytes(HostType))).
274
275
276 -spec file_too_large_error(MaxFileSize :: non_neg_integer()) -> exml:element().
277 file_too_large_error(MaxFileSize) ->
278 1 MaxFileSizeBin = integer_to_binary(MaxFileSize),
279 1 MaxSizeEl = #xmlel{name = <<"max-file-size">>,
280 children = [#xmlcdata{content = MaxFileSizeBin}]},
281 1 FileTooLargeEl = #xmlel{name = <<"file-too-large">>,
282 attrs = [{<<"xmlns">>, ?NS_HTTP_UPLOAD_030}],
283 children = [MaxSizeEl]},
284 1 Error0 = mongoose_xmpp_errors:not_acceptable(),
285 1 Error0#xmlel{children = [FileTooLargeEl | Error0#xmlel.children]}.
286
287
288 -spec parse_request(Request :: exml:element()) ->
289 {Filename :: binary(), Size :: integer(), ContentType :: binary() | undefined} | bad_request.
290 parse_request(Request) ->
291 10 Keys = [<<"filename">>, <<"size">>, <<"content-type">>],
292 10 [Filename, SizeBin, ContentType] = [exml_query:attr(Request, K) || K <- Keys],
293 10 Size = (catch erlang:binary_to_integer(SizeBin)),
294 10 case is_nonempty_binary(Filename) andalso is_positive_integer(Size) of
295 3 false -> bad_request;
296 7 true -> {Filename, Size, ContentType}
297 end.
298
299
300 -spec get_disco_info_form(MaxFileSizeBin :: binary()) -> exml:element().
301 get_disco_info_form(MaxFileSizeBin) ->
302 2 Fields = [#{var => <<"max-file-size">>, values => [MaxFileSizeBin]}],
303 2 mongoose_data_forms:form(#{type => <<"result">>, ns => ?NS_HTTP_UPLOAD_030, fields => Fields}).
304
305
306 -spec header_to_xmlel({Key :: binary(), Value :: binary()}) -> exml:element().
307 header_to_xmlel({Key, Value}) ->
308
:-(
#xmlel{name = <<"header">>,
309 attrs = [{<<"name">>, Key}],
310 children = [#xmlcdata{content = Value}]}.
311
312
313 -spec create_url_xmlel(Name :: binary(), Url :: binary(), Headers :: #{binary() => binary()}) ->
314 exml:element().
315 create_url_xmlel(Name, Url, Headers) ->
316 12 HeadersXml = [header_to_xmlel(H) || H <- maps:to_list(Headers)],
317 12 #xmlel{name = Name, attrs = [{<<"url">>, Url}], children = HeadersXml}.
318
319
320 -spec is_nonempty_binary(term()) -> boolean().
321 9 is_nonempty_binary(<<_, _/binary>>) -> true;
322 1 is_nonempty_binary(_) -> false.
323
324
325 -spec is_positive_integer(term()) -> boolean().
326 8 is_positive_integer(X) when is_integer(X) -> X > 0;
327 1 is_positive_integer(_) -> false.
328
329 config_metrics(HostType) ->
330 24 mongoose_module_metrics:opts_for_module(HostType, ?MODULE, [backend]).
Line Hits Source