Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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);
Comment thread
IntegratedQuantum marked this conversation as resolved.
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 {
Comment thread
IntegratedQuantum marked this conversation as resolved.
Outdated
mutex.lock();
defer mutex.unlock();
const entry = playerDatabase.get(key) orelse return null;
return entry.playerIndex;
return .{.playerIndex = entry.playerIndex, .blocked = entry.blocked};
Comment thread
IntegratedQuantum marked this conversation as resolved.
Outdated
}

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;
Comment thread
IntegratedQuantum marked this conversation as resolved.
Outdated
{
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