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 |
:-( |
#{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 |
:-( |
{Scheme, Host, Port, Path} = extract_uri_params(BucketURL, Token, Filename), |
38 |
|
|
39 |
:-( |
ExpectedHeaders = get_expected_headers(Scheme, Host, Port, Size, |
40 |
|
ContentType, AddACL), |
41 |
:-( |
UnsignedQueries = create_queries(UTCDateTime, AccessKeyId, Region, |
42 |
|
ExpirationTime, ExpectedHeaders), |
43 |
|
|
44 |
:-( |
Signature = aws_signature_v4:sign(<<"PUT">>, Path, UnsignedQueries, ExpectedHeaders, |
45 |
|
UTCDateTime, Region, <<"s3">>, SecretAccessKey), |
46 |
|
|
47 |
:-( |
Queries = maps:put(<<"X-Amz-Signature">>, Signature, UnsignedQueries), |
48 |
|
|
49 |
:-( |
{ |
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 |
:-( |
Scope = aws_signature_v4:compose_scope(UTCDateTime, Region, <<"s3">>), |
65 |
:-( |
SignedHeadersSemi = << <<H/binary, ";">> || H <- maps:keys(ExpectedHeaders) >>, |
66 |
:-( |
SignedHeaders = binary_part(SignedHeadersSemi, 0, byte_size(SignedHeadersSemi) - 1), |
67 |
:-( |
#{ |
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 |
:-( |
Headers = #{<<"host">> => with_port_component(Scheme, Host, Port), |
85 |
|
<<"content-length">> => integer_to_binary(Size)}, |
86 |
:-( |
WithContentType = maybe_add_content_type(ContentType, Headers), |
87 |
:-( |
maybe_add_acl(AddACL, WithContentType). |
88 |
|
|
89 |
|
maybe_add_content_type(undefined, Headers) -> |
90 |
:-( |
Headers; |
91 |
|
maybe_add_content_type(ContentType, Headers) -> |
92 |
:-( |
maps:put(<<"content-type">>, ContentType, Headers). |
93 |
|
|
94 |
|
maybe_add_acl(false, Headers) -> |
95 |
:-( |
Headers; |
96 |
|
maybe_add_acl(true, Headers) -> |
97 |
:-( |
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 |
:-( |
#{host := Host, scheme := Scheme, path := Path0} = Parsed = |
106 |
|
uri_string_parse(BucketURL), |
107 |
:-( |
SchemeAtom = binary_to_existing_atom(Scheme, latin1), |
108 |
:-( |
Port = case maps:get(port, Parsed, undefined) of |
109 |
|
undefined -> |
110 |
:-( |
scheme_to_port(SchemeAtom, 80); |
111 |
|
P -> |
112 |
:-( |
P |
113 |
|
end, |
114 |
:-( |
KeylessPath = trim_slash(Path0), |
115 |
:-( |
EscapedFilename = aws_signature_v4:uri_encode(Filename), |
116 |
:-( |
Path = <<KeylessPath/binary, "/", Token/binary, "/", EscapedFilename/binary>>, |
117 |
:-( |
{SchemeAtom, Host, Port, Path}. |
118 |
|
|
119 |
|
%% Uri is utf-8 encoded binary |
120 |
|
uri_string_parse(Uri) when is_binary(Uri) -> |
121 |
:-( |
case uri_string:parse(Uri) of |
122 |
|
Map when is_map(Map) -> |
123 |
:-( |
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 |
:-( |
SchemeBin = atom_to_binary(Scheme, latin1), |
135 |
:-( |
<<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 |
:-( |
query_string(maps:to_list(Queries), []). |
142 |
|
|
143 |
|
|
144 |
|
-spec query_string(Queries :: [binary()], Acc :: [binary()]) -> binary(). |
145 |
|
query_string([], Acc) -> |
146 |
:-( |
iolist_to_binary(lists:reverse(Acc)); |
147 |
|
query_string([Query | Queries], []) -> |
148 |
:-( |
query_string(Queries, [<<"?", (query_encode(Query))/binary>>]); |
149 |
|
query_string([Query | Queries], Acc) -> |
150 |
:-( |
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 |
:-( |
<<(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 |
:-( |
case scheme_to_port(Scheme, undefined) of |
164 |
:-( |
Port -> Host; |
165 |
:-( |
_ -> <<Host/binary, ":", (integer_to_binary(Port))/binary>> |
166 |
|
end. |
167 |
|
|
168 |
:-( |
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 |
:-( |
<<>>; |
175 |
|
trim_slash(Data) -> |
176 |
:-( |
case binary:last(Data) of |
177 |
:-( |
$/ -> erlang:binary_part(Data, 0, byte_size(Data) - 1); |
178 |
:-( |
_ -> Data |
179 |
|
end. |