./ct_report/coverage/mod_http_upload_s3.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_s3).
18 -author('konrad.zemek@erlang-solutions.com').
19 -behaviour(mod_http_upload_backend).
20
21 -export([create_slot/6]).
22
23 %%--------------------------------------------------------------------
24 %% API
25 %%--------------------------------------------------------------------
26
27 -spec create_slot(UTCDateTime :: calendar:datetime(), Token :: binary(),
28 Filename :: unicode:unicode_binary(), ContentType :: binary() | undefined,
29 Size :: pos_integer(), Opts :: proplists:proplist()) ->
30 {PUTURL :: binary(), GETURL :: binary(),
31 Headers :: #{binary() => binary()}}.
32 create_slot(UTCDateTime, Token, Filename, ContentType, Size, Opts) ->
33 17 S3Opts = gen_mod:get_opt(s3, Opts),
34 17 ExpirationTime = gen_mod:get_opt(expiration_time, Opts, 60),
35 17 AddACL = proplists:get_value(add_acl, S3Opts, false),
36 17 BucketURL = unicode:characters_to_binary(gen_mod:get_opt(bucket_url, S3Opts)),
37 17 Region = list_to_binary(gen_mod:get_opt(region, S3Opts)),
38 17 AccessKeyId = list_to_binary(gen_mod:get_opt(access_key_id, S3Opts)),
39 17 SecretAccessKey = list_to_binary(gen_mod:get_opt(secret_access_key, S3Opts)),
40
41 17 {Scheme, Host, Port, Path} = extract_uri_params(BucketURL, Token, Filename),
42
43 17 ExpectedHeaders = get_expected_headers(Scheme, Host, Port, Size,
44 ContentType, AddACL),
45 17 UnsignedQueries = create_queries(UTCDateTime, AccessKeyId, Region,
46 ExpirationTime, ExpectedHeaders),
47
48 17 Signature = aws_signature_v4:sign(<<"PUT">>, Path, UnsignedQueries, ExpectedHeaders,
49 UTCDateTime, Region, <<"s3">>, SecretAccessKey),
50
51 17 Queries = maps:put(<<"X-Amz-Signature">>, Signature, UnsignedQueries),
52
53 17 {
54 compose_url(Scheme, Host, Port, Path, Queries),
55 compose_url(Scheme, Host, Port, Path, #{}),
56 #{}
57 }.
58
59 %%--------------------------------------------------------------------
60 %% Helpers
61 %%--------------------------------------------------------------------
62
63 -spec create_queries(UTCDateTime :: calendar:datetime(), AccessKeyId :: binary(),
64 Region :: binary(), ExpirationTime :: pos_integer(),
65 ExpectedHeaders :: #{binary() => binary()}) ->
66 Queries :: #{binary() => binary()}.
67 create_queries(UTCDateTime, AccessKeyId, Region, ExpirationTime, ExpectedHeaders) ->
68 17 Scope = aws_signature_v4:compose_scope(UTCDateTime, Region, <<"s3">>),
69 17 SignedHeadersSemi = << <<H/binary, ";">> || H <- maps:keys(ExpectedHeaders) >>,
70 17 SignedHeaders = binary_part(SignedHeadersSemi, 0, byte_size(SignedHeadersSemi) - 1),
71 17 #{
72 <<"X-Amz-Algorithm">> => <<"AWS4-HMAC-SHA256">>,
73 <<"X-Amz-Credential">> => <<AccessKeyId/binary, "/", Scope/binary>>,
74 <<"X-Amz-Date">> => aws_signature_v4:datetime_iso8601(UTCDateTime),
75 <<"X-Amz-Expires">> => integer_to_binary(ExpirationTime),
76 <<"X-Amz-SignedHeaders">> => SignedHeaders
77 }.
78
79
80 -spec get_expected_headers(Scheme :: http | https | atom(),
81 Host :: unicode:unicode_binary(),
82 Port :: inet:port_number(),
83 Size :: pos_integer(),
84 ContentType :: binary() | undefined,
85 AddACL :: boolean()) ->
86 Headers :: #{binary() => binary()}.
87 get_expected_headers(Scheme, Host, Port, Size, ContentType, AddACL) ->
88 17 Headers = #{<<"host">> => with_port_component(Scheme, Host, Port),
89 <<"content-length">> => integer_to_binary(Size)},
90 17 WithContentType = maybe_add_content_type(ContentType, Headers),
91 17 maybe_add_acl(AddACL, WithContentType).
92
93 maybe_add_content_type(undefined, Headers) ->
94 11 Headers;
95 maybe_add_content_type(ContentType, Headers) ->
96 6 maps:put(<<"content-type">>, ContentType, Headers).
97
98 maybe_add_acl(false, Headers) ->
99 6 Headers;
100 maybe_add_acl(true, Headers) ->
101 11 maps:put(<<"x-amz-acl">>, <<"public-read">>, Headers).
102
103
104 -spec extract_uri_params(BucketURL :: unicode:unicode_binary(), Token :: binary(),
105 Filename :: unicode:unicode_binary()) ->
106 {Scheme :: http | https | atom(), Host :: unicode:unicode_binary(),
107 Port :: inet:port_number(), Path :: unicode:unicode_binary()}.
108 extract_uri_params(BucketURL, Token, Filename) ->
109 17 {ok, {Scheme, [], Host, Port, Path0, []}} = http_uri:parse(binary_to_list(BucketURL)),
110 17 KeylessPath = trim_slash(list_to_binary(Path0)),
111 17 EscapedFilename = aws_signature_v4:uri_encode(Filename),
112 17 Path = <<KeylessPath/binary, "/", Token/binary, "/", EscapedFilename/binary>>,
113 17 {Scheme, list_to_binary(Host), Port, Path}.
114
115
116 -spec compose_url(Scheme :: http | https | atom(), Host :: unicode:unicode_binary(),
117 Port :: inet:port_number(), Path :: unicode:unicode_binary(),
118 Queries :: #{binary() => binary()}) ->
119 URL :: unicode:unicode_binary().
120 compose_url(Scheme, Host, Port, Path, Queries) ->
121 34 SchemeBin = atom_to_binary(Scheme, latin1),
122 34 <<SchemeBin/binary, "://", (with_port_component(Scheme, Host, Port))/binary,
123 Path/binary, (query_string(Queries))/binary>>.
124
125
126 -spec query_string(Queries :: #{binary() => binary()}) -> QueryString :: binary().
127 query_string(Queries) ->
128 34 query_string(maps:to_list(Queries), []).
129
130
131 -spec query_string(Queries :: [binary()], Acc :: [binary()]) -> binary().
132 query_string([], Acc) ->
133 34 iolist_to_binary(lists:reverse(Acc));
134 query_string([Query | Queries], []) ->
135 17 query_string(Queries, [<<"?", (query_encode(Query))/binary>>]);
136 query_string([Query | Queries], Acc) ->
137 85 query_string(Queries, [<<"&", (query_encode(Query))/binary>> | Acc]).
138
139
140 -spec query_encode({Key :: binary(), Value :: binary()}) -> QueryComponent :: binary().
141 query_encode({Key, Value}) ->
142 102 <<(aws_signature_v4:uri_encode(Key))/binary, "=",
143 (aws_signature_v4:uri_encode(Value))/binary>>.
144
145
146 -spec with_port_component(Scheme :: http | https | atom(),
147 Host :: unicode:unicode_binary(),
148 Port :: inet:port_number()) -> binary().
149 with_port_component(Scheme, Host, Port) ->
150 51 case lists:keyfind(Scheme, 1, http_uri:scheme_defaults()) of
151 15 {Scheme, Port} -> Host;
152 36 _ -> <<Host/binary, ":", (integer_to_binary(Port))/binary>>
153 end.
154
155
156 %% Path has always at least one byte ("/")
157 -spec trim_slash(binary()) -> binary().
158 trim_slash(Data) ->
159 17 case binary:last(Data) of
160 17 $/ -> erlang:binary_part(Data, 0, byte_size(Data) - 1);
161
:-(
_ -> Data
162 end.
Line Hits Source