./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, "0.3.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 config_spec/0,
36 supported_features/0]).
37
38 %% IQ and hooks handlers
39 -export([process_iq/5,
40 process_disco_iq/5,
41 disco_local_items/1]).
42
43 %% API
44 -export([get_urls/5]).
45
46 %% mongoose_module_metrics callbacks
47 -export([config_metrics/1]).
48
49 -ignore_xref([behaviour_info/1, disco_local_items/1, process_disco_iq/5, process_iq/5]).
50
51 %%--------------------------------------------------------------------
52 %% API
53 %%--------------------------------------------------------------------
54
55 -spec start(HostType :: mongooseim:host_type(), Opts :: gen_mod:module_opts()) -> ok.
56 start(HostType, Opts = #{iqdisc := IQDisc}) ->
57 6 SubdomainPattern = subdomain_pattern(HostType),
58 6 PacketHandler = mongoose_packet_handler:new(ejabberd_local),
59
60 6 mongoose_domain_api:register_subdomain(HostType, SubdomainPattern, PacketHandler),
61 6 [gen_iq_handler:add_iq_handler_for_subdomain(HostType, SubdomainPattern, Namespace,
62 Component, Fn, #{}, IQDisc) ||
63 6 {Component, Namespace, Fn} <- iq_handlers()],
64 6 mod_http_upload_backend:init(HostType, Opts),
65 6 ejabberd_hooks:add(hooks(HostType)),
66 6 ok.
67
68 -spec stop(HostType :: mongooseim:host_type()) -> ok.
69 stop(HostType) ->
70 6 SubdomainPattern = subdomain_pattern(HostType),
71
72 6 ejabberd_hooks:delete(hooks(HostType)),
73 6 [gen_iq_handler:remove_iq_handler_for_subdomain(HostType, SubdomainPattern, Namespace,
74 Component) ||
75 6 {Component, Namespace, _Fn} <- iq_handlers()],
76
77 6 mongoose_domain_api:unregister_subdomain(HostType, SubdomainPattern),
78 6 ok.
79
80 iq_handlers() ->
81 12 [{ejabberd_local, ?NS_HTTP_UPLOAD_030, fun ?MODULE:process_iq/5},
82 {ejabberd_local, ?NS_DISCO_INFO, fun ?MODULE:process_disco_iq/5}].
83
84 hooks(HostType) ->
85 12 [{disco_local_items, HostType, ?MODULE, disco_local_items, 90}].
86
87 %%--------------------------------------------------------------------
88 %% config_spec
89 %%--------------------------------------------------------------------
90
91 -spec config_spec() -> mongoose_config_spec:config_section().
92 config_spec() ->
93 166 #section{
94 items = #{<<"iqdisc">> => mongoose_config_spec:iqdisc(),
95 <<"host">> => #option{type = binary,
96 validate = subdomain_template,
97 process = fun mongoose_subdomain_utils:make_subdomain_pattern/1},
98 <<"backend">> => #option{type = atom,
99 validate = {module, mod_http_upload}},
100 <<"expiration_time">> => #option{type = integer,
101 validate = positive},
102 <<"token_bytes">> => #option{type = integer,
103 validate = positive},
104 <<"max_file_size">> => #option{type = integer,
105 validate = positive},
106 <<"s3">> => s3_spec()
107 },
108 defaults = #{<<"iqdisc">> => one_queue,
109 <<"host">> => <<"upload.@HOST@">>,
110 <<"backend">> => s3,
111 <<"expiration_time">> => 60,
112 <<"token_bytes">> => 32,
113 <<"max_file_size">> => ?DEFAULT_MAX_FILE_SIZE
114 },
115 required = [<<"s3">>]
116 }.
117
118 s3_spec() ->
119 166 #section{
120 items = #{<<"bucket_url">> => #option{type = binary,
121 validate = url},
122 <<"add_acl">> => #option{type = boolean},
123 <<"region">> => #option{type = binary},
124 <<"access_key_id">> => #option{type = binary},
125 <<"secret_access_key">> => #option{type = binary}
126 },
127 defaults = #{<<"add_acl">> => false},
128 required = [<<"bucket_url">>, <<"region">>, <<"access_key_id">>, <<"secret_access_key">>]
129 }.
130
131 -spec supported_features() -> [atom()].
132 supported_features() ->
133
:-(
[dynamic_domains].
134
135 %%--------------------------------------------------------------------
136 %% IQ and hook handlers
137 %%--------------------------------------------------------------------
138
139 -spec process_iq(Acc :: mongoose_acc:t(), From :: jid:jid(), To :: jid:jid(),
140 IQ :: jlib:iq(), map()) ->
141 {mongoose_acc:t(), jlib:iq() | ignore}.
142 process_iq(Acc, _From, _To, IQ = #iq{type = set, lang = Lang, sub_el = SubEl}, _Extra) ->
143 1 Error = mongoose_xmpp_errors:not_allowed(Lang, <<"IQ set is not allowed for HTTP upload">>),
144 1 {Acc, IQ#iq{type = error, sub_el = [SubEl, Error]}};
145 process_iq(Acc, _From, _To, IQ = #iq{type = get, sub_el = Request}, _Extra) ->
146 9 HostType = mongoose_acc:host_type(Acc),
147 9 Res = case parse_request(Request) of
148 {Filename, Size, ContentType} ->
149 6 Opts = module_opts(HostType),
150 6 case get_urls_helper(HostType, Filename, Size, ContentType, Opts) of
151 {PutUrl, GetUrl, Headers} ->
152 5 compose_iq_reply(IQ, PutUrl, GetUrl, Headers);
153 file_too_large_error ->
154 1 IQ#iq{type = error, sub_el = [file_too_large_error(max_file_size(HostType))]}
155 end;
156
157 bad_request ->
158 3 IQ#iq{type = error, sub_el = [Request, mongoose_xmpp_errors:bad_request()]}
159 end,
160 9 {Acc, Res}.
161
162 -spec process_disco_iq(Acc :: mongoose_acc:t(), From :: jid:jid(), To :: jid:jid(),
163 IQ :: jlib:iq(), map()) ->
164 {mongoose_acc:t(), jlib:iq()}.
165 process_disco_iq(Acc, _From, _To, #iq{type = set, lang = Lang, sub_el = SubEl} = IQ, _Extra) ->
166 1 ErrorMsg = <<"IQ set is not allowed for service discovery">>,
167 1 Error = mongoose_xmpp_errors:not_allowed(Lang, ErrorMsg),
168 1 {Acc, IQ#iq{type = error, sub_el = [SubEl, Error]}};
169 process_disco_iq(Acc, _From, _To, #iq{type = get, lang = Lang, sub_el = SubEl} = IQ, _Extra) ->
170 3 Node = xml:get_tag_attr_s(<<"node">>, SubEl),
171 3 case Node of
172 <<>> ->
173 2 Identity = mongoose_disco:identities_to_xml(disco_identity(Lang)),
174 2 Info = disco_info(mongoose_acc:host_type(Acc)),
175 2 Features = mongoose_disco:features_to_xml([?NS_HTTP_UPLOAD_030]),
176 2 {Acc, IQ#iq{type = result,
177 sub_el = [#xmlel{name = <<"query">>,
178 attrs = [{<<"xmlns">>, ?NS_DISCO_INFO}],
179 children = Identity ++ Info ++ Features}]}};
180 _ ->
181 1 ErrorMsg = <<"Node is not supported by HTTP upload">>,
182 1 Error = mongoose_xmpp_errors:item_not_found(Lang, ErrorMsg),
183 1 {Acc, IQ#iq{type = error, sub_el = [SubEl, Error]}}
184 end.
185
186 -spec disco_local_items(mongoose_disco:item_acc()) -> mongoose_disco:item_acc().
187 disco_local_items(Acc = #{host_type := HostType, to_jid := #jid{lserver = Domain}, node := <<>>, lang := Lang}) ->
188 1 mongoose_disco:add_items([#{jid => subdomain(HostType, Domain), name => my_disco_name(Lang)}], Acc);
189 disco_local_items(Acc) ->
190
:-(
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 8 Opts = module_opts(HostType),
198 8 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 14 MaxFileSize = max_file_size(HostType),
206 14 case Size =< MaxFileSize of
207 true ->
208 11 UTCDateTime = calendar:universal_time(),
209 11 Token = generate_token(HostType),
210 11 mod_http_upload_backend:create_slot(HostType, UTCDateTime, Token, Filename,
211 ContentType, Size, Opts);
212 false ->
213 3 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 5 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 5 IQ#iq{type = result, sub_el =[Slot]}.
255
256
257 -spec token_bytes(mongooseim:host_type()) -> pos_integer().
258 token_bytes(HostType) ->
259 11 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 17 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 14 gen_mod:get_module_opts(HostType, ?MODULE).
269
270
271 -spec generate_token(mongooseim:host_type()) -> binary().
272 generate_token(HostType) ->
273 11 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 9 Keys = [<<"filename">>, <<"size">>, <<"content-type">>],
292 9 [Filename, SizeBin, ContentType] = [exml_query:attr(Request, K) || K <- Keys],
293 9 Size = (catch erlang:binary_to_integer(SizeBin)),
294 9 case is_nonempty_binary(Filename) andalso is_positive_integer(Size) of
295 3 false -> bad_request;
296 6 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 #xmlel{name = <<"x">>,
303 attrs = [{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"result">>}],
304 children = [jlib:form_field({<<"FORM_TYPE">>, <<"hidden">>, ?NS_HTTP_UPLOAD_030}),
305 jlib:form_field({<<"max-file-size">>, MaxFileSizeBin})]}.
306
307
308 -spec header_to_xmlel({Key :: binary(), Value :: binary()}) -> exml:element().
309 header_to_xmlel({Key, Value}) ->
310
:-(
#xmlel{name = <<"header">>,
311 attrs = [{<<"name">>, Key}],
312 children = [#xmlcdata{content = Value}]}.
313
314
315 -spec create_url_xmlel(Name :: binary(), Url :: binary(), Headers :: #{binary() => binary()}) ->
316 exml:element().
317 create_url_xmlel(Name, Url, Headers) ->
318 10 HeadersXml = [header_to_xmlel(H) || H <- maps:to_list(Headers)],
319 10 #xmlel{name = Name, attrs = [{<<"url">>, Url}], children = HeadersXml}.
320
321
322 -spec is_nonempty_binary(term()) -> boolean().
323 8 is_nonempty_binary(<<_, _/binary>>) -> true;
324 1 is_nonempty_binary(_) -> false.
325
326
327 -spec is_positive_integer(term()) -> boolean().
328 7 is_positive_integer(X) when is_integer(X) -> X > 0;
329 1 is_positive_integer(_) -> false.
330
331 config_metrics(HostType) ->
332 12 mongoose_module_metrics:opts_for_module(HostType, ?MODULE, [backend]).
Line Hits Source