This commit is contained in:
iceBear67 committed 2024-07-07 19:01:30 +08:00
commit 16fd49be35
17 files changed
+1356

No files matched your search

@@ -0,0 +1,46 @@
package io.ib67.sfcraft;
import io.ib67.sfcraft.access.AccessController;
import lombok.RequiredArgsConstructor;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.text.Text;
import net.minecraft.text.TextColor;
import net.minecraft.util.StringHelper;
import org.jetbrains.annotations.Nullable;
import java.net.InetSocketAddress;
public final class Commands {
@RequiredArgsConstructor
static class AccessControl {
private final AccessController controller;
public void grantAccess(ServerCommandSource sender, String name, @Nullable String ip) {
if (!sender.hasPermissionLevel(2)) {
sender.sendMessage(Text.of("Permission denied."));
return;
}
if (!StringHelper.isValidPlayerName(name)) {
sender.sendMessage(Text.of("Invalid player name."));
return;
}
if (ip == null) {
sender.sendMessage(Text.of("You're granting ANY access to player " + name + "!"));
controller.grantAccess(name, null);
} else {
controller.grantAccess(name, ip);
}
sender.sendMessage(Text.of(name + ": Access granted."));
}
public void revokeAccess(ServerCommandSource sender, String name) {
controller.revokeAccess(name);
sender.sendMessage(Text.of(name + ": Permission revoked."));
}
public void listAccesses(ServerCommandSource sender) {
controller.getEntries().forEach(it -> sender.sendMessage(Text.of(it.name() + ": " + (it.condition() == null ? "ANY" : it.condition()))));
}
}
}
@@ -0,0 +1,41 @@
package io.ib67.sfcraft;
import com.maxmind.geoip2.record.City;
import lombok.SneakyThrows;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.minecraft.network.ClientConnection;
import net.minecraft.network.DisconnectionInfo;
import net.minecraft.text.Text;
import java.net.InetSocketAddress;
import java.time.ZonedDateTime;
import java.util.function.Consumer;
public class Listener {
private final SFCraft core;
public Listener(SFCraft core) {
this.core = core;
}
@SneakyThrows
public boolean onPlayerLogin(String currentPlayer, ClientConnection connection, Consumer<Text> disconnect) {
if (connection.getAddress() instanceof InetSocketAddress address) {
var clock = core.getGeoIPService().clockOf(address.getAddress());
var hourOfDay = ZonedDateTime.now(clock).getHour();
var startHr = SFCraft.getInstance().getConfig().maintainceStartHour;
var endHr = SFCraft.getInstance().getConfig().maintainceEndHour;
if (startHr > hourOfDay && hourOfDay > endHr) {
final var msg = "服务器尚未开放\n服务器维护时间: %1$d~%2$d\n你的下午不值得浪费在游戏上,晚点再来吧!";
disconnect.accept(Text.of(String.format(msg, startHr, endHr)));
return false;
}
var canJoin = core.getAccessController().checkAccess(currentPlayer, address.getAddress().getHostAddress());
if (!canJoin) {
final var msg = "";
disconnect.accept(Text.of(String.format(msg, startHr, endHr)));
}
}
return true;
}
}
@@ -0,0 +1,6 @@
package io.ib67.sfcraft;
public class SFConfig {
public int maintainceStartHour = 17;
public int maintainceEndHour = 1;
}
+113
View File
@@ -0,0 +1,113 @@
package io.ib67.sfcraft;
import com.google.gson.Gson;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import io.ib67.sfcraft.access.AccessController;
import io.ib67.sfcraft.access.GrantedEntry;
import io.ib67.sfcraft.access.impl.SimpleAccessController;
import io.ib67.sfcraft.geoip.GeoIPService;
import lombok.Getter;
import lombok.SneakyThrows;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.minecraft.command.CommandRegistryAccess;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Collectors;
public class SFCraft implements ModInitializer {
@Getter
private SFConfig config;
@Getter
private static Listener listener;
@Getter
private static SFCraft instance;
@Getter
private GeoIPService geoIPService;
@Getter
private AccessController accessController;
@Getter
private Path root;
@Override
@SneakyThrows
public void onInitialize() {
instance = this;
listener = new Listener(this);
root = Path.of("sfcraft");
if (Files.notExists(root)) {
Files.createDirectory(root);
}
loadConfig();
geoIPService = new GeoIPService();
accessController = loadAccessController();
CommandRegistrationCallback.EVENT.register(this::registerCommands);
}
@SneakyThrows
private void loadConfig() {
var gson = new Gson();
var cfgFile = root.resolve("config.json");
if (Files.notExists(cfgFile)) {
Files.writeString(cfgFile, gson.toJson(new SFConfig()));
config = new SFConfig();
return;
}
config = gson.fromJson(Files.readString(cfgFile), SFConfig.class);
}
@SneakyThrows
private AccessController loadAccessController() {
var file = root.resolve("access.txt");
ServerLifecycleEvents.SERVER_STOPPING.register(this::saveAccessRules);
if (Files.notExists(file)) {
return new SimpleAccessController(List.of());
}
return new SimpleAccessController(Files.readAllLines(file).stream().map(it -> it.split(":")).filter(it -> it.length == 2).map(it -> new GrantedEntry(it[0].trim(), it[1].trim())).toList());
}
@SneakyThrows
private void saveAccessRules(MinecraftServer minecraftServer) {
var file = root.resolve("access.txt");
Files.writeString(file, accessController.getEntries().stream().map(i -> i.name() + ":" + i.condition()).collect(Collectors.joining("\n")));
}
private void registerCommands(CommandDispatcher<ServerCommandSource> dispatcher, CommandRegistryAccess commandRegistryAccess, CommandManager.RegistrationEnvironment registrationEnvironment) {
var accessControl = new Commands.AccessControl(accessController);
dispatcher.register(
LiteralArgumentBuilder.<ServerCommandSource>literal("access")
.requires(it -> it.hasPermissionLevel(2))
.then(LiteralArgumentBuilder.<ServerCommandSource>literal("grant")
.then(RequiredArgumentBuilder.<ServerCommandSource, String>argument("name", StringArgumentType.string()).executes(it -> {
accessControl.grantAccess(it.getSource(), it.getArgument("name", String.class), null);
return 0;
}).then(RequiredArgumentBuilder.<ServerCommandSource, String>argument("ip", StringArgumentType.string()).executes(it -> {
accessControl.grantAccess(it.getSource(), it.getArgument("name", String.class), it.getArgument("ip", String.class));
return 0;
}))))
.then(LiteralArgumentBuilder.<ServerCommandSource>literal("list")
.executes(it -> {
accessControl.listAccesses(it.getSource());
return 0;
})
).then(LiteralArgumentBuilder.<ServerCommandSource>literal("revoke")
.then(RequiredArgumentBuilder.<ServerCommandSource, String>argument("name", StringArgumentType.string())
.executes(it -> {
accessControl.revokeAccess(it.getSource(), it.getArgument("name", String.class));
return 0;
}))
)
);
}
}
@@ -0,0 +1,18 @@
package io.ib67.sfcraft.access;
import com.mojang.authlib.GameProfile;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.Collection;
import java.util.List;
public interface AccessController {
boolean checkAccess(String name, String address);
void grantAccess(String name, String condition);
void revokeAccess(String name);
Collection<? extends GrantedEntry> getEntries();
}
@@ -0,0 +1,8 @@
package io.ib67.sfcraft.access;
import com.mojang.authlib.GameProfile;
import java.net.InetSocketAddress;
public record GrantedEntry(String name, String condition) {
}
@@ -0,0 +1,42 @@
package io.ib67.sfcraft.access.impl;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import io.ib67.sfcraft.access.AccessController;
import io.ib67.sfcraft.access.GrantedEntry;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
import java.util.stream.Collectors;
public class SimpleAccessController implements AccessController {
private final Multimap<String, String> whitelist = Multimaps.newListMultimap(new HashMap<>(), ArrayList::new);
public SimpleAccessController(List<GrantedEntry> entries) {
entries.forEach(i -> whitelist.put(i.name(), i.condition()));
}
@Override
public boolean checkAccess(String profile, String address) {
return whitelist.containsEntry(profile, address) || whitelist.containsEntry(profile, null);
}
@Override
public void grantAccess(String profile, String condition) {
whitelist.put(profile, condition);
}
@Override
public void revokeAccess(String profile) {
whitelist.removeAll(profile);
}
@Override
public Collection<? extends GrantedEntry> getEntries() {
return whitelist.asMap().entrySet()
.stream().flatMap(it -> it.getValue().stream().map(i -> new GrantedEntry(it.getKey(), i)))
.collect(Collectors.toSet());
}
}
@@ -0,0 +1,75 @@
package io.ib67.sfcraft.geoip;
import com.maxmind.geoip2.DatabaseReader;
import com.maxmind.geoip2.exception.GeoIp2Exception;
import com.maxmind.geoip2.record.Country;
import io.ib67.sfcraft.SFCraft;
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Clock;
import java.time.ZoneId;
import java.util.TimeZone;
import static java.net.http.HttpResponse.BodyHandlers.ofFile;
@Log4j2
public class GeoIPService implements Closeable {
private static final URI GEOIP_DOWNLOAD_URL = URI.create("https://cdn.jsdelivr.net/gh/Loyalsoldier/geoip@release/Country.mmdb");
private static final Path COUNTRY_MMDB_PATH = SFCraft.getInstance().getRoot().resolve("country.mmdb");
private final DatabaseReader databaseReader;
public GeoIPService() throws InterruptedException {
databaseReader = loadData();
}
@SneakyThrows
public Clock clockOf(InetAddress address) throws GeoIp2Exception {
return Clock.system(TimeZone.getTimeZone(databaseReader.country(address).getCountry().getIsoCode()).toZoneId());
}
private DatabaseReader loadData() throws InterruptedException {
try {
return new DatabaseReader.Builder(COUNTRY_MMDB_PATH.toFile()).build();
} catch (IOException ignored) {
return updateData();
}
}
private DatabaseReader updateData() throws InterruptedException {
var client = HttpClient.newHttpClient();
var req = HttpRequest.newBuilder(GEOIP_DOWNLOAD_URL).GET().build();
int retries = 0;
do {
try {
log.info("Downloading GeoIP database, please wait....");
client.send(req, ofFile(COUNTRY_MMDB_PATH));
return new DatabaseReader.Builder(COUNTRY_MMDB_PATH.toFile()).build();
} catch (IOException | InterruptedException e) {
log.error("Failed to download or the database is corrupted!", e);
int sleep = (int) Math.pow(2, ++retries);
log.info("Retrying after " + sleep + "sec.");
Thread.sleep(sleep);
} finally {
client.close();
}
} while (retries < 4);
throw new InterruptedException("Cannot load GeoIP data.");
}
@Override
public void close() throws IOException {
databaseReader.close();
}
}
@@ -0,0 +1,33 @@
package io.ib67.sfcraft.mixin.server;
import io.ib67.sfcraft.SFCraft;
import net.minecraft.network.ClientConnection;
import net.minecraft.network.packet.c2s.login.LoginHelloC2SPacket;
import net.minecraft.server.network.ServerLoginNetworkHandler;
import net.minecraft.text.Text;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(ServerLoginNetworkHandler.class)
public abstract class ServerLoginNetworkHandlerMixin {
@Shadow
public abstract void disconnect(Text reason);
private String currentPlayer;
@Inject(method = "onHello", at = @At("HEAD"))
public void onHello(LoginHelloC2SPacket packet, CallbackInfo ci) {
currentPlayer = packet.name();
}
@Redirect(method = "onHello", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/ClientConnection;isLocal()Z"))
public boolean isLocal(ClientConnection connection) {
return SFCraft.getListener().onPlayerLogin(currentPlayer, connection, this::disconnect);
}
}
+27
View File
@@ -0,0 +1,27 @@
{
"schemaVersion": 1,
"id": "sfcraft",
"version": "${version}",
"name": "sfcraft",
"description": "",
"authors": [],
"contact": {
"repo": "https://github.com/icebear67/sfcraft"
},
"license": "GPL-3.0",
"icon": "assets/sfcraft/icon.png",
"environment": "server",
"entrypoints": {
"main": [
"io.ib67.sfcraft.SFCraft"
]
},
"mixins": [
"sfcraft.mixins.json"
],
"depends": {
"fabricloader": ">=${loader_version}",
"fabric": "*",
"minecraft": "${minecraft_version}"
}
}
+16
View File
@@ -0,0 +1,16 @@
{
"required": true,
"minVersion": "0.8",
"package": "io.ib67.sfcraft.mixin",
"compatibilityLevel": "JAVA_21",
"mixins": [
],
"client": [
],
"server": [
"server.ServerLoginNetworkHandlerMixin"
],
"injectors": {
"defaultRequire": 1
}
}