Software /
code /
prosody-modules
Comparison
mod_storage_s3/mod_storage_s3.lua @ 5692: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 |
child | 5693:2c9d72ef829e |
comparison
equal
deleted
inserted
replaced
5691:ecfd7aece33b | 5692: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); |