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
7 changes: 6 additions & 1 deletion src/network/protocols.zig
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,12 @@ pub const handShake = struct { // MARK: handShake

if (main.server.world.?.mode != .singleplayer) {
const keys = zon.getChild("keys");
try conn.user.?.identifyFromKeysAndName(name, keys);
try conn.user.?.identifyFromKeysAndName(name, keys, main.server.world.?.settings.whitelistEnabled.load(.monotonic));

if (!conn.user.?.allowedToJoin) {
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();
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});
Comment thread
IntegratedQuantum marked this conversation as resolved.
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
68 changes: 68 additions & 0 deletions src/server/command/whitelist.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
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>
Comment thread
IntegratedQuantum marked this conversation as resolved.
\\/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);

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.

The change also needs to be stored to disk immediately to avoid losing it when the game doesn't close correctly. (world.saveWorldConfig)

main.server.world.?.saveWorldConfig() catch |err| {
std.log.err("Error while saving world config: {s}", .{@errorName(err)});
};
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;
}
}
}
},
}
}
36 changes: 16 additions & 20 deletions src/server/players.zig
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,13 @@ pub fn getLocalPlayerIndex() usize {
return localPlayerIndex;
}

pub fn lookupIndex(key: []const u8) ?usize {
const LookupResult = struct { playerIndex: usize, blocked: bool };

pub fn lookupIndex(key: []const u8) ?LookupResult {
mutex.lock();
defer mutex.unlock();
const entry = playerDatabase.get(key) orelse return null;
return entry.playerIndex;
return .{.playerIndex = entry.playerIndex, .blocked = entry.blocked};
}

pub fn isEmpty() bool {
Expand Down Expand Up @@ -164,6 +166,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 +179,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,27 +189,19 @@ 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);
mutex.lock();
defer mutex.unlock();
const entry = playerDatabase.get(key) orelse return false;
return !entry.blocked;
}

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

init("test", 0);

try std.testing.expectEqual(false, isAllowedToJoin("ed25519:abc"));
try std.testing.expectEqual(null, lookupIndex("ed25519:abc"));
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(false, lookupIndex("ed25519:abc").?.blocked);
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(true, lookupIndex("ed25519:abc").?.blocked);
}

test "addUnblocks" {
Expand All @@ -215,25 +211,25 @@ 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(true, lookupIndex("ed25519:xyz").?.blocked);
try std.testing.expectEqual(.added, add("ed25519:xyz"));
try std.testing.expectEqual(true, isAllowedToJoin("ed25519:xyz"));
try std.testing.expectEqual(false, lookupIndex("ed25519:xyz").?.blocked);
}

test "knownPlayerAllowedByDefaultButBlockable" {
test "lookupIndexDistinguishesKnownAndUnknownKeys" {
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(false, lookupIndex("ed25519:known").?.blocked);
try std.testing.expectEqual(null, lookupIndex("ed25519:unknown"));

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

try std.testing.expectEqual(.added, add("ed25519:known"));
try std.testing.expectEqual(true, isAllowedToJoin("ed25519:known"));
try std.testing.expectEqual(false, lookupIndex("ed25519:known").?.blocked);
}
16 changes: 13 additions & 3 deletions src/server/server.zig
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ pub const User = struct { // MARK: User
newKeyString: ?[]const u8 = null,
key: network.authentication.PublicKey = undefined,
legacyKey: ?network.authentication.PublicKey = null,
allowedToJoin: bool = false,

inventoryClientToServerIdMap: std.AutoHashMap(InventoryId, InventoryId) = undefined,
inventory: ?InventoryId = null,
Expand Down Expand Up @@ -232,9 +233,10 @@ pub const User = struct { // MARK: User
self.jobQueue.deinit();
}

pub fn identifyFromKeysAndName(self: *User, name: []const u8, keys: main.ZonElement) !void {
pub fn identifyFromKeysAndName(self: *User, name: []const u8, keys: main.ZonElement, whitelistEnabled: bool) !void {
std.debug.assert(self.name.len == 0);
self.name = main.globalAllocator.dupe(u8, name);
self.allowedToJoin = !whitelistEnabled;
{
const keyBase64 = keys.get([]const u8, @tagName(main.settings.launchConfig.preferredAuthenticationAlgorithm)) orelse return error.PublicKeyNotPresent;
self.key = try .initFromBase64(keyBase64, main.settings.launchConfig.preferredAuthenticationAlgorithm);
Expand All @@ -245,7 +247,9 @@ pub const User = struct { // MARK: User
const keyBase64 = keys.get([]const u8, keyTypeName) orelse continue;
const keyWithType = main.stackAllocator.print("{s}:{s}", .{keyTypeName, keyBase64});
defer main.stackAllocator.free(keyWithType);
self.playerIndex = main.server.players.lookupIndex(keyWithType) orelse continue;
const lookup = main.server.players.lookupIndex(keyWithType) orelse continue;
self.playerIndex = lookup.playerIndex;
self.allowedToJoin = !lookup.blocked;
foundKey = true;
const keyType = std.meta.stringToEnum(main.network.authentication.KeyTypeEnum, keyTypeName).?;
if (keyType == self.key) break;
Expand All @@ -256,10 +260,16 @@ pub const User = struct { // MARK: User
if (main.server.players.isEmpty()) { // Claim the local player
std.log.info("Here", .{});
self.playerIndex = main.server.players.getLocalPlayerIndex();
self.allowedToJoin = true;
} else {
const nameEntry = main.stackAllocator.print("name:{s}", .{name});
defer main.stackAllocator.free(nameEntry);
self.playerIndex = main.server.players.lookupIndex(nameEntry) orelse main.server.players.allocateNewIndex();
if (main.server.players.lookupIndex(nameEntry)) |lookup| {
self.playerIndex = lookup.playerIndex;
self.allowedToJoin = !lookup.blocked;
} else {
self.playerIndex = main.server.players.allocateNewIndex();
}
}
}
}
Expand Down
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