Software / code / prosody-modules
Comparison
mod_storage_s3/mod_storage_s3.lua @ 5718: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 |
| parent | 5699:799f69a5921a |
| child | 5719:66986f5271c3 |
comparison
equal
deleted
inserted
replaced
| 5717:8afa0fb8a73e | 5718: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"); |