Skip to content
This repository has been archived by the owner on Oct 12, 2024. It is now read-only.

Commit

Permalink
refactor: more signed chat R&D
Browse files Browse the repository at this point in the history
  • Loading branch information
WiIIiam278 committed Feb 21, 2024
1 parent aa15633 commit bf12d44
Show file tree
Hide file tree
Showing 9 changed files with 365 additions and 32 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ allprojects {
mavenCentral()
maven { url = 'https://repo.william278.net/velocity/' }
maven { url = 'https://repo.papermc.io/repository/maven-public/' }
maven { url = 'https://repo.codemc.io/repository/maven-releases/' }
maven { url = 'https://repo.william278.net/releases/' }
maven { url = 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' }
maven { url = 'https://repo.minebench.de/' }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ default void loadLocales() {
setLocales(store.load(path));
return;
}

String a = String.format("locales/%s.yml", getSettings().getLanguage());
// Otherwise, save and read the default locales
try (InputStream input = getResource(String.format("locales/%s.yml", getSettings().getLanguage()))) {
final Locales locales = store.read(input);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,12 @@

package net.william278.huskchat.config;

import de.exlll.configlib.Configuration;
import de.themoep.minedown.adventure.MineDown;
import de.themoep.minedown.adventure.MineDownParser;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.format.TextColor;
Expand All @@ -33,6 +37,10 @@

import java.util.*;

@SuppressWarnings("FieldMayBeFinal")
@Getter
@Configuration
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Locales {

static final String CONFIG_HEADER = """
Expand All @@ -46,7 +54,7 @@ public class Locales {

private static final String SILENT_JOIN_PERMISSION = "huskchat.silent_join";
private static final String SILENT_QUIT_PERMISSION = "huskchat.silent_quit";
static final String DEFAULT_LOCALE = "en";
static final String DEFAULT_LOCALE = "en-gb";

// The raw set of locales loaded from yaml
Map<String, String> locales = new TreeMap<>();
Expand Down
16 changes: 16 additions & 0 deletions velocity/build.gradle
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
plugins {
id 'xyz.jpenilla.run-velocity' version '2.2.2'
}

dependencies {
implementation project(path: ':common')

implementation 'org.bstats:bstats-velocity:3.0.2'
implementation 'com.github.retrooper.packetevents:velocity:2.2.0'

compileOnly "com.velocitypowered:velocity-api:${velocity_api_version}-SNAPSHOT"
compileOnly "com.velocitypowered:velocity-proxy:${velocity_api_version}-SNAPSHOT"

compileOnly 'io.netty:netty-codec-http:4.1.106.Final'
compileOnly 'it.unimi.dsi:fastutil:8.5.12'
compileOnly 'commons-io:commons-io:2.15.1'
compileOnly 'de.themoep:minedown-adventure:1.7.2-SNAPSHOT'
compileOnly 'org.jetbrains:annotations:24.1.0'
compileOnly 'org.projectlombok:lombok:1.18.30'
compileOnly 'net.kyori:adventure-nbt:4.15.0'

annotationProcessor 'org.projectlombok:lombok:1.18.30'
}
Expand All @@ -25,11 +33,19 @@ shadowJar {
relocate 'org.jetbrains', 'net.william278.huskchat.libraries'
relocate 'org.intellij', 'net.william278.huskchat.libraries'
relocate 'org.bstats', 'net.william278.huskchat.libraries.bstats'
relocate 'com.github.retrooper', 'net.william278.huskchat.libraries'
relocate 'io.github.retrooper', 'net.william278.huskchat.libraries'

dependencies {
//noinspection GroovyAssignabilityCheck
exclude dependency(':slf4j-api')
}

minimize()
}

tasks {
runVelocity {
velocityVersion("${velocity_api_version}-SNAPSHOT")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
import net.william278.huskchat.getter.DataGetter;
import net.william278.huskchat.getter.DefaultDataGetter;
import net.william278.huskchat.getter.LuckPermsDataGetter;
import net.william278.huskchat.listener.VelocityListener;
import net.william278.huskchat.packet.PacketManager;
import net.william278.huskchat.placeholders.DefaultReplacer;
import net.william278.huskchat.placeholders.PAPIProxyBridgeReplacer;
import net.william278.huskchat.placeholders.PlaceholderReplacer;
Expand Down Expand Up @@ -104,11 +104,6 @@ public VelocityHuskChat(@NotNull ProxyServer server, @NotNull org.slf4j.Logger l

@Subscribe
public void onProxyInitialization(@NotNull ProxyInitializeEvent event) {
// Check plugin compat
if (!isSigningPluginInstalled()) {
return;
}

// Load config and locale files
this.loadConfig();

Expand All @@ -129,7 +124,8 @@ public void onProxyInitialization(@NotNull ProxyInitializeEvent event) {
}

// Register events
getProxyServer().getEventManager().register(this, new VelocityListener(this));
// getProxyServer().getEventManager().register(this, new VelocityListener(this));
new PacketManager(this).load(); // todo wip

// Register commands & channel shortcuts
VelocityCommand.Type.registerAll(this);
Expand All @@ -146,27 +142,6 @@ public void onProxyInitialization(@NotNull ProxyInitializeEvent event) {
log(Level.INFO, "Enabled HuskChat version " + getVersion());
}

// Ensures a signing plugin is installed
private boolean isSigningPluginInstalled() {
boolean usvPresent = isPluginPresent("unsignedvelocity");
boolean svPresent = isPluginPresent("signedvelocity");
if (usvPresent && svPresent) {
log(Level.SEVERE, "Both UnsignedVelocity and SignedVelocity are present!\n" +
"Please uninstall UnsignedVelocity. HuskChat will now be disabled."
);
return false;
}
if (!(usvPresent || svPresent)) {
log(Level.WARNING, "Neither UnsignedVelocity nor SignedVelocity are present!\n" +
"Install SignedVelocity (https://modrinth.com/plugin/signedvelocity) for 1.19+ support.");
} else if (usvPresent) {
log(Level.WARNING, "UnsignedVelocity is deprecated; please install SignedVelocity " +
" (https://modrinth.com/plugin/signedvelocity) instead for better support.");
}
return true;
}


@Override
public Optional<DiscordHook> getDiscordHook() {
return Optional.ofNullable(discordHook);
Expand Down Expand Up @@ -236,7 +211,7 @@ public Optional<OnlineUser> findPlayer(@NotNull String username) {
@Nullable
@Override
public InputStream getResource(@NotNull String path) {
return HuskChat.class.getClassLoader().getResourceAsStream(path);
return getClass().getClassLoader().getResourceAsStream(path);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package net.william278.huskchat.packet;

import com.github.retrooper.packetevents.PacketEvents;
import io.github.retrooper.packetevents.velocity.factory.VelocityPacketEventsBuilder;
import net.william278.huskchat.VelocityHuskChat;
import org.jetbrains.annotations.NotNull;

public class PacketManager {

private final VelocityHuskChat plugin;

public PacketManager(@NotNull VelocityHuskChat plugin) {
this.plugin = plugin;
}

public void load() {
PacketEvents.setAPI(VelocityPacketEventsBuilder.build(plugin.getProxyServer(), plugin.getContainer()));
PacketEvents.getAPI().load();
PacketEvents.getAPI().getEventManager().registerListener(new PlayerPacketListener(plugin));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package net.william278.huskchat.packet;

import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.event.SimplePacketListenerAbstract;
import com.github.retrooper.packetevents.event.simple.PacketConfigSendEvent;
import com.github.retrooper.packetevents.event.simple.PacketPlayReceiveEvent;
import com.github.retrooper.packetevents.manager.player.PlayerManager;
import com.github.retrooper.packetevents.protocol.chat.ChatTypes;
import com.github.retrooper.packetevents.protocol.chat.LastSeenMessages;
import com.github.retrooper.packetevents.protocol.chat.MessageSignature;
import com.github.retrooper.packetevents.protocol.chat.filter.FilterMask;
import com.github.retrooper.packetevents.protocol.chat.message.ChatMessage_v1_19_1;
import com.github.retrooper.packetevents.protocol.chat.message.ChatMessage_v1_19_3;
import com.github.retrooper.packetevents.protocol.nbt.NBTCompound;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.github.retrooper.packetevents.protocol.player.User;
import com.github.retrooper.packetevents.util.crypto.MessageSignData;
import com.github.retrooper.packetevents.wrapper.configuration.server.WrapperConfigServerRegistryData;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientChatMessage;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerChatMessage;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import io.netty.buffer.ByteBuf;
import net.kyori.adventure.text.Component;
import net.william278.huskchat.VelocityHuskChat;
import org.jetbrains.annotations.NotNull;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Level;

public class PlayerPacketListener extends SimplePacketListenerAbstract {

private static final int MAX_SEEN = 20;

private final VelocityHuskChat plugin;
private final PlayerManager playerManager = PacketEvents.getAPI().getPlayerManager();
private final Map<UUID, List<LastMessage>> seenMessages = Maps.newConcurrentMap();
private final List<LastMessage> allMessages = Lists.newArrayList();

public PlayerPacketListener(@NotNull VelocityHuskChat plugin) {
this.plugin = plugin;
ChatTypes.define(RegistryEditor.HUSKCHAT.getName().asString());
}

@Override
public void onPacketPlayReceive(PacketPlayReceiveEvent event) {
if (event.getPacketType() == PacketType.Play.Client.CHAT_MESSAGE) {
try {
final WrapperPlayClientChatMessage packet = new WrapperPlayClientChatMessage(event);
this.sendSignedMessage(event.getUser(), packet);
event.setCancelled(true);
} catch (Throwable e) {
plugin.log(Level.SEVERE, "Failed to handle CHAT_MESSAGE packet", e);
}
} else if (event.getPacketType() == PacketType.Play.Client.CHAT_ACK) {
try {
final ByteBuf byteBuf = (ByteBuf) event.getByteBuf();
seenMessages.getOrDefault(event.getUser().getUUID(), Lists.newArrayList())
.add(allMessages.get(byteBuf.readInt()));
plugin.log(Level.INFO, "Received CHAT_ACK packet");
event.setCancelled(true);
} catch (Throwable e) {
plugin.log(Level.SEVERE, "Failed to handle CHAT_ACK packet", e);
}
}
}

@Override
public void onPacketConfigSend(PacketConfigSendEvent event) {
if (event.getPacketType() == PacketType.Configuration.Server.REGISTRY_DATA) {
try {
final WrapperConfigServerRegistryData wrapper = new WrapperConfigServerRegistryData(event);
wrapper.setRegistryData(injectChatTypes(wrapper.getRegistryData()));
event.markForReEncode(true);

// Initialize the user's message history
final UUID uuid = event.getUser().getUUID();
seenMessages.put(uuid, Lists.newArrayList());
} catch (Throwable e) {
plugin.log(Level.SEVERE, "Failed to handle SERVERBOUND_REGISTRY_SYNC packet", e);
}
}
}

@NotNull
private NBTCompound injectChatTypes(@NotNull NBTCompound data) {
final RegistryEditor wrapper = new RegistryEditor(data);
wrapper.injectTypes(List.of(RegistryEditor.HUSKCHAT));
return wrapper.root();
}

private void sendSignedMessage(@NotNull User sender, @NotNull WrapperPlayClientChatMessage packet) {
if (packet.getLastSeenMessages() == null) {
throw new IllegalStateException("LastSeenMessages is null");
}

(plugin.getPlayer(sender.getUUID())).get().sendMessage(Component.text(
"Offset: " + packet.getLastSeenMessages().getOffset() + " "
+ packet.getLastSeenMessages().getAcknowledged().toString()
));

int messagesSinceILastSentOne = packet.getLastSeenMessages().getOffset();
int messagesSinceILastSaw = packet.getLastSeenMessages().getAcknowledged().size();

// todo isAllowed
try {
plugin.log(Level.INFO, "Sending signed message 1");
final MessageSignData sign = packet.getMessageSignData().orElse(null);
if (sign == null) {
plugin.log(Level.WARNING, "Failed to send signed message (no sign data)");
return;
}
plugin.log(Level.INFO, "Sending signed message 2");

System.out.println("SENDING: " + UUID.nameUUIDFromBytes(sign.getSaltSignature().getSignature()).toString().split("-")[0]);

ChatMessage_v1_19_3 message = new ChatMessage_v1_19_3(
sender.getUUID(),
0,
sign.getSaltSignature().getSignature(),
packet.getMessage(),
sign.getTimestamp(),
sign.getSaltSignature().getSalt(),
null,
null,
FilterMask.PASS_THROUGH,
new ChatMessage_v1_19_1.ChatTypeBoundNetwork(
// ChatTypes.getByName(RegistryEditor.HUSKCHAT.getName().asString()),
ChatTypes.CHAT,
Component.text(sender.getName()), //todo chat message format goes here
null
)
);
plugin.log(Level.INFO, "Sending signed message 3");
final LastMessage thisMessage = new LastMessage(sender.getUUID(), sign.getSaltSignature().getSignature());
plugin.getProxyServer().getAllPlayers().forEach(receiver -> {
final List<LastMessage> seen = seenMessages.getOrDefault(receiver.getUniqueId(), Lists.newArrayList());
playerManager.sendPacket(receiver, getChatPacket(message, seen));
seen.add(thisMessage);
});
allMessages.add(0, thisMessage);

} catch (Throwable e) {
plugin.log(Level.SEVERE, "Failed to dispatch packet (unsupported client or server version)", e);
}
}

@NotNull
private WrapperPlayServerChatMessage getChatPacket(@NotNull ChatMessage_v1_19_3 message, @NotNull List<LastMessage> seen) {
message.setIndex((int) seen.stream().filter(s -> s.sender.equals(message.getSenderUUID())).count());
message.setLastSeenMessagesPacked(LastMessage.pack(allMessages, seen));
return new WrapperPlayServerChatMessage(message);
}

private record LastMessage(UUID sender, byte[] signature) {

// Pack the last MAX_SEEN messages into an Array of LastSeenMessages.Packed
@NotNull
private static LastSeenMessages.Packed pack(@NotNull List<LastMessage> all, @NotNull List<LastMessage> seen) {
final List<MessageSignature.Packed> packed = Lists.newArrayList();
for (int i = Math.min(all.size() - 1, MAX_SEEN - 1); i >= 0; i--) {
final LastMessage message = all.get(i);
if (packed.isEmpty() && !seen.contains(message)) {
continue;
}
packed.add(seen.contains(message)
? new MessageSignature.Packed(i)
: new MessageSignature.Packed(new MessageSignature(message.signature)));
System.out.println("SENT: " + UUID.nameUUIDFromBytes(message.signature).toString().split("-")[0]);
}
return new LastSeenMessages.Packed(packed);
}

@Override
public boolean equals(Object obj) {
if (obj instanceof LastMessage message && message.sender.equals(sender)) {
return Arrays.equals(message.signature, signature);
}
return false;
}

}

}
Loading

0 comments on commit bf12d44

Please sign in to comment.