comparison mod_storage_s3/mod_storage_s3.lua @ 5695:b4632d5f840b

mod_storage_s3: Move request signing into a net.http hook
author Kim Alvefur <zash@zash.se>
date Sat, 11 Nov 2023 17:01:29 +0100
parents 799f69a5921a
children 66986f5271c3
comparison
equal deleted inserted replaced
5694:8afa0fb8a73e 5695:b4632d5f840b
23 local region = module:get_option_string("s3_region", "us-east-1"); 23 local region = module:get_option_string("s3_region", "us-east-1");
24 24
25 local access_key = module:get_option_string("s3_access_key"); 25 local access_key = module:get_option_string("s3_access_key");
26 local secret_key = module:get_option_string("s3_secret_key"); 26 local secret_key = module:get_option_string("s3_secret_key");
27 27
28 function driver:open(store, typ)
29 local mt = self[typ or "keyval"]
30 if not mt then
31 return nil, "unsupported-store";
32 end
33 return setmetatable({ store = store; bucket = bucket; type = typ }, mt);
34 end
35
36 local keyval = { };
37 driver.keyval = { __index = keyval; __name = module.name .. " keyval store" };
38
39 local aws4_format = "AWS4-HMAC-SHA256 Credential=%s/%s, SignedHeaders=%s, Signature=%s"; 28 local aws4_format = "AWS4-HMAC-SHA256 Credential=%s/%s, SignedHeaders=%s, Signature=%s";
40 29
41 local function new_request(method, path, query, payload) 30 local function aws_auth(event)
42 local request = url.parse(base_uri); 31 local request, options = event.request, event.options;
43 request.path = path; 32 local method = options.method or "GET";
33 local query = options.query;
34 local payload = options.body;
44 35
45 local payload_type = nil; 36 local payload_type = nil;
46 if st.is_stanza(payload) then 37 if st.is_stanza(payload) then
47 payload_type = "application/xml"; 38 payload_type = "application/xml";
48 payload = tostring(payload); 39 payload = tostring(payload);
49 elseif payload ~= nil then 40 elseif payload ~= nil then
50 payload_type = "application/json"; 41 payload_type = "application/json";
51 payload = json.encode(payload); 42 payload = json.encode(payload);
52 end 43 end
44 options.body = payload;
53 45
54 local payload_hash = sha256(payload or "", true); 46 local payload_hash = sha256(payload or "", true);
55 47
56 local now = os.time(); 48 local now = os.time();
57 local aws_datetime = os.date("!%Y%m%dT%H%M%SZ", now); 49 local aws_datetime = os.date("!%Y%m%dT%H%M%SZ", now);
110 102
111 local signature = hmac_sha256(signing_key, signature_payload, true); 103 local signature = hmac_sha256(signing_key, signature_payload, true);
112 104
113 headers["Authorization"] = string.format(aws4_format, access_key, scope, signed_headers, signature); 105 headers["Authorization"] = string.format(aws4_format, access_key, scope, signed_headers, signature);
114 106
115 return http.request(url.build(request), { method = method; headers = headers; body = payload }); 107 options.headers = headers;
108 end
109
110 function driver:open(store, typ)
111 local mt = self[typ or "keyval"]
112 if not mt then
113 return nil, "unsupported-store";
114 end
115 local httpclient = http.new({});
116 httpclient.events.add_handler("pre-request", aws_auth);
117 return setmetatable({ store = store; bucket = bucket; type = typ; http = httpclient }, mt);
118 end
119
120 local keyval = { };
121 driver.keyval = { __index = keyval; __name = module.name .. " keyval store" };
122
123 local function new_request(self, method, path, query, payload)
124 local request = url.parse(base_uri);
125 request.path = path;
126
127 return self.http:request(url.build(request), { method = method; body = payload; query = query });
116 end 128 end
117 129
118 -- coerce result back into Prosody data type 130 -- coerce result back into Prosody data type
119 local function on_result(response) 131 local function on_result(response)
120 if response.code == 404 and response.request.method == "GET" then 132 if response.code == 404 and response.request.method == "GET" then
145 jid.escape(key or "@"); 157 jid.escape(key or "@");
146 }) 158 })
147 end 159 end
148 160
149 function keyval:get(user) 161 function keyval:get(user)
150 return async.wait_for(new_request("GET", self:_path(user)):next(on_result)); 162 return async.wait_for(new_request(self, "GET", self:_path(user)):next(on_result));
151 end 163 end
152 164
153 function keyval:set(user, data) 165 function keyval:set(user, data)
154 166
155 if data == nil or (type(data) == "table" and next(data) == nil) then 167 if data == nil or (type(data) == "table" and next(data) == nil) then
156 return async.wait_for(new_request("DELETE", self:_path(user))); 168 return async.wait_for(new_request(self, "DELETE", self:_path(user)));
157 end 169 end
158 170
159 return async.wait_for(new_request("PUT", self:_path(user), nil, data)); 171 return async.wait_for(new_request(self, "PUT", self:_path(user), nil, data));
160 end 172 end
161 173
162 function keyval:users() 174 function keyval:users()
163 local bucket_path = url.build_path({ is_absolute = true; bucket; is_directory = true }); 175 local bucket_path = url.build_path({ is_absolute = true; bucket; is_directory = true });
164 local prefix = url.build_path({ jid.escape(module.host); jid.escape(self.store); is_directory = true }); 176 local prefix = url.build_path({ jid.escape(module.host); jid.escape(self.store); is_directory = true });
165 local list_result, err = async.wait_for(new_request("GET", bucket_path, { prefix = prefix })) 177 local list_result, err = async.wait_for(new_request(self, "GET", bucket_path, { prefix = prefix }))
166 if err or list_result.code ~= 200 then 178 if err or list_result.code ~= 200 then
167 return nil, err; 179 return nil, err;
168 end 180 end
169 local list_bucket_result = xml.parse(list_result.body); 181 local list_bucket_result = xml.parse(list_result.body);
170 if list_bucket_result:get_child_text("IsTruncated") == "true" then 182 if list_bucket_result:get_child_text("IsTruncated") == "true" then
206 local wrapper = st.stanza("wrapper"); 218 local wrapper = st.stanza("wrapper");
207 -- Minio had trouble with timestamps, probably the ':' characters, in paths. 219 -- Minio had trouble with timestamps, probably the ':' characters, in paths.
208 wrapper:tag("delay", { xmlns = "urn:xmpp:delay"; stamp = dt.datetime(when) }):up(); 220 wrapper:tag("delay", { xmlns = "urn:xmpp:delay"; stamp = dt.datetime(when) }):up();
209 wrapper:add_direct_child(value); 221 wrapper:add_direct_child(value);
210 key = key or new_uuid(); 222 key = key or new_uuid();
211 return async.wait_for(new_request("PUT", self:_path(username, nil, when, with, key), nil, wrapper):next(function(r) 223 return async.wait_for(new_request(self, "PUT", self:_path(username, nil, when, with, key), nil, wrapper):next(function(r)
212 if r.code == 200 then 224 if r.code == 200 then
213 return key; 225 return key;
214 else 226 else
215 error(r.body); 227 error(r.body);
216 end 228 end
230 table.insert(prefix, sha256(jid.prep(query["with"]), true):sub(1,24)); 242 table.insert(prefix, sha256(jid.prep(query["with"]), true):sub(1,24));
231 end 243 end
232 end 244 end
233 245
234 prefix = url.build_path(prefix); 246 prefix = url.build_path(prefix);
235 local list_result, err = async.wait_for(new_request("GET", bucket_path, { 247 local list_result, err = async.wait_for(new_request(self, "GET", bucket_path, {
236 prefix = prefix; 248 prefix = prefix;
237 ["max-keys"] = query["max"] and tostring(query["max"]); 249 ["max-keys"] = query["max"] and tostring(query["max"]);
238 })); 250 }));
239 if err or list_result.code ~= 200 then 251 if err or list_result.code ~= 200 then
240 return nil, err; 252 return nil, err;
274 local item = keys[i]; 286 local item = keys[i];
275 if item == nil then 287 if item == nil then
276 return nil; 288 return nil;
277 end 289 end
278 -- luacheck: ignore 431/err 290 -- luacheck: ignore 431/err
279 local value, err = async.wait_for(new_request("GET", self:_path(username or "@", item.date, nil, item.with, item.key)):next(on_result)); 291 local value, err = async.wait_for(new_request(self, "GET", self:_path(username or "@", item.date, nil, item.with, item.key)):next(on_result));
280 if not value then 292 if not value then
281 module:log("error", "%s", err); 293 module:log("error", "%s", err);
282 return nil; 294 return nil;
283 end 295 end
284 local delay = value:get_child("delay", "urn:xmpp:delay"); 296 local delay = value:get_child("delay", "urn:xmpp:delay");