Skip to content

Commit 4fb376e

Browse files
committed
Update the s3 example to support POST/DELETE/PUT etc as well
as just get, by adding payload signing and generalizing the HTTP request type into a parameter
1 parent 96b01b5 commit 4fb376e

File tree

1 file changed

+75
-27
lines changed

1 file changed

+75
-27
lines changed

examples/s3.sql

Lines changed: 75 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
2-
--
3-
-- s3_get
1+
--
2+
-- s3_request
43
--
54
-- Installation:
65
--
76
-- CREATE EXTENSION pgcrypto;
87
-- CREATE EXTENSION http;
98
--
109
-- Utility function to take S3 object and access keys and create
11-
-- a signed HTTP GET request using the AWS4 signing scheme.
10+
-- a signed HTTP request using the AWS4 signing scheme.
1211
-- https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html
1312
--
1413
-- Various pieces of the request are gathered into strings bundled together
@@ -18,23 +17,57 @@
1817
--
1918
-- https://cleverelephant-west-1.s3.amazonaws.com/META.json
2019
--
21-
-- SELECT * FROM s3_get(
20+
-- SELECT * FROM s3_request(
21+
-- 'your_s3_access_key', -- access
22+
-- 'your_s3_secret_key', -- secret
23+
-- 'us-west-1', -- region
24+
-- 'cleverelephant-west-1', -- bucket
25+
-- 'META.json', -- object name
26+
-- );
27+
--
28+
--
29+
-- Created and delete objects too!
30+
--
31+
-- SELECT * FROM s3_request(
32+
-- 'your_s3_access_key', -- access
33+
-- 'your_s3_secret_key', -- secret
34+
-- 'us-west-1', -- region
35+
-- 'cleverelephant-west-1', -- bucket
36+
-- 'testfile.txt', -- object name
37+
-- 'PUT', -- http method
38+
-- 'this is a test' -- payload
39+
-- 'text/plain' -- payload mime type
40+
-- );
41+
--
42+
-- SELECT * FROM s3_request(
43+
-- 'your_s3_access_key', -- access
44+
-- 'your_s3_secret_key', -- secret
45+
-- 'us-west-1', -- region
46+
-- 'cleverelephant-west-1', -- bucket
47+
-- 'testfile.txt', -- object name
48+
-- );
49+
--
50+
-- SELECT * FROM s3_request(
2251
-- 'your_s3_access_key', -- access
2352
-- 'your_s3_secret_key', -- secret
2453
-- 'us-west-1', -- region
2554
-- 'cleverelephant-west-1', -- bucket
26-
-- 'META.json' -- object
55+
-- 'testfile.txt', -- object name
56+
-- 'DELETE' -- http method
2757
-- );
2858
--
29-
CREATE OR REPLACE FUNCTION s3_get(
59+
60+
CREATE OR REPLACE FUNCTION s3_request(
3061
access_key TEXT,
3162
secret_key TEXT,
3263
region TEXT,
3364
bucket TEXT,
34-
object_key TEXT
65+
object_key TEXT,
66+
http_method TEXT DEFAULT 'GET',
67+
object_payload TEXT DEFAULT NULL,
68+
object_mimetype TEXT DEFAULT 'text/plain'
3569
) RETURNS http_response AS $$
3670
DECLARE
37-
http_method TEXT := 'GET';
3871
host TEXT := bucket || '.s3.' || region || '.amazonaws.com';
3972
endpoint TEXT := 'https://' || host || '/' || object_key;
4073
canonical_uri TEXT := '/' || object_key;
@@ -45,7 +78,9 @@ DECLARE
4578
now TIMESTAMP := now() AT TIME ZONE 'UTC';
4679
amz_date TEXT := to_char(now, 'YYYYMMDD"T"HH24MISS"Z"');
4780
date_stamp TEXT := to_char(now, 'YYYYMMDD');
48-
empty_payload_hash TEXT := 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';
81+
82+
-- Must use this magic hash if the payload is empty
83+
payload_hash TEXT := 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';
4984

5085
canonical_headers TEXT;
5186
canonical_request TEXT;
@@ -62,18 +97,33 @@ DECLARE
6297
request http_request;
6398
BEGIN
6499

100+
http_method := upper(http_method);
101+
102+
IF object_payload IS NOT NULL
103+
THEN
104+
payload_hash := encode(digest(convert_to(object_payload, 'UTF8'), 'sha256'), 'hex');
105+
END IF;
106+
65107
-- Construct the canonical headers
66108
canonical_headers := 'host:' || host || E'\n'
67-
|| 'x-amz-content-sha256:' || empty_payload_hash || E'\n'
109+
|| 'x-amz-content-sha256:' || payload_hash || E'\n'
68110
|| 'x-amz-date:' || amz_date || E'\n';
69111

112+
-- Signed headers must be in alphabetical order
113+
-- so content-type goes first
114+
IF object_payload IS NOT NULL
115+
THEN
116+
canonical_headers := 'content-type:' || lower(object_mimetype) || E'\n' || canonical_headers;
117+
signed_headers := 'content-type;' || signed_headers;
118+
END IF;
119+
70120
-- Create the canonical request
71121
canonical_request := http_method || E'\n' ||
72122
canonical_uri || E'\n' ||
73123
canonical_querystring || E'\n' ||
74124
canonical_headers || E'\n' ||
75125
signed_headers || E'\n' ||
76-
empty_payload_hash;
126+
payload_hash;
77127

78128
-- Define the credential scope
79129
credential_scope := date_stamp || '/' || region || '/' || service || '/aws4_request';
@@ -108,29 +158,27 @@ BEGIN
108158

109159
-- Perform the HTTP request
110160
request := (
111-
'GET',
112-
endpoint,
113-
http_headers('Authorization', authorization_header,
114-
'x-amz-content-sha256', empty_payload_hash,
115-
'x-amz-date', amz_date,
116-
'host', host),
117-
NULL,
118-
NULL
161+
http_method,
162+
endpoint,
163+
http_headers('Authorization', authorization_header,
164+
'x-amz-content-sha256', payload_hash,
165+
'x-amz-date', amz_date,
166+
'host', host),
167+
object_mimetype,
168+
object_payload
119169
)::http_request;
120170

121171
-- Getting the canonical request and payload strings perfectly
122172
-- formatted is an important step so debugging here in case
123173
-- S3 rejects signed request
124-
RAISE DEBUG 's3_get, canonical_request: %', canonical_request;
125-
RAISE DEBUG 's3_get, string_to_sign: %', string_to_sign;
126-
RAISE DEBUG 's3_get, request %', request;
174+
RAISE DEBUG 's3_request, payload_hash: %', payload_hash;
175+
RAISE DEBUG 's3_request, canonical_request: %', canonical_request;
176+
RAISE DEBUG 's3_request, string_to_sign: %', string_to_sign;
177+
RAISE DEBUG 's3_request, request %', request;
127178

128179
RETURN http(request);
180+
-- RETURN NULL;
129181

130182
END;
131183
$$ LANGUAGE 'plpgsql'
132184
VOLATILE;
133-
134-
135-
136-

0 commit comments

Comments
 (0)