commands: endSessions judges the array it is handed
It stays a no-op -- there is nothing to end -- but stops being a blind `ok`. This is the one session command that arrives in normal operation: a driver sends it on close for every session it handed out. Validating an argument that is then discarded looks like ceremony, and is not: `ok: 1` to a malformed `endSessions` says the server understood something it never read, which is the same class of answer the previous commit removed for `txnNumber`. Codes and messages measured against mongod 8.3.7, including the field path `endSessions.endSessionsFromClient` -- an IDL artefact, since the command's own field is `endSessions` and the parsed argument carries a different name. It is reproduced rather than tidied: a path that differs from the real server's is worse than an odd one. With this the raw probe's replies are identical to mongod's for every lsid and endSessions shape. What is left is five deliberate divergences, all recorded: three because mongod knows per command whether `txnNumber` is even accepted and answers Location50889 or 263 before reaching the standalone refusal, and two because `startSession` and `refreshSessions` are not implemented -- a driver calls neither, generating session ids locally, so CommandNotFound is the honest answer. `commitTransaction` joins them, and it can only be reached by a client whose write this server already refused. 175/175 unit tests.
This commit is contained in:
141
src/commands.zig
141
src/commands.zig
@@ -559,10 +559,108 @@ fn reject_bad_session_fields(msg: *wire.Message, reply: *wire.Reply, cmd: []cons
|
|||||||
return reject_txn_fields(msg, reply, cmd);
|
return reject_txn_fields(msg, reply, cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cmd_end_sessions(_: *Context, _: *wire.Message, reply: *wire.Reply) !void {
|
/// Still a no-op -- there is nothing to end -- but no longer a blind `ok`.
|
||||||
|
///
|
||||||
|
/// A driver sends this on close for every session it handed out, so it is the
|
||||||
|
/// one session command that arrives in normal operation. Validating an array
|
||||||
|
/// we then discard looks like ceremony; it is not. `ok: 1` to a malformed
|
||||||
|
/// `endSessions` is the same class of answer as `ok: 1` to a transactional
|
||||||
|
/// write: the client is told the server understood, and it did not. mongod's
|
||||||
|
/// field path is `endSessions.endSessionsFromClient`, an IDL artefact -- the
|
||||||
|
/// command's own field is `endSessions` and the parsed argument has a
|
||||||
|
/// different name -- and it is reproduced rather than tidied, because a name
|
||||||
|
/// that differs from the real server's is worse than an odd one.
|
||||||
|
fn cmd_end_sessions(_: *Context, msg: *wire.Message, reply: *wire.Reply) !void {
|
||||||
|
const arena = reply.arena_alloc();
|
||||||
|
const sessions = switch (msg.body.get("endSessions").?) {
|
||||||
|
.array => |a| a,
|
||||||
|
else => |v| {
|
||||||
|
const text = try std.fmt.allocPrint(
|
||||||
|
arena,
|
||||||
|
"BSON field 'endSessions.endSessions' is the wrong type '{s}', expected type 'array'",
|
||||||
|
.{v.type_name()},
|
||||||
|
);
|
||||||
|
return reply.put_error(@intFromEnum(ErrorCode.type_mismatch), "TypeMismatch", text);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (sessions, 0..) |entry, i| {
|
||||||
|
if (try reject_bad_session_entry(entry, i, reply)) return;
|
||||||
|
}
|
||||||
try reply.put_ok();
|
try reply.put_ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One element of `endSessions`. Answers whether an error reply was written.
|
||||||
|
fn reject_bad_session_entry(entry: bson.Value, i: usize, reply: *wire.Reply) !bool {
|
||||||
|
const arena = reply.arena_alloc();
|
||||||
|
const prefix = "BSON field 'endSessions.endSessionsFromClient";
|
||||||
|
const doc = switch (entry) {
|
||||||
|
.doc => |d| d,
|
||||||
|
else => {
|
||||||
|
const text = try std.fmt.allocPrint(
|
||||||
|
arena,
|
||||||
|
"{s}.{d}' is the wrong type '{s}', expected type 'object'",
|
||||||
|
.{ prefix, i, entry.type_name() },
|
||||||
|
);
|
||||||
|
try reply.put_error(@intFromEnum(ErrorCode.type_mismatch), "TypeMismatch", text);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var seen_id = false;
|
||||||
|
for (doc) |pair| {
|
||||||
|
if (std.mem.eql(u8, pair.key, "uid")) continue;
|
||||||
|
if (!std.mem.eql(u8, pair.key, "id")) {
|
||||||
|
const text = try std.fmt.allocPrint(
|
||||||
|
arena,
|
||||||
|
"{s}.{s}' is an unknown field.",
|
||||||
|
.{ prefix, pair.key },
|
||||||
|
);
|
||||||
|
try reply.put_error(@intFromEnum(ErrorCode.idl_unknown_field), "IDLUnknownField", text);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
seen_id = true;
|
||||||
|
const bin = switch (pair.value) {
|
||||||
|
.binary => |b| b,
|
||||||
|
else => {
|
||||||
|
const text = try std.fmt.allocPrint(
|
||||||
|
arena,
|
||||||
|
"{s}.id' is the wrong type '{s}', expected type 'binData'",
|
||||||
|
.{ prefix, pair.value.type_name() },
|
||||||
|
);
|
||||||
|
try reply.put_error(@intFromEnum(ErrorCode.type_mismatch), "TypeMismatch", text);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (bin.subtype != 4) {
|
||||||
|
const text = try std.fmt.allocPrint(
|
||||||
|
arena,
|
||||||
|
"{s}.id' is the wrong binData type '{s}', expected type 'UUID'",
|
||||||
|
.{ prefix, subtype_name(bin.subtype) },
|
||||||
|
);
|
||||||
|
try reply.put_error(@intFromEnum(ErrorCode.type_mismatch), "TypeMismatch", text);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (bin.data.len != 16) {
|
||||||
|
try reply.put_error(
|
||||||
|
@intFromEnum(ErrorCode.invalid_uuid),
|
||||||
|
"InvalidUUID",
|
||||||
|
"uuid must be a 16-byte binary field with UUID (4) subtype",
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!seen_id) {
|
||||||
|
try reply.put_error(
|
||||||
|
@intFromEnum(ErrorCode.idl_failed_to_parse),
|
||||||
|
"IDLFailedToParse",
|
||||||
|
"BSON field 'endSessions.endSessionsFromClient.id' is missing but a required field",
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
fn cmd_connection_status(_: *Context, _: *wire.Message, reply: *wire.Reply) !void {
|
fn cmd_connection_status(_: *Context, _: *wire.Message, reply: *wire.Reply) !void {
|
||||||
const auth_info = try reply.arena_alloc().alloc(bson.Pair, 2);
|
const auth_info = try reply.arena_alloc().alloc(bson.Pair, 2);
|
||||||
auth_info[0] = .{ .key = "authenticatedUsers", .value = .{ .array = &.{} } };
|
auth_info[0] = .{ .key = "authenticatedUsers", .value = .{ .array = &.{} } };
|
||||||
@@ -3194,6 +3292,47 @@ test "a transactional write is refused rather than applied" {
|
|||||||
try testing.expectEqual(@as(i64, 0), try doc_count(&ctx, "txn"));
|
try testing.expectEqual(@as(i64, 0), try doc_count(&ctx, "txn"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "endSessions judges the array it is handed" {
|
||||||
|
// Still a no-op, and that is not what is being tested. The command arrives
|
||||||
|
// in normal operation -- a driver sends it on close for every session it
|
||||||
|
// handed out -- so a blind `ok` here tells a client the server understood
|
||||||
|
// something it never looked at. Codes measured against mongod 8.3.7.
|
||||||
|
var threaded: std.Io.Threaded = .init_single_threaded;
|
||||||
|
defer threaded.deinit();
|
||||||
|
const io = threaded.io();
|
||||||
|
var tdb = try TestDb.init(io);
|
||||||
|
defer tdb.deinit();
|
||||||
|
var ctx = tdb.ctx(io);
|
||||||
|
|
||||||
|
const uuid = [_]u8{0x6B} ** 16;
|
||||||
|
const good = bson.Value{ .binary = .{ .subtype = 4, .data = &uuid } };
|
||||||
|
const one = bson.Value{ .doc = &.{.{ .key = "id", .value = good }} };
|
||||||
|
|
||||||
|
try testing.expectEqual(@as(?i32, null), try run_for_code(&ctx, "endSessions", .{ .array = &.{} }, &.{}));
|
||||||
|
try testing.expectEqual(@as(?i32, null), try run_for_code(&ctx, "endSessions", .{ .array = &.{one} }, &.{}));
|
||||||
|
// `uid` rides along once authentication is on, exactly as it does in lsid.
|
||||||
|
try testing.expectEqual(@as(?i32, null), try run_for_code(&ctx, "endSessions", .{ .array = &.{.{ .doc = &.{
|
||||||
|
.{ .key = "id", .value = good },
|
||||||
|
.{ .key = "uid", .value = .{ .binary = .{ .subtype = 0, .data = &[_]u8{0} ** 32 } } },
|
||||||
|
} }} }, &.{}));
|
||||||
|
|
||||||
|
try testing.expectEqual(@as(i32, 14), (try run_for_code(&ctx, "endSessions", .{ .string = "nope" }, &.{})).?);
|
||||||
|
try testing.expectEqual(@as(i32, 14), (try run_for_code(&ctx, "endSessions", .{ .array = &.{.{ .int32 = 5 }} }, &.{})).?);
|
||||||
|
try testing.expectEqual(@as(i32, 14), (try run_for_code(&ctx, "endSessions", .{ .array = &.{.{ .doc = &.{
|
||||||
|
.{ .key = "id", .value = .{ .string = "nope" } },
|
||||||
|
} }} }, &.{})).?);
|
||||||
|
try testing.expectEqual(@as(i32, 207), (try run_for_code(&ctx, "endSessions", .{ .array = &.{.{ .doc = &.{
|
||||||
|
.{ .key = "id", .value = .{ .binary = .{ .subtype = 4, .data = uuid[0..15] } } },
|
||||||
|
} }} }, &.{})).?);
|
||||||
|
try testing.expectEqual(@as(i32, 40414), (try run_for_code(&ctx, "endSessions", .{ .array = &.{.{ .doc = &.{} }} }, &.{})).?);
|
||||||
|
try testing.expectEqual(@as(i32, 40415), (try run_for_code(&ctx, "endSessions", .{ .array = &.{.{ .doc = &.{
|
||||||
|
.{ .key = "id", .value = good },
|
||||||
|
.{ .key = "bogus", .value = .{ .int32 = 1 } },
|
||||||
|
} }} }, &.{})).?);
|
||||||
|
// A bad entry after a good one is still a bad entry.
|
||||||
|
try testing.expectEqual(@as(i32, 14), (try run_for_code(&ctx, "endSessions", .{ .array = &.{ one, .{ .int32 = 5 } } }, &.{})).?);
|
||||||
|
}
|
||||||
|
|
||||||
test "an unknown command is reported before its session id is judged" {
|
test "an unknown command is reported before its session id is judged" {
|
||||||
// Measured: mongod answers CommandNotFound to `{nosuchcmd: 1, lsid: 5}`,
|
// Measured: mongod answers CommandNotFound to `{nosuchcmd: 1, lsid: 5}`,
|
||||||
// so the lookup comes first and this is where the check belongs.
|
// so the lookup comes first and this is where the check belongs.
|
||||||
|
|||||||
Reference in New Issue
Block a user