tt
This commit is contained in:
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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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}"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user