./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 4 SubdomainPattern = subdomain_pattern(HostType),
58 4 PacketHandler = mongoose_packet_handler:new(ejabberd_local),
59
60 4 mongoose_domain_api:register_subdomain(HostType, SubdomainPattern, PacketHandler),
61 4 [gen_iq_handler:add_iq_handler_for_subdomain(HostType, SubdomainPattern, Namespace,
62 Component, Fn, #{}, IQDisc) ||
63 4 {Component, Namespace, Fn} <- iq_handlers()],
64 4 mod_http_upload_backend:init(HostType, Opts),
65 4 ejabberd_hooks:add(hooks(HostType)),
66 4 ok.
67
68 -spec stop(HostType :: mongooseim:host_type()) -> ok.
69 stop(HostType) ->
70 4 SubdomainPattern = subdomain_pattern(HostType),
71
72 4 ejabberd_hooks:delete(hooks(HostType)),
73 4 [gen_iq_handler:remove_iq_handler_for_subdomain(HostType, SubdomainPattern, Namespace,
74 Component) ||
75 4 {Component, Namespace, _Fn} <- iq_handlers()],
76
77 4 mongoose_domain_api:unregister_subdomain(HostType, SubdomainPattern),
78 4 ok.
79
80 iq_handlers() ->
81 8 [{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 8 [{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 152 #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 format_items = map
117 }.
118
119 s3_spec() ->
120 152 #section{
121 items = #{<<"bucket_url">> => #option{type = binary,
122 validate = url},
123 <<"add_acl">> => #option{type = boolean},
124 <<"region">> => #option{type = binary},
125 <<"access_key_id">> => #option{type = binary},
126 <<"secret_access_key">> => #option{type = binary}
127 },
128 defaults = #{<<"add_acl">> => false},
129 required = [<<"bucket_url">>, <<"region">>, <<"access_key_id">>, <<"secret_access_key">>],
130 format_items = map
131 }.
132
133 -spec supported_features() -> [atom()].
134 supported_features() ->
135 6 [dynamic_domains].
136
137 %%--------------------------------------------------------------------
138 %% IQ and hook handlers
139 %%--------------------------------------------------------------------
140
141 -spec process_iq(Acc :: mongoose_acc:t(), From :: jid:jid(), To :: jid:jid(),
142 IQ :: jlib:iq(), map()) ->
143 {mongoose_acc:t(), jlib:iq() | ignore}.
144 process_iq(Acc, _From, _To, IQ = #iq{type = set, lang = Lang, sub_el = SubEl}, _Extra) ->
145 1 Error = mongoose_xmpp_errors:not_allowed(Lang, <<"IQ set is not allowed for HTTP upload">>),
146 1 {Acc, IQ#iq{type = error, sub_el = [SubEl, Error]}};
147 process_iq(Acc, _From, _To, IQ = #iq{type = get, sub_el = Request}, _Extra) ->
148 9 HostType = mongoose_acc:host_type(Acc),
149 9 Res = case parse_request(Request) of
150 {Filename, Size, ContentType} ->
151 6 MaxFileSize = max_file_size(HostType),
152 6 case Size =< MaxFileSize of
153 true ->
154 5 UTCDateTime = calendar:universal_time(),
155 5 Token = generate_token(HostType),
156 5 Opts = module_opts(HostType),
157
158 5 {PutUrl, GetUrl, Headers} =
159 mod_http_upload_backend:create_slot(HostType, UTCDateTime, Token, Filename,
160 ContentType, Size, Opts),
161
162 5 compose_iq_reply(IQ, PutUrl, GetUrl, Headers);
163
164 false ->
165 1 IQ#iq{type = error, sub_el = [file_too_large_error(MaxFileSize)]}
166 end;
167
168 bad_request ->
169 3 IQ#iq{type = error, sub_el = [Request, mongoose_xmpp_errors:bad_request()]}
170 end,
171 9 {Acc, Res}.
172
173 -spec process_disco_iq(Acc :: mongoose_acc:t(), From :: jid:jid(), To :: jid:jid(),
174 IQ :: jlib:iq(), map()) ->
175 {mongoose_acc:t(), jlib:iq()}.
176 process_disco_iq(Acc, _From, _To, #iq{type = set, lang = Lang, sub_el = SubEl} = IQ, _Extra) ->
177 1 ErrorMsg = <<"IQ set is not allowed for service discovery">>,
178 1 Error = mongoose_xmpp_errors:not_allowed(Lang, ErrorMsg),
179 1 {Acc, IQ#iq{type = error, sub_el = [SubEl, Error]}};
180 process_disco_iq(Acc, _From, _To, #iq{type = get, lang = Lang, sub_el = SubEl} = IQ, _Extra) ->
181 3 Node = xml:get_tag_attr_s(<<"node">>, SubEl),
182 3 case Node of
183 <<>> ->
184 2 Identity = mongoose_disco:identities_to_xml(disco_identity(Lang)),
185 2 Info = disco_info(mongoose_acc:host_type(Acc)),
186 2 Features = mongoose_disco:features_to_xml([?NS_HTTP_UPLOAD_030]),
187 2 {Acc, IQ#iq{type = result,
188 sub_el = [#xmlel{name = <<"query">>,
189 attrs = [{<<"xmlns">>, ?NS_DISCO_INFO}],
190 children = Identity ++ Info ++ Features}]}};
191 _ ->
192 1 ErrorMsg = <<"Node is not supported by HTTP upload">>,
193 1 Error = mongoose_xmpp_errors:item_not_found(Lang, ErrorMsg),
194 1 {Acc, IQ#iq{type = error, sub_el = [SubEl, Error]}}
195 end.
196
197 -spec disco_local_items(mongoose_disco:item_acc()) -> mongoose_disco:item_acc().
198 disco_local_items(Acc = #{host_type := HostType, to_jid := #jid{lserver = Domain}, node := <<>>, lang := Lang}) ->
199 1 mongoose_disco:add_items([#{jid => subdomain(HostType, Domain), name => my_disco_name(Lang)}], Acc);
200 disco_local_items(Acc) ->
201
:-(
Acc.
202
203 -spec get_urls(HostType :: mongooseim:host_type(), Filename :: binary(), Size :: pos_integer(),
204 ContentType :: binary() | undefined, Timeout :: pos_integer()) ->
205 {PutURL :: binary(), GetURL :: binary(), Headers :: #{binary() => binary()}}.
206 get_urls(HostType, Filename, Size, ContentType, Timeout) ->
207 4 UTCDateTime = calendar:universal_time(),
208 4 Token = generate_token(HostType),
209 4 Opts = module_opts(HostType),
210 4 NewOpts = Opts#{expiration_time := Timeout},
211 4 mod_http_upload_backend:create_slot(HostType, UTCDateTime, Token, Filename,
212 ContentType, Size, NewOpts).
213
214 %%--------------------------------------------------------------------
215 %% Helpers
216 %%--------------------------------------------------------------------
217
218 -spec disco_identity(ejabberd:lang()) -> [mongoose_disco:identity()].
219 disco_identity(Lang) ->
220 2 [#{category => <<"store">>,
221 type => <<"file">>,
222 name => my_disco_name(Lang)}].
223
224 -spec disco_info(mongooseim:host_type()) -> [exml:element()].
225 disco_info(HostType) ->
226 2 MaxFileSize = max_file_size(HostType),
227 2 MaxFileSizeBin = integer_to_binary(MaxFileSize),
228 2 [get_disco_info_form(MaxFileSizeBin)].
229
230 -spec subdomain(mongooseim:host_type(), mongooseim:domain_name()) -> mongooseim:domain_name().
231 subdomain(HostType, Domain) ->
232 1 SubdomainPattern = subdomain_pattern(HostType),
233 1 mongoose_subdomain_utils:get_fqdn(SubdomainPattern, Domain).
234
235 -spec subdomain_pattern(mongooseim:host_type()) ->
236 mongoose_subdomain_utils:subdomain_pattern().
237 subdomain_pattern(HostType) ->
238 9 gen_mod:get_module_opt(HostType, ?MODULE, host).
239
240 -spec my_disco_name(ejabberd:lang()) -> binary().
241 my_disco_name(Lang) ->
242 3 translate:translate(Lang, <<"HTTP File Upload">>).
243
244
245 -spec compose_iq_reply(IQ :: jlib:iq(),
246 PutUrl :: binary(),
247 GetUrl :: binary(),
248 Headers :: #{binary() => binary()}) ->
249 Reply :: jlib:iq().
250 compose_iq_reply(IQ, PutUrl, GetUrl, Headers) ->
251 5 Slot = #xmlel{
252 name = <<"slot">>,
253 attrs = [{<<"xmlns">>, ?NS_HTTP_UPLOAD_030}],
254 children = [create_url_xmlel(<<"put">>, PutUrl, Headers),
255 create_url_xmlel(<<"get">>, GetUrl, #{})]},
256 5 IQ#iq{type = result, sub_el =[Slot]}.
257
258
259 -spec token_bytes(mongooseim:host_type()) -> pos_integer().
260 token_bytes(HostType) ->
261 9 gen_mod:get_module_opt(HostType, ?MODULE, token_bytes).
262
263
264 -spec max_file_size(mongooseim:host_type()) -> pos_integer() | undefined.
265 max_file_size(HostType) ->
266 8 gen_mod:get_module_opt(HostType, ?MODULE, max_file_size).
267
268 -spec module_opts(mongooseim:host_type())-> gen_mod:module_opts().
269 module_opts(HostType) ->
270 9 gen_mod:get_module_opts(HostType, ?MODULE).
271
272
273 -spec generate_token(mongooseim:host_type()) -> binary().
274 generate_token(HostType) ->
275 9 base16:encode(crypto:strong_rand_bytes(token_bytes(HostType))).
276
277
278 -spec file_too_large_error(MaxFileSize :: non_neg_integer()) -> exml:element().
279 file_too_large_error(MaxFileSize) ->
280 1 MaxFileSizeBin = integer_to_binary(MaxFileSize),
281 1 MaxSizeEl = #xmlel{name = <<"max-file-size">>,
282 children = [#xmlcdata{content = MaxFileSizeBin}]},
283 1 FileTooLargeEl = #xmlel{name = <<"file-too-large">>,
284 attrs = [{<<"xmlns">>, ?NS_HTTP_UPLOAD_030}],
285 children = [MaxSizeEl]},
286 1 Error0 = mongoose_xmpp_errors:not_acceptable(),
287 1 Error0#xmlel{children = [FileTooLargeEl | Error0#xmlel.children]}.
288
289
290 -spec parse_request(Request :: exml:element()) ->
291 {Filename :: binary(), Size :: integer(), ContentType :: binary() | undefined} | bad_request.
292 parse_request(Request) ->
293 9 Keys = [<<"filename">>, <<"size">>, <<"content-type">>],
294 9 [Filename, SizeBin, ContentType] = [exml_query:attr(Request, K) || K <- Keys],
295 9 Size = (catch erlang:binary_to_integer(SizeBin)),
296 9 case is_nonempty_binary(Filename) andalso is_positive_integer(Size) of
297 3 false -> bad_request;
298 6 true -> {Filename, Size, ContentType}
299 end.
300
301
302 -spec get_disco_info_form(MaxFileSizeBin :: binary()) -> exml:element().
303 get_disco_info_form(MaxFileSizeBin) ->
304 2 #xmlel{name = <<"x">>,
305 attrs = [{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"result">>}],
306 children = [jlib:form_field({<<"FORM_TYPE">>, <<"hidden">>, ?NS_HTTP_UPLOAD_030}),
307 jlib:form_field({<<"max-file-size">>, MaxFileSizeBin})]}.
308
309
310 -spec header_to_xmlel({Key :: binary(), Value :: binary()}) -> exml:element().
311 header_to_xmlel({Key, Value}) ->
312
:-(
#xmlel{name = <<"header">>,
313 attrs = [{<<"name">>, Key}],
314 children = [#xmlcdata{content = Value}]}.
315
316
317 -spec create_url_xmlel(Name :: binary(), Url :: binary(), Headers :: #{binary() => binary()}) ->
318 exml:element().
319 create_url_xmlel(Name, Url, Headers) ->
320 10 HeadersXml = [header_to_xmlel(H) || H <- maps:to_list(Headers)],
321 10 #xmlel{name = Name, attrs = [{<<"url">>, Url}], children = HeadersXml}.
322
323
324 -spec is_nonempty_binary(term()) -> boolean().
325 8 is_nonempty_binary(<<_, _/binary>>) -> true;
326 1 is_nonempty_binary(_) -> false.
327
328
329 -spec is_positive_integer(term()) -> boolean().
330 7 is_positive_integer(X) when is_integer(X) -> X > 0;
331 1 is_positive_integer(_) -> false.
332
333 config_metrics(HostType) ->
334 24 mongoose_module_metrics:opts_for_module(HostType, ?MODULE, [backend]).
Line Hits Source