feat: add handleMetrics and handleAuth to server
This commit is contained in:
1 parent
af3de01cad
commit
cf471eb9f7
1 file changed
+157
+157
@@ -929,3 +929,160 @@ pub const Server = struct {
|
||||
}
|
||||
}
|
||||
|
||||
/// Answers the `metrics` opcode: the network counterpart of the CLI's
|
||||
/// `stats --prom` (which only ever renders `Db.stats()` locally) plus
|
||||
/// this server's own request/rate-limit/auth/session counters from
|
||||
/// `registerMetrics` - until now those counters were only ever
|
||||
/// observable in-process, by a test holding a direct `*Server`
|
||||
/// pointer. The registry is built fresh per request and thrown away
|
||||
/// immediately after rendering: there is nothing to keep alive between
|
||||
/// calls, since every counter it reads lives on `self` regardless.
|
||||
fn handleMetrics(self: *Server, writer: anytype) !void {
|
||||
var reg = metrics.Registry.init(self.allocator);
|
||||
defer reg.deinit();
|
||||
self.registerMetrics(®) catch |err| return writeErr(writer, .internal, @errorName(err));
|
||||
|
||||
var buf = std.ArrayList(u8).init(self.allocator);
|
||||
defer buf.deinit();
|
||||
reg.exportText(self.allocator, buf.writer()) catch |err| return writeErr(writer, .internal, @errorName(err));
|
||||
metrics.formatDbStats(buf.writer(), "cobalt", self.db.stats()) catch |err| return writeErr(writer, .internal, @errorName(err));
|
||||
|
||||
try protocol.writeFrame(writer, @intFromEnum(protocol.Status.ok), buf.items);
|
||||
}
|
||||
|
||||
/// Verifies credentials against `store` and, on success, stamps the
|
||||
/// resulting role into this connection's `connRole` slot so every
|
||||
/// subsequent request on the same connection is dispatched under it.
|
||||
/// Locks `self.mutex` only around the actual `Db` read `verify`
|
||||
/// performs, same as every other handler here.
|
||||
fn handleAuth(self: *Server, store: authmod.Store, payload: []const u8, writer: anytype, connRole: *?authmod.Role, connExpiresAtNanos: *?i128) !void {
|
||||
const args = protocol.decodeAuth(payload) catch {
|
||||
return writeErr(writer, .bad_request, "malformed auth payload");
|
||||
};
|
||||
|
||||
self.mutex.lock();
|
||||
const result = store.verify(self.allocator, args.username, args.password);
|
||||
self.mutex.unlock();
|
||||
|
||||
const role = result catch |err| {
|
||||
self.unauthorizedTotal.incr();
|
||||
return switch (err) {
|
||||
error.InvalidCredentials, error.Malformed => writeErr(writer, .unauthorized, "invalid credentials"),
|
||||
else => writeErr(writer, .internal, @errorName(err)),
|
||||
};
|
||||
};
|
||||
connRole.* = role;
|
||||
connExpiresAtNanos.* = null;
|
||||
|
||||
if (self.sessions) |store_| {
|
||||
const now = std.time.nanoTimestamp();
|
||||
const token = store_.issue(role, self.sessionTtlNanos, now) catch |err| {
|
||||
return writeErr(writer, .internal, @errorName(err));
|
||||
};
|
||||
defer self.allocator.free(token);
|
||||
connExpiresAtNanos.* = now + self.sessionTtlNanos;
|
||||
const responsePayload = protocol.encodeAuthOk(self.allocator, token) catch |err| {
|
||||
return writeErr(writer, .internal, @errorName(err));
|
||||
};
|
||||
defer self.allocator.free(responsePayload);
|
||||
return protocol.writeFrame(writer, @intFromEnum(protocol.Status.ok), responsePayload);
|
||||
}
|
||||
try protocol.writeFrame(writer, @intFromEnum(protocol.Status.ok), "");
|
||||
}
|
||||
|
||||
/// Authenticates a connection from a previously issued session token
|
||||
/// instead of a username/password pair (see `enableSessions`). Only
|
||||
/// reachable when `self.sessions` is set, since `dispatch` only routes
|
||||
/// `auth_token` here while `self.authStore` is also set.
|
||||
fn handleAuthToken(self: *Server, payload: []const u8, writer: anytype, connRole: *?authmod.Role, connExpiresAtNanos: *?i128) !void {
|
||||
const store = self.sessions orelse {
|
||||
self.unauthorizedTotal.incr();
|
||||
return writeErr(writer, .unauthorized, "session tokens are not enabled on this server");
|
||||
};
|
||||
const token = protocol.decodeAuthToken(payload);
|
||||
const session = store.validate(token, std.time.nanoTimestamp()) orelse {
|
||||
self.unauthorizedTotal.incr();
|
||||
return writeErr(writer, .unauthorized, "invalid or expired session token");
|
||||
};
|
||||
connRole.* = session.role;
|
||||
connExpiresAtNanos.* = session.expiresAtNanos;
|
||||
try protocol.writeFrame(writer, @intFromEnum(protocol.Status.ok), "");
|
||||
}
|
||||
|
||||
fn handleScan(self: *Server, payload: []const u8, writer: anytype) !void {
|
||||
const args = protocol.decodeScan(payload) catch {
|
||||
return writeErr(writer, .bad_request, "malformed scan payload");
|
||||
};
|
||||
var entries = std.ArrayList(dbmod.Entry).init(self.allocator);
|
||||
defer entries.deinit();
|
||||
|
||||
if (args.reverse) {
|
||||
var it = self.db.scanReverse(self.allocator, .{ .start = args.start, .end = args.end }) catch |err| {
|
||||
return writeErr(writer, .internal, @errorName(err));
|
||||
};
|
||||
defer it.deinit();
|
||||
while (try it.next()) |e| {
|
||||
try entries.append(e);
|
||||
if (args.limit != 0 and entries.items.len >= args.limit) break;
|
||||
}
|
||||
} else {
|
||||
var it = self.db.scanRange(self.allocator, .{ .start = args.start, .end = args.end }) catch |err| {
|
||||
return writeErr(writer, .internal, @errorName(err));
|
||||
};
|
||||
defer it.deinit();
|
||||
while (try it.next()) |e| {
|
||||
try entries.append(e);
|
||||
if (args.limit != 0 and entries.items.len >= args.limit) break;
|
||||
}
|
||||
}
|
||||
|
||||
const out = protocol.encodeEntries(self.allocator, entries.items) catch |err| {
|
||||
return writeErr(writer, .internal, @errorName(err));
|
||||
};
|
||||
defer self.allocator.free(out);
|
||||
try protocol.writeFrame(writer, @intFromEnum(protocol.Status.ok), out);
|
||||
}
|
||||
|
||||
fn handlePrefix(self: *Server, payload: []const u8, writer: anytype) !void {
|
||||
const args = protocol.decodePrefix(payload) catch {
|
||||
return writeErr(writer, .bad_request, "malformed prefix payload");
|
||||
};
|
||||
var it = self.db.scanPrefix(self.allocator, args.prefix) catch |err| {
|
||||
return writeErr(writer, .internal, @errorName(err));
|
||||
};
|
||||
defer it.deinit();
|
||||
|
||||
var entries = std.ArrayList(dbmod.Entry).init(self.allocator);
|
||||
defer entries.deinit();
|
||||
while (try it.next()) |e| {
|
||||
try entries.append(e);
|
||||
if (args.limit != 0 and entries.items.len >= args.limit) break;
|
||||
}
|
||||
|
||||
const out = protocol.encodeEntries(self.allocator, entries.items) catch |err| {
|
||||
return writeErr(writer, .internal, @errorName(err));
|
||||
};
|
||||
defer self.allocator.free(out);
|
||||
try protocol.writeFrame(writer, @intFromEnum(protocol.Status.ok), out);
|
||||
}
|
||||
|
||||
fn handleBatch(self: *Server, payload: []const u8, writer: anytype) !void {
|
||||
const ops = protocol.decodeBatch(self.allocator, payload) catch {
|
||||
return writeErr(writer, .bad_request, "malformed batch payload");
|
||||
};
|
||||
defer self.allocator.free(ops);
|
||||
|
||||
var wb = dbmod.WriteBatch.init(self.allocator);
|
||||
defer wb.deinit();
|
||||
for (ops) |op| {
|
||||
switch (op) {
|
||||
.put => |kv| try wb.put(kv.key, kv.value),
|
||||
.del => |key| try wb.delete(key),
|
||||
}
|
||||
}
|
||||
self.db.apply(&wb) catch |err| {
|
||||
return writeErr(writer, .internal, @errorName(err));
|
||||
};
|
||||
try protocol.writeFrame(writer, @intFromEnum(protocol.Status.ok), "");
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user