./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 9 #{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 9 {Scheme, Host, Port, Path} = extract_uri_params(BucketURL, Token, Filename),
38
39 9 ExpectedHeaders = get_expected_headers(Scheme, Host, Port, Size,
40 ContentType, AddACL),
41 9 UnsignedQueries = create_queries(UTCDateTime, AccessKeyId, Region,
42 ExpirationTime, ExpectedHeaders),
43
44 9 Signature = aws_signature_v4:sign(<<"PUT">>, Path, UnsignedQueries, ExpectedHeaders,
45 UTCDateTime, Region, <<"s3">>, SecretAccessKey),
46
47 9 Queries = maps:put(<<"X-Amz-Signature">>, Signature, UnsignedQueries),
48
49 9 {
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 9 Scope = aws_signature_v4:compose_scope(UTCDateTime, Region, <<"s3">>),
65 9 SignedHeadersSemi = << <<H/binary, ";">> || H <- maps:keys(ExpectedHeaders) >>,
66 9 SignedHeaders = binary_part(SignedHeadersSemi, 0, byte_size(SignedHeadersSemi) - 1),
67 9 #{
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 9 Headers = #{<<"host">> => with_port_component(Scheme, Host, Port),
85 <<"content-length">> => integer_to_binary(Size)},
86 9 WithContentType = maybe_add_content_type(ContentType, Headers),
87 9 maybe_add_acl(AddACL, WithContentType).
88
89 maybe_add_content_type(undefined, Headers) ->
90 7 Headers;
91 maybe_add_content_type(ContentType, Headers) ->
92 2 maps:put(<<"content-type">>, ContentType, Headers).
93
94 maybe_add_acl(false, Headers) ->
95 2 Headers;
96 maybe_add_acl(true, Headers) ->
97 7 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 9 {ok, {Scheme, [], Host, Port, Path0, []}} = http_uri:parse(binary_to_list(BucketURL)),
106 9 KeylessPath = trim_slash(list_to_binary(Path0)),
107 9 EscapedFilename = aws_signature_v4:uri_encode(Filename),
108 9 Path = <<KeylessPath/binary, "/", Token/binary, "/", EscapedFilename/binary>>,
109 9 {Scheme, list_to_binary(Host), Port, Path}.
110
111
112 -spec compose_url(Scheme :: http | https | atom(), Host :: unicode:unicode_binary(),
113 Port :: inet:port_number(), Path :: unicode:unicode_binary(),
114 Queries :: #{binary() => binary()}) ->
115 URL :: unicode:unicode_binary().
116 compose_url(Scheme, Host, Port, Path, Queries) ->
117 18 SchemeBin = atom_to_binary(Scheme, latin1),
118 18 <<SchemeBin/binary, "://", (with_port_component(Scheme, Host, Port))/binary,
119 Path/binary, (query_string(Queries))/binary>>.
120
121
122 -spec query_string(Queries :: #{binary() => binary()}) -> QueryString :: binary().
123 query_string(Queries) ->
124 18 query_string(maps:to_list(Queries), []).
125
126
127 -spec query_string(Queries :: [binary()], Acc :: [binary()]) -> binary().
128 query_string([], Acc) ->
129 18 iolist_to_binary(lists:reverse(Acc));
130 query_string([Query | Queries], []) ->
131 9 query_string(Queries, [<<"?", (query_encode(Query))/binary>>]);
132 query_string([Query | Queries], Acc) ->
133 45 query_string(Queries, [<<"&", (query_encode(Query))/binary>> | Acc]).
134
135
136 -spec query_encode({Key :: binary(), Value :: binary()}) -> QueryComponent :: binary().
137 query_encode({Key, Value}) ->
138 54 <<(aws_signature_v4:uri_encode(Key))/binary, "=",
139 (aws_signature_v4:uri_encode(Value))/binary>>.
140
141
142 -spec with_port_component(Scheme :: http | https | atom(),
143 Host :: unicode:unicode_binary(),
144 Port :: inet:port_number()) -> binary().
145 with_port_component(Scheme, Host, Port) ->
146 27 case lists:keyfind(Scheme, 1, http_uri:scheme_defaults()) of
147 15 {Scheme, Port} -> Host;
148 12 _ -> <<Host/binary, ":", (integer_to_binary(Port))/binary>>
149 end.
150
151
152 %% Path has always at least one byte ("/")
153 -spec trim_slash(binary()) -> binary().
154 trim_slash(Data) ->
155 9 case binary:last(Data) of
156 9 $/ -> erlang:binary_part(Data, 0, byte_size(Data) - 1);
157
:-(
_ -> Data
158 end.
Line Hits Source