1//! Reference: https://www.postgresql.org/docs/current/protocol.html
2//! Implements version 3.0 (PostgreSQL 7.4 and later)
3
4const std = @import("std");
5const builtin = @import("builtin");
6const url = @import("url");
7const net = @import("net");
8const nio = @import("nio");
9const extras = @import("extras");
10const tracer = @import("tracer");
11
12const sys = switch (builtin.target.os.tag) {
13 .linux => @import("sys-linux"),
14 .freebsd => @import("sys-freebsd"),
15 .netbsd => @import("sys-netbsd"),
16 .openbsd => @import("sys-openbsd"),
17 else => unreachable,
18};
19
20const Driver = @This();
21
22pub const Error = error{};
23
24conn: net.Stream,
25bufw: nio.BufferedWriter(4096, net.Stream),
26bufr: nio.BufferedReader(4096, net.Stream),
27
28pub fn connect(allocator: std.mem.Allocator, connect_s: [:0]const u8) !Driver {
29 const connect_url = try url.URL.parse(allocator, connect_s, null);
30 defer allocator.free(connect_url.href);
31 std.log.scoped(.zorm).info("connecting to {s} @ postgresql://{s}{s}", .{ "postgresql", connect_url.hostname, connect_url.pathname });
32
33 const addr: net.Address = try .fromUrl(&connect_url, allocator);
34 const conn = try addr.tcpConnect();
35 errdefer conn.close();
36
37 var bufw: nio.BufferedWriter(4096, net.Stream) = .init(conn);
38 _ = &bufw;
39
40 var bufr: nio.BufferedReader(4096, net.Stream) = .init(conn);
41 _ = &bufr;
42
43 // https://github.com/postgres/postgres/blob/REL_18_0/src/include/common/scram-common.h#L32-L37
44 const nonce_len = 18;
45 const cnonce = nio.randomBytes(nonce_len);
46 _ = &cnonce;
47
48 var snonce_b64_buf: [128]u8 = @splat(0);
49 var snonce_b64: []u8 = snonce_b64_buf[0..];
50
51 var scram_i_buf: [16]u8 = @splat(0);
52 var scram_i_s: []u8 = scram_i_buf[0..];
53 var scram_i: u32 = 0;
54
55 const Base64Enc = std.base64.standard.Encoder;
56 const Base64Dec = std.base64.standard.Decoder;
57
58 {
59 try proto.StartupMessage.write(
60 &bufw,
61 connect_url.username,
62 connect_url.pathname[1..],
63 );
64 try bufw.flush();
65 }
66 {
67 const t: BackendMessageType = @enumFromInt(try bufr.readByte());
68 std.debug.assert(t == .Authentication);
69 const auth_len = try bufr.readInt(u32, .big);
70 const auth = try bufr.readInt(u32, .big);
71 switch (auth) {
72 10 => { // AuthenticationSASL
73 const methods = try bufr.readAlloc(allocator, auth_len - 4 - 4);
74 defer allocator.free(methods);
75 var methods_iter = std.mem.splitScalar(u8, methods, '\x00');
76
77 while (methods_iter.next()) |method| {
78 const method_z = method.ptr[0..method.len :0];
79 if (method_z.len == 0) break;
80
81 // https://datatracker.ietf.org/doc/html/rfc7677
82 // https://datatracker.ietf.org/doc/html/rfc5802
83 // https://datatracker.ietf.org/doc/html/rfc4422
84 // Salted Challenge Response Authentication Mechanism (SCRAM) SASL and GSS-API Mechanisms
85
86 const mechanisms = [_]struct { []const u8, type }{
87 .{ "SCRAM-SHA-256", std.crypto.hash.sha2.Sha256 },
88 .{ "SCRAM-SHA-1", std.crypto.hash.Sha1 },
89 };
90 inline for (&mechanisms) |mechanism| {
91 const name, const Hash = mechanism;
92 const Hmac = std.crypto.auth.hmac.Hmac(Hash);
93 comptime std.debug.assert(Hash.digest_length == Hmac.mac_length);
94
95 if (std.mem.eql(u8, method_z, name)) {
96 // This is a simple example of a SCRAM-SHA-256 authentication exchange when the client doesn't support channel bindings.
97 // The username 'user' and password 'pencil' are being used.
98 // C: n,,n=user,r=rOprNGfwEbeRWgbNEkqO
99 // S: r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096
100 // C: c=biws,r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,p=dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ=
101 // S: v=6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4=
102
103 // SaltedPassword := Hi(Normalize(password), salt, i)
104 // ClientKey := HMAC(SaltedPassword, "Client Key")
105 // StoredKey := H(ClientKey)
106 // AuthMessage := client-first-message-bare + "," + server-first-message + "," + client-final-message-without-proof
107 // ClientSignature := HMAC(StoredKey, AuthMessage)
108 // ClientProof := ClientKey XOR ClientSignature
109 // ServerKey := HMAC(SaltedPassword, "Server Key")
110 // ServerSignature := HMAC(ServerKey, AuthMessage)
111
112 // server-final-message = (verifier / server-error) ["," extensions]
113 // verifier = "v=" base64
114 // server-error = "e=" server-error-value
115 // server-error-value = "invalid-encoding" / "extensions-not-supported" / "invalid-proof" / "channel-bindings-dont-match" / "server-does-support-channel-binding" / "channel-binding-not-supported" / "unsupported-channel-binding-type" / "unknown-user" / "invalid-username-encoding" / "no-resources" / "other-error" / server-error-value-ext
116
117 var cnonce_b64_buf: [Base64Enc.calcSize(nonce_len)]u8 = @splat(0);
118 const cnonce_b64 = Base64Enc.encode(&cnonce_b64_buf, &cnonce);
119
120 var salt_b64_buf: [128]u8 = @splat(0);
121 var salt_b64: []u8 = salt_b64_buf[0..];
122
123 var salt_buf: [128]u8 = @splat(0);
124 var salt_dec: []u8 = salt_buf[0..];
125
126 { //->SASLInitialResponse (scram client-first-message)
127
128 try proto.SASLInitialResponse.writev(
129 &bufw,
130 "SCRAM-SHA-256",
131 &.{
132 "n", "",
133 ",", "",
134 ",n=", connect_url.username,
135 ",r=", cnonce_b64,
136 },
137 );
138 try bufw.flush();
139 }
140 { //<-AuthenticationSASLContinue (scram server-first-message)
141 const t2: BackendMessageType = @enumFromInt(try bufr.readByte());
142 if (t2 == .ErrorResponse) try printError(&bufr, allocator);
143 std.debug.assert(t2 == .Authentication);
144 const len = try bufr.readInt(u32, .big);
145 const ok = try bufr.readInt(u32, .big);
146 std.debug.assert(ok == 11);
147 const data = try bufr.readAlloc(allocator, len - 8);
148 defer allocator.free(data);
149 var iter = std.mem.splitScalar(u8, data, ',');
150 while (iter.next()) |field| {
151 if (extras.trimPrefixEnsure(field, "r=")) |r| {
152 snonce_b64 = snonce_b64_buf[0..r.len];
153 @memcpy(snonce_b64, r);
154 }
155 if (extras.trimPrefixEnsure(field, "s=")) |s| {
156 salt_b64 = salt_b64_buf[0..s.len];
157 @memcpy(salt_b64, s);
158 const s_len = try Base64Dec.calcSizeForSlice(s);
159 salt_dec = salt_buf[0..s_len];
160 try Base64Dec.decode(salt_dec, s);
161 }
162 if (extras.trimPrefixEnsure(field, "i=")) |i| {
163 scram_i_s = scram_i_buf[0..i.len];
164 @memcpy(scram_i_s, i);
165 scram_i = extras.parseDigits(u32, i, 10) catch 0;
166 if (scram_i < 4096) return error.WeakParameters;
167 }
168 }
169 }
170 { //->SASLResponse (scram client-final-message)
171 var buf: [1024]u8 = @splat(0);
172 var fbs: nio.FixedBufferStream([]u8) = .init(&buf);
173 try fbs.writeAll("c=biws");
174 try fbs.writeAll(",r=");
175 try fbs.writeAll(snonce_b64);
176
177 const salted_password = hi(Hmac, connect_url.password, salt_dec, scram_i);
178 const client_key = hmac(Hmac, &salted_password, "Client Key");
179 const auth_messagev: []const []const u8 = &.{ "n=", connect_url.username, ",r=", cnonce_b64, ",", "r=", snonce_b64, ",s=", salt_b64, ",i=", scram_i_s, ",", fbs.written() };
180 const stored_key = h(Hash, &client_key);
181 const client_signature = hmacv(Hmac, &stored_key, auth_messagev);
182 const client_proof = xor(client_key, client_signature);
183 try fbs.writeAll(",p=");
184 var stdw: std.Io.Writer = .fixed(fbs.written());
185 try Base64Enc.encodeWriter(&stdw, &client_proof);
186
187 try proto.SASLResponse.write(
188 &bufw,
189 fbs.written(),
190 );
191 try bufw.flush();
192 }
193 { //<-AuthenticationSASLFinal (scram server-final-message)
194 const t2: BackendMessageType = @enumFromInt(try bufr.readByte());
195 if (t2 == .ErrorResponse) try printError(&bufr, allocator);
196 std.debug.assert(t2 == .Authentication);
197 const len = try bufr.readInt(u32, .big);
198 const ok = try bufr.readInt(u32, .big);
199 std.debug.assert(ok == 12);
200 const data = try bufr.readAlloc(allocator, len - 8);
201 defer allocator.free(data);
202 }
203 { //<-AuthenticationOk
204 const t2: BackendMessageType = @enumFromInt(try bufr.readByte());
205 if (t2 == .ErrorResponse) try printError(&bufr, allocator);
206 std.debug.assert(t2 == .Authentication);
207 const len = try bufr.readInt(u32, .big);
208 std.debug.assert(len == 8);
209 const ok = try bufr.readInt(u32, .big);
210 std.debug.assert(ok == 0);
211 }
212 }
213 }
214 }
215 },
216 else => unreachable, // TODO
217 }
218 }
219
220 return .{
221 .conn = conn,
222 .bufw = bufw,
223 .bufr = bufr,
224 };
225}
226
227pub fn close(driver: *Driver) void {
228 driver.conn.close();
229}
230
231//
232
233pub fn exec(driver: *Driver, alloc: std.mem.Allocator, comptime query: []const u8, args: anytype) !void {
234 _ = driver;
235 _ = alloc;
236 _ = query;
237 _ = args;
238 @panic("TODO");
239}
240
241pub fn first(driver: *Driver, alloc: std.mem.Allocator, comptime T: type, comptime query: []const u8, args: anytype) !?T {
242 _ = driver;
243 _ = alloc;
244 _ = query;
245 _ = args;
246 @panic("TODO");
247}
248
249pub fn collect(driver: *Driver, alloc: std.mem.Allocator, comptime T: type, comptime query: []const u8, args: anytype) ![]T {
250 _ = driver;
251 _ = alloc;
252 _ = query;
253 _ = args;
254 @panic("TODO");
255}
256
257//
258
259pub fn doesTableExist(driver: *Driver, alloc: std.mem.Allocator, name: []const u8) !bool {
260 const t = tracer.trace(@src(), " {s}", .{name});
261 defer t.end();
262 return try driver.first(alloc, bool, "SELECT EXISTS ( SELECT FROM pg_tables WHERE schemaname = ? AND tablename = ? )", .{ "public", name }) orelse unreachable;
263}
264
265pub fn hasColumnWithName(driver: *Driver, alloc: std.mem.Allocator, comptime table: []const u8, comptime column: []const u8) !bool {
266 _ = driver;
267 _ = alloc;
268 _ = table;
269 _ = column;
270 @panic("TODO");
271}
272
273pub fn createTable(driver: *Driver, alloc: std.mem.Allocator, comptime name: []const u8, comptime pk_name: []const u8, pk_type: type) !void {
274 _ = driver;
275 _ = alloc;
276 _ = name;
277 _ = pk_name;
278 _ = pk_type;
279 @panic("TODO");
280}
281
282pub fn addColumn(driver: *Driver, alloc: std.mem.Allocator, comptime table_name: []const u8, comptime col_name: []const u8, T: type) !void {
283 _ = driver;
284 _ = alloc;
285 _ = table_name;
286 _ = col_name;
287 _ = T;
288 @panic("TODO");
289}
290
291pub fn addColumnForeign(driver: *Driver, alloc: std.mem.Allocator, comptime table_name: []const u8, comptime col_name: []const u8, T: type, comptime table_name2: []const u8, comptime col_name2: []const u8) !void {
292 _ = driver;
293 _ = alloc;
294 _ = table_name;
295 _ = col_name;
296 _ = T;
297 _ = table_name2;
298 _ = col_name2;
299 @panic("TODO");
300}
301
302pub fn nameForType(T: type) []const u8 {
303 if (@typeInfo(T) == .optional) {
304 return nameForType2(T);
305 }
306 return nameForType2(T) ++ " not null";
307}
308
309pub fn nameForType2(T: type) []const u8 {
310 _ = T;
311 @panic("TODO");
312}
313
314//
315// CancelRequest
316// GSSENCRequest
317// SSLRequest
318// StartupMessage
319
320pub const BackendMessageType = enum(u8) {
321 Authentication = 'R',
322 BackendKeyData = 'K',
323 BindComplete = '2',
324 CloseComplete = '3',
325 CommandComplete = 'C',
326 CopyData = 'd',
327 CopyDone = 'c',
328 CopyInResponse = 'G',
329 CopyOutResponse = 'H',
330 CopyBothResponse = 'W',
331 DataRow = 'D',
332 EmptyQueryResponse = 'I',
333 ErrorResponse = 'E',
334 FunctionCallResponse = 'V',
335 NegotiateProtocolVersion = 'v',
336 NoData = 'n',
337 NoticeResponse = 'N',
338 NotificationResponse = 'A',
339 ParameterDescription = 't',
340 ParameterStatus = 'S',
341 ParseComplete = '1',
342 PortalSuspended = 's',
343 ReadyForQuery = 'Z',
344 RowDescription = 'T',
345};
346
347pub const FrontendMessageType = enum(u8) {
348 Bind = 'B',
349 Close = 'C',
350 CopyData = 'd',
351 CopyDone = 'c',
352 CopyFail = 'f',
353 Describe = 'D',
354 Execute = 'E',
355 Flush = 'H',
356 FunctionCall = 'F',
357 GSSResponse = 'p',
358 Parse = 'P',
359 PasswordMessage = 'p',
360 Query = 'Q',
361 // SASLInitialResponse = 'p',
362 // SASLResponse = 'p',
363 Sync = 'S',
364 Terminate = 'X',
365};
366
367const proto = struct {
368 const StartupMessage = struct {
369 fn write(writer: anytype, username: []const u8, database: []const u8) !void {
370 const length: u32 = 4 + 4 +
371 4 + 1 + @as(u32, @intCast(username.len)) + 1 +
372 8 + 1 + @as(u32, @intCast(database.len)) + 1 +
373 1;
374 try writer.writeInt(u32, length, .big);
375 try writer.writeAll(&.{ 0, 3, 0, 0 });
376 try writer.writevAll(&.{ "user", "\x00", username, "\x00" });
377 try writer.writevAll(&.{ "database", "\x00", database, "\x00" });
378 try writer.writeAll("\x00");
379 }
380 };
381
382 const SASLInitialResponse = struct {
383 fn writev(writer: anytype, mechanism: []const u8, response_parts: []const []const u8) !void {
384 const parts_len = extras.sumLen(u8, response_parts);
385 const length: u32 = 4 +
386 @as(u32, @intCast(mechanism.len)) + 1 +
387 4 +
388 @as(u32, @intCast(parts_len)) +
389 0;
390 try writer.writeAll("p");
391 try writer.writeInt(u32, length, .big);
392 try writer.writevAll(&.{ mechanism, "\x00" });
393 try writer.writeInt(u32, @intCast(parts_len), .big);
394 try writer.writevAll(response_parts);
395 }
396 };
397
398 const SASLResponse = struct {
399 fn write(writer: anytype, response: []const u8) !void {
400 const length: u32 = 4 +
401 @as(u32, @intCast(response.len)) +
402 0;
403 try writer.writeAll("p");
404 try writer.writeInt(u32, length, .big);
405 try writer.writeAll(response);
406 }
407 };
408};
409
410fn h(Hash: type, str: []const u8) [Hash.digest_length]u8 {
411 var s: Hash = .init(.{});
412 s.update(str);
413 var out: [Hash.digest_length]u8 = @splat(0);
414 s.final(&out);
415 return out;
416}
417
418fn hmac(Hmac: type, key: []const u8, str: []const u8) [Hmac.mac_length]u8 {
419 var s: Hmac = .init(key);
420 s.update(str);
421 var out: [Hmac.mac_length]u8 = @splat(0);
422 s.final(&out);
423 return out;
424}
425fn hmacv(Hmac: type, key: []const u8, strs: []const []const u8) [Hmac.mac_length]u8 {
426 var s: Hmac = .init(key);
427 for (strs) |str| s.update(str);
428 var out: [Hmac.mac_length]u8 = @splat(0);
429 s.final(&out);
430 return out;
431}
432
433fn hi(Hmac: type, str: []const u8, salt: []const u8, i: u32) [Hmac.mac_length]u8 {
434 var dk: [Hmac.mac_length]u8 = @splat(0);
435 std.crypto.pwhash.pbkdf2(&dk, str, salt, i, Hmac) catch unreachable;
436 return dk;
437}
438
439fn xor(l: anytype, r: anytype) @TypeOf(l, r) {
440 var out: @TypeOf(l, r) = @splat(0);
441 for (&l, &r, &out) |a, b, *o| o.* = a ^ b;
442 return out;
443}
444
445fn printError(bufr: *nio.BufferedReader(4096, net.Stream), allocator: std.mem.Allocator) !noreturn {
446 const len = try bufr.readInt(u32, .big);
447 const data = try bufr.readAlloc(allocator, len - 4);
448 defer allocator.free(data);
449 var iter = std.mem.splitScalar(u8, data, 0);
450 while (iter.next()) |f| if (f.len > 0) std.log.err("{c}: {s}", .{ f[0], f[1..] });
451 std.process.exit(1);
452}