Mercurial > prosody-modules
comparison mod_storage_s3/mod_storage_s3.lua @ 5669:30f91daa40b4
mod_storage_s3: Beginnings of an experimental S3 storage driver
Tested against MinIO
| author | Kim Alvefur <zash@zash.se> |
|---|---|
| date | Sat, 14 Oct 2023 17:31:06 +0200 |
| parents | |
| children | 2c9d72ef829e |
comparison
equal
deleted
inserted
replaced
| 5668:ecfd7aece33b | 5669:30f91daa40b4 |
|---|---|
| 1 local http = require "prosody.net.http"; | |
| 2 local array = require "prosody.util.array"; | |
| 3 local async = require "prosody.util.async"; | |
| 4 local hashes = require "prosody.util.hashes"; | |
| 5 local httputil = require "prosody.util.http"; | |
| 6 local it = require "prosody.util.iterators"; | |
| 7 local jid = require "prosody.util.jid"; | |
| 8 local json = require "prosody.util.json"; | |
| 9 local st = require "prosody.util.stanza"; | |
| 10 local xml = require "prosody.util.xml"; | |
| 11 local url = require "socket.url"; | |
| 12 | |
| 13 local hmac_sha256 = hashes.hmac_sha256; | |
| 14 local sha256 = hashes.sha256; | |
| 15 | |
| 16 local driver = {}; | |
| 17 | |
| 18 local bucket = module:get_option_string("s3_bucket", "prosody"); | |
| 19 local base_uri = module:get_option_string("s3_base_uri", "http://localhost:9000"); | |
| 20 local region = module:get_option_string("s3_region", "us-east-1"); | |
| 21 | |
| 22 local access_key = module:get_option_string("s3_access_key"); | |
| 23 local secret_key = module:get_option_string("s3_secret_key"); | |
| 24 | |
| 25 function driver:open(store, typ) | |
| 26 local mt = self[typ or "keyval"] | |
| 27 if not mt then | |
| 28 return nil, "unsupported-store"; | |
| 29 end | |
| 30 return setmetatable({ store = store; bucket = bucket; type = typ }, mt); | |
| 31 end | |
| 32 | |
| 33 local keyval = { }; | |
| 34 driver.keyval = { __index = keyval; __name = module.name .. " keyval store" }; | |
| 35 | |
| 36 local aws4_format = "AWS4-HMAC-SHA256 Credential=%s/%s, SignedHeaders=%s, Signature=%s"; | |
| 37 | |
| 38 local function new_request(method, path, payload) | |
| 39 local request = url.parse(base_uri); | |
| 40 request.path = path; | |
| 41 | |
| 42 local payload_type = nil; | |
| 43 if st.is_stanza(payload) then | |
| 44 payload_type = "application/xml"; | |
| 45 payload = tostring(payload); | |
| 46 elseif payload ~= nil then | |
| 47 payload_type = "application/json"; | |
| 48 payload = json.encode(payload); | |
| 49 end | |
| 50 | |
| 51 local payload_hash = sha256(payload or "", true); | |
| 52 | |
| 53 local now = os.time(); | |
| 54 local aws_datetime = os.date("!%Y%m%dT%H%M%SZ", now); | |
| 55 local aws_date = os.date("!%Y%m%d", now); | |
| 56 | |
| 57 local headers = { | |
| 58 ["Accept"] = "*/*"; | |
| 59 ["Authorization"] = nil; | |
| 60 ["Content-Type"] = payload_type; | |
| 61 ["Host"] = request.authority; | |
| 62 ["User-Agent"] = "Prosody XMPP Server"; | |
| 63 ["X-Amz-Content-Sha256"] = payload_hash; | |
| 64 ["X-Amz-Date"] = aws_datetime; | |
| 65 }; | |
| 66 | |
| 67 local canonical_uri = url.build({ path = request.path }); | |
| 68 local canonical_query = ""; | |
| 69 local canonical_headers = array(); | |
| 70 local signed_headers = array() | |
| 71 | |
| 72 for header_name, header_value in it.sorted_pairs(headers) do | |
| 73 header_name = header_name:lower(); | |
| 74 canonical_headers:push(header_name .. ":" .. header_value .. "\n"); | |
| 75 signed_headers:push(header_name); | |
| 76 end | |
| 77 | |
| 78 canonical_headers = canonical_headers:concat(); | |
| 79 signed_headers = signed_headers:concat(";"); | |
| 80 | |
| 81 local scope = aws_date .. "/" .. region .. "/s3/aws4_request"; | |
| 82 | |
| 83 local canonical_request = method .. "\n" | |
| 84 .. canonical_uri .. "\n" | |
| 85 .. canonical_query .. "\n" | |
| 86 .. canonical_headers .. "\n" | |
| 87 .. signed_headers .. "\n" | |
| 88 .. payload_hash; | |
| 89 | |
| 90 local signature_payload = "AWS4-HMAC-SHA256" .. "\n" .. aws_datetime .. "\n" .. scope .. "\n" .. sha256(canonical_request, true); | |
| 91 | |
| 92 -- This can be cached? | |
| 93 local date_key = hmac_sha256("AWS4" .. secret_key, aws_date); | |
| 94 local date_region_key = hmac_sha256(date_key, region); | |
| 95 local date_region_service_key = hmac_sha256(date_region_key, "s3"); | |
| 96 local signing_key = hmac_sha256(date_region_service_key, "aws4_request"); | |
| 97 | |
| 98 local signature = hmac_sha256(signing_key, signature_payload, true); | |
| 99 | |
| 100 headers["Authorization"] = string.format(aws4_format, access_key, scope, signed_headers, signature); | |
| 101 | |
| 102 return http.request(url.build(request), { method = method; headers = headers; body = payload }); | |
| 103 end | |
| 104 | |
| 105 -- coerce result back into Prosody data type | |
| 106 local function on_result(response) | |
| 107 local content_type = response.headers["content-type"]; | |
| 108 if content_type == "application/json" then | |
| 109 return json.decode(response.body); | |
| 110 elseif content_type == "application/xml" then | |
| 111 return xml.parse(response.body); | |
| 112 elseif content_type == "application/x-www-form-urlencoded" then | |
| 113 return httputil.formdecode(response.body); | |
| 114 else | |
| 115 response.log("warn", "Unknown response data type %s", content_type); | |
| 116 return response.body; | |
| 117 end | |
| 118 end | |
| 119 | |
| 120 function keyval:_path(key) | |
| 121 return url.build_path({ | |
| 122 is_absolute = true; | |
| 123 bucket; | |
| 124 jid.escape(module.host); | |
| 125 jid.escape(self.store); | |
| 126 jid.escape(key or ""); | |
| 127 }) | |
| 128 end | |
| 129 | |
| 130 function keyval:get(user) | |
| 131 return async.wait_for(new_request("GET", self:_path(user)):next(on_result)); | |
| 132 end | |
| 133 | |
| 134 function keyval:set(user, data) | |
| 135 return async.wait_for(new_request("PUT", self:_path(user), data)); | |
| 136 end | |
| 137 | |
| 138 function keyval:users() | |
| 139 return nil, "not-implemented"; | |
| 140 end | |
| 141 | |
| 142 module:provides("storage", driver); |
