File: s3.sql

package info (click to toggle)
pgsql-http 1.7.0-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 276 kB
  • sloc: ansic: 1,211; sql: 483; makefile: 22; sh: 9
file content (183 lines) | stat: -rw-r--r-- 6,480 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
--
-- s3_request
--
-- Installation:
--
--   CREATE EXTENSION pgcrypto;
--   CREATE EXTENSION http;
--
-- Utility function to take S3 object and access keys and create
-- a signed HTTP request using the AWS4 signing scheme.
-- https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html
-- 
-- Various pieces of the request are gathered into strings bundled together
-- and ultimately signed with the s3 secret key.
--
-- Example:
--
-- https://cleverelephant-west-1.s3.amazonaws.com/META.json
--
-- SELECT * FROM s3_request(
--     'your_s3_access_key',      -- access
--     'your_s3_secret_key',      -- secret
--     'us-west-1',               -- region
--     'cleverelephant-west-1',   -- bucket
--     'META.json',               -- object name
-- );
--
--
-- Create and delete objects too!
--
-- SELECT * FROM s3_request(
--     'your_s3_access_key',      -- access
--     'your_s3_secret_key',      -- secret
--     'us-west-1',               -- region
--     'cleverelephant-west-1',   -- bucket
--     'testfile.txt',            -- object name
--     'PUT',                     -- http method
--     'this is a test'           -- payload
--     'text/plain'               -- payload mime type
-- );
--
-- SELECT * FROM s3_request(
--     'your_s3_access_key',      -- access
--     'your_s3_secret_key',      -- secret
--     'us-west-1',               -- region
--     'cleverelephant-west-1',   -- bucket
--     'testfile.txt',            -- object name
-- );
--
-- SELECT * FROM s3_request(
--     'your_s3_access_key',      -- access
--     'your_s3_secret_key',      -- secret
--     'us-west-1',               -- region
--     'cleverelephant-west-1',   -- bucket
--     'testfile.txt',            -- object name
--     'DELETE'                   -- http method
-- );
--

CREATE OR REPLACE FUNCTION s3_request(
    access_key TEXT,
    secret_key TEXT,
    region TEXT,
    bucket TEXT,
    object_key TEXT,
    http_method TEXT DEFAULT 'GET',
    object_payload TEXT DEFAULT NULL,
    object_mimetype TEXT DEFAULT 'text/plain'
) RETURNS http_response AS $$
DECLARE
    host TEXT := bucket || '.s3.' || region || '.amazonaws.com';
    endpoint TEXT := 'https://' || host || '/' || object_key;
    canonical_uri TEXT := '/' || object_key;
    canonical_querystring TEXT := '';
    signed_headers TEXT := 'host;x-amz-content-sha256;x-amz-date';
    service TEXT := 's3';
    
    now TIMESTAMP := now() AT TIME ZONE 'UTC';
    amz_date TEXT := to_char(now, 'YYYYMMDD"T"HH24MISS"Z"');
    date_stamp TEXT := to_char(now, 'YYYYMMDD');

    -- Must use this magic hash if the payload is empty
    payload_hash TEXT := 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';

    canonical_headers TEXT;
    canonical_request TEXT;
    string_to_sign TEXT;
    credential_scope TEXT;
    date_key BYTEA; 
    date_region_key BYTEA;
    date_region_service_key BYTEA;
    signing_key BYTEA;
    signature TEXT;
    authorization_header TEXT;
    canonical_request_digest TEXT;
    request http_request;
BEGIN

    http_method := upper(http_method);
    object_mimetype := lower(object_mimetype);

    IF object_payload IS NOT NULL
    THEN
        payload_hash := encode(digest(convert_to(object_payload, 'UTF8'), 'sha256'), 'hex');
    END IF;

    -- Construct the canonical headers
    canonical_headers := 'host:' || host || E'\n' 
                      || 'x-amz-content-sha256:' || payload_hash || E'\n'
                      || 'x-amz-date:' || amz_date || E'\n';

    -- Signed headers must be in alphabetical order
    -- so content-type goes first
    IF object_payload IS NOT NULL
    THEN
        canonical_headers := 'content-type:' || object_mimetype || E'\n' || canonical_headers;
        signed_headers :=  'content-type;' || signed_headers;
    END IF;

    -- Create the canonical request
    canonical_request := http_method || E'\n' ||
                         canonical_uri || E'\n' ||
                         canonical_querystring || E'\n' ||
                         canonical_headers || E'\n' ||
                         signed_headers || E'\n' ||
                         payload_hash;

    -- Define the credential scope
    credential_scope := date_stamp || '/' || region || '/' || service || '/aws4_request';

    -- Get sha256 hash of request
    canonical_request_digest := encode(digest(canonical_request, 'sha256'), 'hex');

    -- Create the string to sign
    string_to_sign := 'AWS4-HMAC-SHA256' || E'\n' ||
                      amz_date || E'\n' ||
                      credential_scope || E'\n' ||
                      canonical_request_digest;

    --
    -- Signature of pgcrypto function is hmac(payload, secret, algo)
    -- Each piece of the signing key is bundled together with the 
    -- previous piece, starting with the S3 secret key.
    --
    date_key := hmac(convert_to(date_stamp, 'UTF8'), convert_to('AWS4' || secret_key, 'UTF8'), 'sha256');
    date_region_key := hmac(convert_to(region, 'UTF8'), date_key, 'sha256');
    date_region_service_key := hmac(convert_to(service, 'UTF8'), date_region_key, 'sha256');
    signing_key := hmac(convert_to('aws4_request','UTF8'), date_region_service_key, 'sha256');

    -- Compute the signature
    signature := encode(hmac(convert_to(string_to_sign, 'UTF8'), signing_key, 'sha256'), 'hex');

    -- Construct the Authorization header
    authorization_header := 'AWS4-HMAC-SHA256 Credential=' || access_key || '/' || credential_scope ||
                            ', SignedHeaders=' || signed_headers ||
                            ', Signature=' || signature;


    -- Perform the HTTP request
    request := (
            http_method,
            endpoint,
            http_headers('Authorization', authorization_header,
                         'x-amz-content-sha256', payload_hash,
                         'x-amz-date', amz_date,
                         'host', host),
            object_mimetype,
            object_payload
        )::http_request;

    -- Getting the canonical request and payload strings perfectly 
    -- formatted is an important step so debugging here in case
    -- S3 rejects signed request
    RAISE DEBUG 's3_request, payload_hash: %', payload_hash;
    RAISE DEBUG 's3_request, canonical_request: %', canonical_request;
    RAISE DEBUG 's3_request, string_to_sign: %', string_to_sign;
    RAISE DEBUG 's3_request, request %', request;

    RETURN http(request);

END;
$$ LANGUAGE 'plpgsql'
VOLATILE;