./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 :: gen_mod:module_opts()) ->
30 {PUTURL :: binary(), GETURL :: binary(),
31 Headers :: #{binary() => binary()}}.
32 create_slot(UTCDateTime, Token, Filename, ContentType, Size, Opts) ->
33 21 #{s3 := #{add_acl := AddACL, region := Region, access_key_id := AccessKeyId,
34 secret_access_key := SecretAccessKey, bucket_url := BucketURL},
35 expiration_time := ExpirationTime} = Opts,
36
37 21 {Scheme, Host, Port, Path} = extract_uri_params(BucketURL, Token, Filename),
38
39 21 ExpectedHeaders = get_expected_headers(Scheme, Host, Port, Size,
40 ContentType, AddACL),
41 21 UnsignedQueries = create_queries(UTCDateTime, AccessKeyId, Region,
42 ExpirationTime, ExpectedHeaders),
43
44 21 Signature = aws_signature_v4:sign(<<"PUT">>, Path, UnsignedQueries, ExpectedHeaders,
45 UTCDateTime, Region, <<"s3">>, SecretAccessKey),
46
47 21 Queries = maps:put(<<"X-Amz-Signature">>, Signature, UnsignedQueries),
48
49 21 {
50 compose_url(Scheme, Host, Port, Path, Queries),
51 compose_url(Scheme, Host, Port, Path, #{}),
52 #{}
53 }.
54
55 %%--------------------------------------------------------------------
56 %% Helpers
57 %%--------------------------------------------------------------------
58
59 -spec create_queries(UTCDateTime :: calendar:datetime(), AccessKeyId :: binary(),
60 Region :: binary(), ExpirationTime :: pos_integer(),
61 ExpectedHeaders :: #{binary() => binary()}) ->
62 Queries :: #{binary() => binary()}.
63 create_queries(UTCDateTime, AccessKeyId, Region, ExpirationTime, ExpectedHeaders) ->
64 21 Scope = aws_signature_v4:compose_scope(UTCDateTime, Region, <<"s3">>),
65 21 SignedHeadersSemi = << <<H/binary, ";">> || H <- maps:keys(ExpectedHeaders) >>,
66 21 SignedHeaders = binary_part(SignedHeadersSemi, 0, byte_size(SignedHeadersSemi) - 1),
67 21 #{
68 <<"X-Amz-Algorithm">> => <<"AWS4-HMAC-SHA256">>,
69 <<"X-Amz-Credential">> => <<AccessKeyId/binary, "/", Scope/binary>>,
70 <<"X-Amz-Date">> => aws_signature_v4:datetime_iso8601(UTCDateTime),
71 <<"X-Amz-Expires">> => integer_to_binary(ExpirationTime),
72 <<"X-Amz-SignedHeaders">> => SignedHeaders
73 }.
74
75
76 -spec get_expected_headers(Scheme :: http | https | atom(),
77 Host :: unicode:unicode_binary(),
78 Port :: inet:port_number(),
79 Size :: pos_integer(),
80 ContentType :: binary() | undefined,
81 AddACL :: boolean()) ->
82 Headers :: #{binary() => binary()}.
83 get_expected_headers(Scheme, Host, Port, Size, ContentType, AddACL) ->
84 21 Headers = #{<<"host">> => with_port_component(Scheme, Host, Port),
85 <<"content-length">> => integer_to_binary(Size)},
86 21 WithContentType = maybe_add_content_type(ContentType, Headers),
87 21 maybe_add_acl(AddACL, WithContentType).
88
89 maybe_add_content_type(undefined, Headers) ->
90 14 Headers;
91 maybe_add_content_type(ContentType, Headers) ->
92 7 maps:put(<<"content-type">>, ContentType, Headers).
93
94 maybe_add_acl(false, Headers) ->
95 2 Headers;
96 maybe_add_acl(true, Headers) ->
97 19 maps:put(<<"x-amz-acl">>, <<"public-read">>, Headers).
98
99
100 -spec extract_uri_params(BucketURL :: unicode:unicode_binary(), Token :: binary(),
101 Filename :: unicode:unicode_binary()) ->
102 {Scheme :: http | https | atom(), Host :: unicode:unicode_binary(),
103 Port :: inet:port_number(), Path :: unicode:unicode_binary()}.
104 extract_uri_params(BucketURL, Token, Filename) ->
105 21 #{host := Host, scheme := Scheme, path := Path0} = Parsed =
106 uri_string_parse(BucketURL),
107 21 SchemeAtom = binary_to_existing_atom(Scheme, latin1),
108 21 Port = case maps:get(port, Parsed, undefined) of
109 undefined ->
110 17 scheme_to_port(SchemeAtom, 80);
111 P ->
112 4 P
113 end,
114 21 KeylessPath = trim_slash(Path0),
115 21 EscapedFilename = aws_signature_v4:uri_encode(Filename),
116 21 Path = <<KeylessPath/binary, "/", Token/binary, "/", EscapedFilename/binary>>,
117 21 {SchemeAtom, Host, Port, Path}.
118
119 %% Uri is utf-8 encoded binary
120 uri_string_parse(Uri) when is_binary(Uri) ->
121 21 case uri_string:parse(Uri) of
122 Map when is_map(Map) ->
123 21 Map;
124 Other ->
125
:-(
error(#{what => failed_to_parse_uri, uri_string => Uri,
126 reason => Other})
127 end.
128
129 -spec compose_url(Scheme :: http | https | atom(), Host :: unicode:unicode_binary(),
130 Port :: inet:port_number(), Path :: unicode:unicode_binary(),
131 Queries :: #{binary() => binary()}) ->
132 URL :: unicode:unicode_binary().
133 compose_url(Scheme, Host, Port, Path, Queries) ->
134 42 SchemeBin = atom_to_binary(Scheme, latin1),
135 42 <<SchemeBin/binary, "://", (with_port_component(Scheme, Host, Port))/binary,
136 Path/binary, (query_string(Queries))/binary>>.
137
138
139 -spec query_string(Queries :: #{binary() => binary()}) -> QueryString :: binary().
140 query_string(Queries) ->
141 42 query_string(maps:to_list(Queries), []).
142
143
144 -spec query_string(Queries :: [binary()], Acc :: [binary()]) -> binary().
145 query_string([], Acc) ->
146 42 iolist_to_binary(lists:reverse(Acc));
147 query_string([Query | Queries], []) ->
148 21 query_string(Queries, [<<"?", (query_encode(Query))/binary>>]);
149 query_string([Query | Queries], Acc) ->
150 105 query_string(Queries, [<<"&", (query_encode(Query))/binary>> | Acc]).
151
152
153 -spec query_encode({Key :: binary(), Value :: binary()}) -> QueryComponent :: binary().
154 query_encode({Key, Value}) ->
155 126 <<(aws_signature_v4:uri_encode(Key))/binary, "=",
156 (aws_signature_v4:uri_encode(Value))/binary>>.
157
158
159 -spec with_port_component(Scheme :: http | https | atom(),
160 Host :: unicode:unicode_binary(),
161 Port :: inet:port_number()) -> binary().
162 with_port_component(Scheme, Host, Port) ->
163 63 case scheme_to_port(Scheme, undefined) of
164 51 Port -> Host;
165 12 _ -> <<Host/binary, ":", (integer_to_binary(Port))/binary>>
166 end.
167
168 80 scheme_to_port(http, _Default) -> 80;
169
:-(
scheme_to_port(https, _Default) -> 443;
170
:-(
scheme_to_port(_Scheme, Default) -> Default.
171
172 -spec trim_slash(binary()) -> binary().
173 trim_slash(<<>>) ->
174 17 <<>>;
175 trim_slash(Data) ->
176 4 case binary:last(Data) of
177 4 $/ -> erlang:binary_part(Data, 0, byte_size(Data) - 1);
178
:-(
_ -> Data
179 end.
Line Hits Source