added all the code.
This commit is contained in:
58
src/client/java/com/example/signleakshield/ExploitState.java
Normal file
58
src/client/java/com/example/signleakshield/ExploitState.java
Normal file
@@ -0,0 +1,58 @@
|
||||
package com.example.signleakshield;
|
||||
|
||||
import net.minecraft.text.Text;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class ExploitState {
|
||||
public static final Map<BlockPos, CapturedSignData> SIGNS = new ConcurrentHashMap<>();
|
||||
public static volatile ForcedOpenContext pendingForcedOpen;
|
||||
|
||||
private ExploitState() {
|
||||
}
|
||||
|
||||
public static void rememberForcedOpen(BlockPos pos, boolean front) {
|
||||
pendingForcedOpen = new ForcedOpenContext(pos.toImmutable(), front, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
public static void clearForcedOpen() {
|
||||
pendingForcedOpen = null;
|
||||
}
|
||||
|
||||
public record ForcedOpenContext(BlockPos pos, boolean front, long timeMs) {
|
||||
}
|
||||
|
||||
public static final class CapturedSignData {
|
||||
private final Text[] front;
|
||||
private final Text[] back;
|
||||
private final boolean suspicious;
|
||||
|
||||
public CapturedSignData(Text[] front, Text[] back, boolean suspicious) {
|
||||
this.front = copy(front);
|
||||
this.back = copy(back);
|
||||
this.suspicious = suspicious;
|
||||
}
|
||||
|
||||
public Text[] getFront() {
|
||||
return copy(front);
|
||||
}
|
||||
|
||||
public Text[] getBack() {
|
||||
return copy(back);
|
||||
}
|
||||
|
||||
public boolean isSuspicious() {
|
||||
return suspicious;
|
||||
}
|
||||
|
||||
private static Text[] copy(Text[] source) {
|
||||
Text[] out = new Text[4];
|
||||
for (int i = 0; i < 4; i++) {
|
||||
out[i] = i < source.length && source[i] != null ? source[i] : Text.empty();
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.example.signleakshield;
|
||||
|
||||
import net.fabricmc.api.ClientModInitializer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public final class SignLeakShieldClient implements ClientModInitializer {
|
||||
public static final String MOD_ID = "signleakshield";
|
||||
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
|
||||
|
||||
@Override
|
||||
public void onInitializeClient() {
|
||||
LOGGER.info("Sign Leak Shield initialized");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.example.signleakshield;
|
||||
|
||||
import net.minecraft.nbt.NbtCompound;
|
||||
import net.minecraft.nbt.NbtElement;
|
||||
import net.minecraft.nbt.NbtList;
|
||||
import net.minecraft.text.Text;
|
||||
|
||||
public final class SignTextExtractor {
|
||||
private SignTextExtractor() {
|
||||
}
|
||||
|
||||
public static ExploitState.CapturedSignData fromNbt(NbtCompound nbt) {
|
||||
Text[] front = readSide(nbt, "front_text");
|
||||
Text[] back = readSide(nbt, "back_text");
|
||||
boolean suspicious = containsSuspicious(front) || containsSuspicious(back);
|
||||
return new ExploitState.CapturedSignData(front, back, suspicious);
|
||||
}
|
||||
|
||||
private static boolean containsSuspicious(Text[] lines) {
|
||||
for (Text line : lines) {
|
||||
if (TextSanitizer.isSuspicious(line)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Text[] readSide(NbtCompound root, String sideKey) {
|
||||
Text[] lines = new Text[] { Text.empty(), Text.empty(), Text.empty(), Text.empty() };
|
||||
|
||||
if (!root.contains(sideKey, NbtElement.COMPOUND_TYPE)) {
|
||||
return lines;
|
||||
}
|
||||
|
||||
NbtCompound side = root.getCompound(sideKey);
|
||||
if (!side.contains("messages", NbtElement.LIST_TYPE)) {
|
||||
return lines;
|
||||
}
|
||||
|
||||
NbtList messages = side.getList("messages", NbtElement.STRING_TYPE);
|
||||
for (int i = 0; i < 4 && i < messages.size(); i++) {
|
||||
String raw = messages.getString(i);
|
||||
try {
|
||||
Text parsed = Text.Serialization.fromJson(raw, null);
|
||||
lines[i] = parsed != null ? parsed : Text.literal(raw);
|
||||
} catch (Throwable ignored) {
|
||||
lines[i] = Text.literal(raw);
|
||||
}
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.example.signleakshield;
|
||||
|
||||
import net.minecraft.text.KeybindTextContent;
|
||||
import net.minecraft.text.PlainTextContent;
|
||||
import net.minecraft.text.Text;
|
||||
import net.minecraft.text.TextContent;
|
||||
import net.minecraft.text.TranslatableTextContent;
|
||||
|
||||
public final class TextSanitizer {
|
||||
private TextSanitizer() {
|
||||
}
|
||||
|
||||
public static boolean isSuspicious(Text text) {
|
||||
if (text == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
TextContent content = text.getContent();
|
||||
|
||||
if (content instanceof TranslatableTextContent || content instanceof KeybindTextContent) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (Text sibling : text.getSiblings()) {
|
||||
if (isSuspicious(sibling)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static String sanitize(Text text) {
|
||||
StringBuilder out = new StringBuilder();
|
||||
append(text, out);
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
private static void append(Text text, StringBuilder out) {
|
||||
if (text == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
TextContent content = text.getContent();
|
||||
|
||||
if (content instanceof PlainTextContent plain) {
|
||||
out.append(plain.string());
|
||||
} else if (content instanceof TranslatableTextContent translatable) {
|
||||
String fallback = translatable.getFallback();
|
||||
if (fallback != null && !fallback.isEmpty()) {
|
||||
out.append(fallback);
|
||||
} else {
|
||||
out.append(translatable.getKey());
|
||||
}
|
||||
} else if (content instanceof KeybindTextContent keybind) {
|
||||
out.append(keybind.getKey());
|
||||
} else {
|
||||
out.append(text.getString());
|
||||
}
|
||||
|
||||
for (Text sibling : text.getSiblings()) {
|
||||
append(sibling, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.example.signleakshield.mixin;
|
||||
|
||||
import com.example.signleakshield.ExploitState;
|
||||
import com.example.signleakshield.TextSanitizer;
|
||||
import net.minecraft.network.ClientConnection;
|
||||
import net.minecraft.network.PacketCallbacks;
|
||||
import net.minecraft.network.packet.Packet;
|
||||
import net.minecraft.network.packet.c2s.play.UpdateSignC2SPacket;
|
||||
import net.minecraft.text.Text;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.ModifyVariable;
|
||||
|
||||
@Mixin(ClientConnection.class)
|
||||
public abstract class ClientConnectionMixin {
|
||||
@ModifyVariable(method = "send(Lnet/minecraft/network/packet/Packet;Lnet/minecraft/network/PacketCallbacks;Z)V", at = @At("HEAD"), argsOnly = true)
|
||||
private Packet<?> signleakshield$rewriteOutgoing(Packet<?> packet) {
|
||||
if (!(packet instanceof UpdateSignC2SPacket signPacket)) {
|
||||
return packet;
|
||||
}
|
||||
|
||||
ExploitState.ForcedOpenContext forcedOpen = ExploitState.pendingForcedOpen;
|
||||
if (forcedOpen == null) {
|
||||
return packet;
|
||||
}
|
||||
|
||||
BlockPos pos = signPacket.getPos();
|
||||
if (!forcedOpen.pos().equals(pos)) {
|
||||
return packet;
|
||||
}
|
||||
|
||||
if (forcedOpen.front() != signPacket.isFront()) {
|
||||
return packet;
|
||||
}
|
||||
|
||||
long ageMs = System.currentTimeMillis() - forcedOpen.timeMs();
|
||||
if (ageMs < 0L || ageMs > 5000L) {
|
||||
return packet;
|
||||
}
|
||||
|
||||
ExploitState.CapturedSignData captured = ExploitState.SIGNS.get(pos);
|
||||
if (captured == null || !captured.isSuspicious()) {
|
||||
return packet;
|
||||
}
|
||||
|
||||
Text[] lines = signPacket.isFront() ? captured.getFront() : captured.getBack();
|
||||
String line1 = TextSanitizer.sanitize(lines[0]);
|
||||
String line2 = TextSanitizer.sanitize(lines[1]);
|
||||
String line3 = TextSanitizer.sanitize(lines[2]);
|
||||
String line4 = TextSanitizer.sanitize(lines[3]);
|
||||
|
||||
ExploitState.clearForcedOpen();
|
||||
return new UpdateSignC2SPacket(pos, signPacket.isFront(), line1, line2, line3, line4);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.example.signleakshield.mixin;
|
||||
|
||||
import com.example.signleakshield.ExploitState;
|
||||
import com.example.signleakshield.SignTextExtractor;
|
||||
import net.minecraft.block.entity.BlockEntityType;
|
||||
import net.minecraft.client.network.ClientPlayNetworkHandler;
|
||||
import net.minecraft.network.packet.s2c.play.BlockEntityUpdateS2CPacket;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(ClientPlayNetworkHandler.class)
|
||||
public abstract class ClientPlayNetworkHandlerBlockEntityUpdateMixin {
|
||||
@Inject(method = "onBlockEntityUpdate", at = @At("HEAD"))
|
||||
private void signleakshield$captureSign(BlockEntityUpdateS2CPacket packet, CallbackInfo ci) {
|
||||
if (packet.getNbt() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (packet.getBlockEntityType() != BlockEntityType.SIGN && packet.getBlockEntityType() != BlockEntityType.HANGING_SIGN) {
|
||||
return;
|
||||
}
|
||||
|
||||
BlockPos pos = packet.getPos();
|
||||
ExploitState.SIGNS.put(pos.toImmutable(), SignTextExtractor.fromNbt(packet.getNbt()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.example.signleakshield.mixin;
|
||||
|
||||
import com.example.signleakshield.ExploitState;
|
||||
import net.minecraft.client.network.ClientPlayNetworkHandler;
|
||||
import net.minecraft.network.packet.s2c.play.SignEditorOpenS2CPacket;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(ClientPlayNetworkHandler.class)
|
||||
public abstract class ClientPlayNetworkHandlerSignEditorOpenMixin {
|
||||
@Inject(method = "onSignEditorOpen", at = @At("HEAD"))
|
||||
private void signleakshield$rememberForcedOpen(SignEditorOpenS2CPacket packet, CallbackInfo ci) {
|
||||
ExploitState.rememberForcedOpen(packet.getPos(), packet.isFront());
|
||||
}
|
||||
}
|
||||
30
src/main/resources/fabric.mod.json
Normal file
30
src/main/resources/fabric.mod.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "signleakshield",
|
||||
"version": "${version}",
|
||||
"name": "Sign Leak Shield",
|
||||
"description": "Client-side defensive patch for forced sign-editor translation/keybind leak probes.",
|
||||
"authors": [
|
||||
"Omni Systems"
|
||||
],
|
||||
"contact": {
|
||||
"email": "contact@d3lta.one",
|
||||
"sources": "https://github.com/OusmBlueNinja/SignShild"
|
||||
},
|
||||
"license": "MIT",
|
||||
"environment": "client",
|
||||
"entrypoints": {
|
||||
"client": [
|
||||
"com.example.signleakshield.SignLeakShieldClient"
|
||||
]
|
||||
},
|
||||
"mixins": [
|
||||
"signleakshield.mixins.json"
|
||||
],
|
||||
"depends": {
|
||||
"fabricloader": ">=0.18.4",
|
||||
"minecraft": "1.21.1",
|
||||
"java": ">=21",
|
||||
"fabric-api": "*"
|
||||
}
|
||||
}
|
||||
13
src/main/resources/signleakshield.mixins.json
Normal file
13
src/main/resources/signleakshield.mixins.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"required": true,
|
||||
"package": "com.example.signleakshield.mixin",
|
||||
"compatibilityLevel": "JAVA_21",
|
||||
"client": [
|
||||
"ClientConnectionMixin",
|
||||
"ClientPlayNetworkHandlerBlockEntityUpdateMixin",
|
||||
"ClientPlayNetworkHandlerSignEditorOpenMixin"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user