Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/network.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1737,7 +1737,7 @@ pub const Connection = struct { // MARK: Connection

pub fn receive(self: *Connection, data: []const u8) void {
self.tryReceive(data) catch |err| {
std.log.err("Got error while processing received network data: {s}", .{@errorName(err)});
std.log.warn("Got error while processing received network data: {s}", .{@errorName(err)});
if (@errorReturnTrace()) |trace| {
std.log.info("{f}", .{main.fmt.FormatErrorTrace{.stackTrace = trace.*}});
}
Expand Down
5 changes: 5 additions & 0 deletions src/network/protocols.zig
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,11 @@ pub const handShake = struct { // MARK: handShake
const keys = zon.getChild("keys");
try conn.user.?.identifyFromKeysAndName(name, keys);

if (!main.server.players.isAllowedToJoin(conn.user.?.newKeyString.?, main.server.world.?.settings.whitelistEnabled.load(.monotonic))) {
std.log.info("Rejected connection from '{s}' ({s})", .{name, conn.user.?.newKeyString.?});
return error.NotWhitelisted;
}

var writer: utils.BinaryWriter = .init(main.stackAllocator);
defer writer.deinit();
writer.writeEnum(Connection.HandShakeState, .signatureRequest);
Expand Down
20 changes: 20 additions & 0 deletions src/server/command.zig
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,26 @@ pub const PlayerIndex = struct {
}
};

pub const KeyString = struct {
key: []const u8,

pub fn parse(_: NeverFailingAllocator, name: []const u8, arg: []const u8, errorMessage: *ListManaged(u8)) error{ParseError}!KeyString {
const colonIndex = std.mem.indexOfScalar(u8, arg, ':') orelse {
errorMessage.print("Expected a public key of the form \"<keyType>:<base64>\" for <{s}>, found \"{s}\"", .{name, arg});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of adding your own validation logic, please use authentication.PublicKey.initFromBase64

return error.ParseError;
};
const keyType = std.meta.stringToEnum(main.network.authentication.KeyTypeEnum, arg[0..colonIndex]) orelse {
errorMessage.print("Unknown key type \"{s}\" for <{s}>", .{arg[0..colonIndex], name});
return error.ParseError;
};
_ = main.network.authentication.PublicKey.initFromBase64(arg[colonIndex + 1 ..], keyType) catch {
errorMessage.print("Invalid public key \"{s}\" for <{s}>", .{arg, name});
return error.ParseError;
};
return .{.key = arg};
}
};

pub const BiomeId = struct {
biome: *const main.server.terrain.biomes.Biome,

Expand Down
1 change: 1 addition & 0 deletions src/server/command/_list.zig
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub const spawn = @import("spawn.zig");
pub const tickspeed = @import("tickspeed.zig");
pub const time = @import("time.zig");
pub const tp = @import("tp.zig");
pub const whitelist = @import("whitelist.zig");

pub const avatar = @import("entity/avatar.zig");

Expand Down
65 changes: 65 additions & 0 deletions src/server/command/whitelist.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const std = @import("std");

const main = @import("main");
const command = main.server.command;
const Source = command.Source;
const players = main.server.players;

pub const description = "Manages the connection whitelist";
pub const usage =
\\/whitelist <add/block> <keyType>:<base64Key>
\\/whitelist <add/block> @<playerIndex>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to also have whitelist enable/disable commands?
To implement this in a thread-safe manner you can just make the bool atomic.

\\/whitelist <enable/disable>
;

const Action = enum { add, block };
const Toggle = enum { enable, disable };

pub const Args = union(enum) {
@"/whitelist <action> <key>": struct { action: Action, key: command.KeyString },
@"/whitelist <action> <playerIndex>": struct { action: Action, playerIndex: command.PlayerIndex },
@"/whitelist <enable/disable>": struct { toggle: Toggle },
};

pub fn execute(args: Args, source: Source) void {
switch (args) {
.@"/whitelist <action> <key>" => |params| applyAction(source, params.action, params.key.key),
.@"/whitelist <action> <playerIndex>" => |params| {
const target = command.Target.fromPlayerIndex(params.playerIndex, source) catch return;
const key = target.user.newKeyString orelse {
source.sendMessage("#ff0000Player {s}§#ff0000 has no public key to whitelist", .{target.user.name});
return;
};
applyAction(source, params.action, key);
},
.@"/whitelist <enable/disable>" => |params| {
main.server.world.?.settings.whitelistEnabled.store(params.toggle == .enable, .monotonic);
source.sendMessage("#00ff00Whitelist {s}", .{if (params.toggle == .enable) "enabled" else "disabled"});
},
}
}

fn applyAction(source: Source, action: Action, key: []const u8) void {
switch (action) {
.add => switch (players.add(key)) {
.added => source.sendMessage("#00ff00Added {s}§#00ff00 to the whitelist", .{key}),
.alreadyAllowed => source.sendMessage("#ff0000{s}§#ff0000 is already on the whitelist", .{key}),
},
.block => {
switch (players.block(key)) {
.blocked => source.sendMessage("#00ff00Blocked {s}§#00ff00 from connecting", .{key}),
.alreadyBlocked => source.sendMessage("#ff0000{s}§#ff0000 is already blocked", .{key}),
}
const userList = main.server.getUserList(main.stackAllocator);
defer main.stackAllocator.free(userList);
for (userList) |user| {
if (user.newKeyString) |userKey| {
if (std.mem.eql(u8, userKey, key)) {
user.conn.disconnect();
break;
}
}
}
},
}
}
28 changes: 15 additions & 13 deletions src/server/players.zig
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ fn saveBlocked(index: usize, value: bool) void {
const AddResult = enum { added, alreadyAllowed };

pub fn add(key: []const u8) AddResult {
sync.threadContext.assertCorrectContext(.server);
mutex.lock();
defer mutex.unlock();
const result = ensurePlayerRecord(key);
Expand All @@ -176,6 +177,7 @@ pub fn add(key: []const u8) AddResult {
const BlockResult = enum { blocked, alreadyBlocked };

pub fn block(key: []const u8) BlockResult {
sync.threadContext.assertCorrectContext(.server);
mutex.lock();
defer mutex.unlock();
const result = ensurePlayerRecord(key);
Expand All @@ -185,11 +187,10 @@ pub fn block(key: []const u8) BlockResult {
return if (result.wasNew or !wasBlocked) .blocked else .alreadyBlocked;
}

pub fn isAllowedToJoin(key: []const u8) bool {
sync.threadContext.assertCorrectContext(.server);
pub fn isAllowedToJoin(key: []const u8, whitelistEnabled: bool) bool {
mutex.lock();
defer mutex.unlock();
const entry = playerDatabase.get(key) orelse return false;
const entry = playerDatabase.get(key) orelse return !whitelistEnabled;
return !entry.blocked;
}

Expand All @@ -199,13 +200,13 @@ test "addContainsRemove" {

init("test", 0);

try std.testing.expectEqual(false, isAllowedToJoin("ed25519:abc"));
try std.testing.expectEqual(false, isAllowedToJoin("ed25519:abc", true));
try std.testing.expectEqual(.added, add("ed25519:abc"));
try std.testing.expectEqual(.alreadyAllowed, add("ed25519:abc"));
try std.testing.expectEqual(true, isAllowedToJoin("ed25519:abc"));
try std.testing.expectEqual(true, isAllowedToJoin("ed25519:abc", true));
try std.testing.expectEqual(.blocked, block("ed25519:abc"));
try std.testing.expectEqual(.alreadyBlocked, block("ed25519:abc"));
try std.testing.expectEqual(false, isAllowedToJoin("ed25519:abc"));
try std.testing.expectEqual(false, isAllowedToJoin("ed25519:abc", true));
}

test "addUnblocks" {
Expand All @@ -215,25 +216,26 @@ test "addUnblocks" {
init("test", 0);

try std.testing.expectEqual(.blocked, block("ed25519:xyz"));
try std.testing.expectEqual(false, isAllowedToJoin("ed25519:xyz"));
try std.testing.expectEqual(false, isAllowedToJoin("ed25519:xyz", false));
try std.testing.expectEqual(.added, add("ed25519:xyz"));
try std.testing.expectEqual(true, isAllowedToJoin("ed25519:xyz"));
try std.testing.expectEqual(true, isAllowedToJoin("ed25519:xyz", false));
}

test "knownPlayerAllowedByDefaultButBlockable" {
test "whitelistToggleAffectsUnknownKeysOnly" {
main.heap.allocators.createWorldArena();
defer main.heap.allocators.destroyWorldArena();

init("test", 0);

playerDatabase.put(main.worldArena.allocator, main.worldArena.dupe(u8, "ed25519:known"), .{.playerIndex = 0, .blocked = false}) catch unreachable;

try std.testing.expectEqual(true, isAllowedToJoin("ed25519:known"));
try std.testing.expectEqual(false, isAllowedToJoin("ed25519:unknown"));
try std.testing.expectEqual(true, isAllowedToJoin("ed25519:known", true));
try std.testing.expectEqual(true, isAllowedToJoin("ed25519:unknown", false));
try std.testing.expectEqual(false, isAllowedToJoin("ed25519:unknown", true));

try std.testing.expectEqual(.blocked, block("ed25519:known"));
try std.testing.expectEqual(false, isAllowedToJoin("ed25519:known"));
try std.testing.expectEqual(false, isAllowedToJoin("ed25519:known", false));

try std.testing.expectEqual(.added, add("ed25519:known"));
try std.testing.expectEqual(true, isAllowedToJoin("ed25519:known"));
try std.testing.expectEqual(true, isAllowedToJoin("ed25519:known", false));
}
6 changes: 5 additions & 1 deletion src/server/world.zig
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ pub const Settings = struct {
defaultGamemode: Gamemode = .creative,
allowCheats: bool = true,
testingMode: bool = false,
whitelistEnabled: std.atomic.Value(bool) = .init(false),
seed: u64 = undefined,

pub const defaults: Settings = .{};
Expand All @@ -48,15 +49,17 @@ pub const Settings = struct {
.defaultGamemode = std.meta.stringToEnum(main.game.Gamemode, zon.get([]const u8, "defaultGamemode") orelse @tagName(defaults.defaultGamemode)) orelse defaults.defaultGamemode,
.allowCheats = zon.get(bool, "allowCheats") orelse defaults.allowCheats,
.testingMode = zon.get(bool, "testingMode") orelse defaults.testingMode,
.whitelistEnabled = .init(zon.get(bool, "whitelistEnabled") orelse defaults.whitelistEnabled.load(.monotonic)),
};
}

pub fn toZon(self: Settings, allocator: NeverFailingAllocator) ZonElement {
pub fn toZon(self: *const Settings, allocator: NeverFailingAllocator) ZonElement {
const zon = main.ZonElement.initObject(allocator);

zon.put("defaultGamemode", @tagName(self.defaultGamemode));
zon.put("allowCheats", self.allowCheats);
zon.put("testingMode", self.testingMode);
zon.put("whitelistEnabled", self.whitelistEnabled.load(.monotonic));
zon.put("seed", self.seed);

return zon;
Expand Down Expand Up @@ -665,6 +668,7 @@ pub const ServerWorld = struct { // MARK: ServerWorld
worldData.put("name", self.name);
worldData.put("lastUsedTime", std.Io.Clock.Timestamp.now(main.io, .real).raw.toMilliseconds());
worldData.put("tickSpeed", self.tickSpeed.load(.monotonic));
worldData.put("settings", self.settings.toZon(main.stackAllocator));
worldData.put("localPlayer", players.getLocalPlayerIndex());

try files.cubyzDir().writeZon(path, worldData);
Expand Down
Loading