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);