Merge branch 'master' into plugin-message-event

# Conflicts:
#	proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java
#	proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java
This commit is contained in:
Andrew Steinborn
2018-09-16 02:35:38 -04:00
33 changed files with 750 additions and 215 deletions

View File

@@ -14,8 +14,11 @@ compileTestJava {
jar {
manifest {
def buildNumber = System.getenv("BUILD_NUMBER") ?: "unknown"
def version = "${project.version} (git-${project.ext.getCurrentShortRevision()}, build ${buildNumber})"
attributes 'Main-Class': 'com.velocitypowered.proxy.Velocity'
attributes 'Implementation-Version': project.version
attributes 'Implementation-Version': version
}
}

View File

@@ -19,7 +19,7 @@ public class Velocity {
public static void main(String... args) {
startTime = System.currentTimeMillis();
logger.info("Booting up Velocity...");
logger.info("Booting up Velocity {}...", Velocity.class.getPackage().getImplementationVersion());
VelocityServer server = new VelocityServer();
server.start();

View File

@@ -29,7 +29,7 @@ public class VelocityCommand implements Command {
.append(TextComponent.of(" or the ").resetStyle())
.append(TextComponent.builder("Velocity GitHub")
.color(TextColor.GREEN)
.clickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://github.com/astei/velocity"))
.clickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://github.com/VelocityPowered/Velocity"))
.build())
.build();

View File

@@ -1,5 +1,6 @@
package com.velocitypowered.proxy.config;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableMap;
import com.moandjiezana.toml.Toml;
import com.velocitypowered.api.util.Favicon;
@@ -48,7 +49,7 @@ public class VelocityConfiguration extends AnnotatedConfig {
@Comment({
"Should we forward IP addresses and other data to backend servers?",
"Available options:",
"- \"none\": No forwarding will be done. All players will appear to be Should we forward IP addresses and other data to backend servers?connecting from the proxy",
"- \"none\": No forwarding will be done. All players will appear to be connecting from the proxy",
" and will have offline-mode UUIDs.",
"- \"legacy\": Forward player IPs and UUIDs in BungeeCord-compatible fashion. Use this if you run",
" servers using Minecraft 1.12 or lower.",
@@ -62,6 +63,10 @@ public class VelocityConfiguration extends AnnotatedConfig {
@ConfigKey("forwarding-secret")
private byte[] forwardingSecret = generateRandomString(12).getBytes(StandardCharsets.UTF_8);
@Comment("Announce whether or not your server supports Forge/FML. If you run a modded server, we suggest turning this on.")
@ConfigKey("announce-forge")
private boolean announceForge = false;
@Table("[servers]")
private final Servers servers;
@@ -83,12 +88,13 @@ public class VelocityConfiguration extends AnnotatedConfig {
}
private VelocityConfiguration(String bind, String motd, int showMaxPlayers, boolean onlineMode,
PlayerInfoForwarding playerInfoForwardingMode, byte[] forwardingSecret, Servers servers,
Advanced advanced, Query query) {
boolean announceForge, PlayerInfoForwarding playerInfoForwardingMode, byte[] forwardingSecret,
Servers servers, Advanced advanced, Query query) {
this.bind = bind;
this.motd = motd;
this.showMaxPlayers = showMaxPlayers;
this.onlineMode = onlineMode;
this.announceForge = announceForge;
this.playerInfoForwardingMode = playerInfoForwardingMode;
this.forwardingSecret = forwardingSecret;
this.servers = servers;
@@ -103,13 +109,13 @@ public class VelocityConfiguration extends AnnotatedConfig {
if (bind.isEmpty()) {
logger.error("'bind' option is empty.");
valid = false;
}
try {
AddressUtil.parseAddress(bind);
} catch (IllegalArgumentException e) {
logger.error("'bind' option does not specify a valid IP address.", e);
valid = false;
} else {
try {
AddressUtil.parseAddress(bind);
} catch (IllegalArgumentException e) {
logger.error("'bind' option does not specify a valid IP address.", e);
valid = false;
}
}
if (!onlineMode) {
@@ -118,11 +124,11 @@ public class VelocityConfiguration extends AnnotatedConfig {
switch (playerInfoForwardingMode) {
case NONE:
logger.info("Player info forwarding is disabled! All players will appear to be connecting from the proxy and will have offline-mode UUIDs.");
logger.warn("Player info forwarding is disabled! All players will appear to be connecting from the proxy and will have offline-mode UUIDs.");
break;
case MODERN:
if (forwardingSecret.length == 0) {
logger.error("You don't have a forwarding secret set.");
if (forwardingSecret == null || forwardingSecret.length == 0) {
logger.error("You don't have a forwarding secret set. This is required for security.");
valid = false;
}
break;
@@ -148,7 +154,7 @@ public class VelocityConfiguration extends AnnotatedConfig {
for (String s : servers.getAttemptConnectionOrder()) {
if (!servers.getServers().containsKey(s)) {
logger.error("Fallback server " + s + " doesn't exist!");
logger.error("Fallback server " + s + " is not registered in your configuration!");
valid = false;
}
}
@@ -165,18 +171,18 @@ public class VelocityConfiguration extends AnnotatedConfig {
logger.error("Invalid compression level {}", advanced.compressionLevel);
valid = false;
} else if (advanced.compressionLevel == 0) {
logger.warn("ALL packets going through the proxy are going to be uncompressed. This will increase bandwidth usage.");
logger.warn("ALL packets going through the proxy will be uncompressed. This will increase bandwidth usage.");
}
if (advanced.compressionThreshold < -1) {
logger.error("Invalid compression threshold {}", advanced.compressionLevel);
valid = false;
} else if (advanced.compressionThreshold == 0) {
logger.warn("ALL packets going through the proxy are going to be compressed. This may hurt performance.");
logger.warn("ALL packets going through the proxy will be compressed. This will compromise throughput and increase CPU usage!");
}
if (advanced.loginRatelimit < 0) {
logger.error("Invalid login ratelimit {}", advanced.loginRatelimit);
logger.error("Invalid login ratelimit {}ms", advanced.loginRatelimit);
valid = false;
}
@@ -217,7 +223,7 @@ public class VelocityConfiguration extends AnnotatedConfig {
if (motd.startsWith("{")) {
motdAsComponent = ComponentSerializers.JSON.deserialize(motd);
} else {
motdAsComponent = ComponentSerializers.LEGACY.deserialize(LegacyChatColorUtils.translate('&', motd));
motdAsComponent = ComponentSerializers.LEGACY.deserialize(motd, '&');
}
}
return motdAsComponent;
@@ -263,54 +269,34 @@ public class VelocityConfiguration extends AnnotatedConfig {
return favicon;
}
private void setBind(String bind) {
this.bind = bind;
public boolean isAnnounceForge() {
return announceForge;
}
private void setMotd(String motd) {
this.motd = motd;
public int getConnectTimeout() {
return advanced.getConnectionTimeout();
}
private void setShowMaxPlayers(int showMaxPlayers) {
this.showMaxPlayers = showMaxPlayers;
}
private void setOnlineMode(boolean onlineMode) {
this.onlineMode = onlineMode;
}
private void setPlayerInfoForwardingMode(PlayerInfoForwarding playerInfoForwardingMode) {
this.playerInfoForwardingMode = playerInfoForwardingMode;
}
private void setForwardingSecret(byte[] forwardingSecret) {
this.forwardingSecret = forwardingSecret;
}
private void setMotdAsComponent(Component motdAsComponent) {
this.motdAsComponent = motdAsComponent;
}
private void setFavicon(Favicon favicon) {
this.favicon = favicon;
public int getReadTimeout() {
return advanced.getReadTimeout();
}
@Override
public String toString() {
return "VelocityConfiguration{"
+ "bind='" + bind + '\''
+ ", motd='" + motd + '\''
+ ", showMaxPlayers=" + showMaxPlayers
+ ", onlineMode=" + onlineMode
+ ", playerInfoForwardingMode=" + playerInfoForwardingMode
+ ", forwardingSecret=" + ByteBufUtil.hexDump(forwardingSecret)
+ ", servers=" + servers
+ ", advanced=" + advanced
+ ", query=" + query
+ ", motdAsComponent=" + motdAsComponent
+ ", favicon=" + favicon
+ '}';
return MoreObjects.toStringHelper(this)
.add("configVersion", configVersion)
.add("bind", bind)
.add("motd", motd)
.add("showMaxPlayers", showMaxPlayers)
.add("onlineMode", onlineMode)
.add("playerInfoForwardingMode", playerInfoForwardingMode)
.add("forwardingSecret", forwardingSecret)
.add("announceForge", announceForge)
.add("servers", servers)
.add("advanced", advanced)
.add("query", query)
.add("favicon", favicon)
.toString();
}
public static VelocityConfiguration read(Path path) throws IOException {
@@ -335,6 +321,7 @@ public class VelocityConfiguration extends AnnotatedConfig {
toml.getString("motd", "&3A Velocity Server"),
toml.getLong("show-max-players", 500L).intValue(),
toml.getBoolean("online-mode", true),
toml.getBoolean("announce-forge", false),
PlayerInfoForwarding.valueOf(toml.getString("player-info-forwarding-mode", "MODERN").toUpperCase()),
forwardingSecret,
servers,
@@ -441,21 +428,23 @@ public class VelocityConfiguration extends AnnotatedConfig {
"Disable by setting to 0"})
@ConfigKey("login-ratelimit")
private int loginRatelimit = 3000;
@Comment({"Specify a custom timeout for connection timeouts here. The default is five seconds."})
@ConfigKey("connection-timeout")
private int connectionTimeout = 5000;
@Comment({"Specify a read timeout for connections here. The default is 30 seconds."})
@ConfigKey("read-timeout")
private int readTimeout = 30000;
private Advanced() {
}
private Advanced(int compressionThreshold, int compressionLevel, int loginRatelimit) {
this.compressionThreshold = compressionThreshold;
this.compressionLevel = compressionLevel;
this.loginRatelimit = loginRatelimit;
}
private Advanced(Toml toml) {
if (toml != null) {
this.compressionThreshold = toml.getLong("compression-threshold", 1024L).intValue();
this.compressionLevel = toml.getLong("compression-level", -1L).intValue();
this.loginRatelimit = toml.getLong("login-ratelimit", 3000L).intValue();
this.connectionTimeout = toml.getLong("connection-timeout", 5000L).intValue();
this.readTimeout = toml.getLong("read-timeout", 30000L).intValue();
}
}
@@ -463,33 +452,31 @@ public class VelocityConfiguration extends AnnotatedConfig {
return compressionThreshold;
}
public void setCompressionThreshold(int compressionThreshold) {
this.compressionThreshold = compressionThreshold;
}
public int getCompressionLevel() {
return compressionLevel;
}
public void setCompressionLevel(int compressionLevel) {
this.compressionLevel = compressionLevel;
}
public int getLoginRatelimit() {
return loginRatelimit;
}
public void setLoginRatelimit(int loginRatelimit) {
this.loginRatelimit = loginRatelimit;
public int getConnectionTimeout() {
return connectionTimeout;
}
public int getReadTimeout() {
return readTimeout;
}
@Override
public String toString() {
return "Advanced{"
+ "compressionThreshold=" + compressionThreshold
+ ", compressionLevel=" + compressionLevel
+ ", loginRatelimit=" + loginRatelimit
+ '}';
return "Advanced{" +
"compressionThreshold=" + compressionThreshold +
", compressionLevel=" + compressionLevel +
", loginRatelimit=" + loginRatelimit +
", connectionTimeout=" + connectionTimeout +
", readTimeout=" + readTimeout +
'}';
}
}
@@ -521,18 +508,10 @@ public class VelocityConfiguration extends AnnotatedConfig {
return queryEnabled;
}
public void setQueryEnabled(boolean queryEnabled) {
this.queryEnabled = queryEnabled;
}
public int getQueryPort() {
return queryPort;
}
public void setQueryPort(int queryPort) {
this.queryPort = queryPort;
}
@Override
public String toString() {
return "Query{"

View File

@@ -45,7 +45,9 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
private MinecraftSessionHandler sessionHandler;
private int protocolVersion;
private MinecraftConnectionAssociation association;
private boolean isLegacyForge;
private final VelocityServer server;
private boolean canSendLegacyFMLResetPacket = false;
public MinecraftConnection(Channel channel, VelocityServer server) {
this.channel = channel;
@@ -105,6 +107,13 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
}
}
@Override
public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
if (sessionHandler != null) {
sessionHandler.writabilityChanged();
}
}
public void write(Object msg) {
if (channel.isActive()) {
channel.writeAndFlush(msg, channel.voidPromise());
@@ -222,4 +231,20 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
public void setAssociation(MinecraftConnectionAssociation association) {
this.association = association;
}
public boolean isLegacyForge() {
return isLegacyForge;
}
public void setLegacyForge(boolean isForge) {
this.isLegacyForge = isForge;
}
public boolean canSendLegacyFMLResetPacket() {
return canSendLegacyFMLResetPacket;
}
public void setCanSendLegacyFMLResetPacket(boolean canSendLegacyFMLResetPacket) {
this.canSendLegacyFMLResetPacket = isLegacyForge && canSendLegacyFMLResetPacket;
}
}

View File

@@ -29,4 +29,8 @@ public interface MinecraftSessionHandler {
default void exception(Throwable throwable) {
}
default void writabilityChanged() {
}
}

View File

@@ -6,4 +6,10 @@ public class VelocityConstants {
}
public static final String VELOCITY_IP_FORWARDING_CHANNEL = "velocity:player_info";
public static final String FORGE_LEGACY_HANDSHAKE_CHANNEL = "FML|HS";
public static final String FORGE_LEGACY_CHANNEL = "FML";
public static final String FORGE_MULTIPART_LEGACY_CHANNEL = "FML|MP";
public static final byte[] FORGE_LEGACY_HANDSHAKE_RESET_DATA = new byte[] { -2, 0 };
}

View File

@@ -3,7 +3,9 @@ package com.velocitypowered.proxy.connection.backend;
import com.velocitypowered.api.event.connection.PluginMessageEvent;
import com.velocitypowered.api.event.player.ServerConnectedEvent;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.VelocityConstants;
import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler;
import com.velocitypowered.proxy.connection.util.ConnectionMessages;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.packet.*;
@@ -30,7 +32,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
if (!connection.getPlayer().isActive()) {
// Connection was left open accidentally. Close it so as to avoid "You logged in from another location"
// errors.
connection.getMinecraftConnection().close();
connection.disconnect();
return;
}
@@ -42,6 +44,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
connection.getPlayer().getConnection().write(packet);
} else if (packet instanceof Disconnect) {
Disconnect original = (Disconnect) packet;
connection.disconnect();
connection.getPlayer().handleConnectionException(connection.getServerInfo(), original);
} else if (packet instanceof JoinGame) {
playerHandler.handleBackendJoinGame((JoinGame) packet);
@@ -67,6 +70,20 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
return;
}
if (!connection.hasCompletedJoin() && pm.getChannel().equals(VelocityConstants.FORGE_LEGACY_HANDSHAKE_CHANNEL)) {
if (!connection.isLegacyForge()) {
connection.setLegacyForge(true);
// We must always reset the handshake before a modded connection is established if
// we haven't done so already.
connection.getPlayer().sendLegacyForgeHandshakeResetPacket();
}
// Always forward these messages during login
connection.getPlayer().getConnection().write(pm);
return;
}
PluginMessageEvent event = new PluginMessageEvent(connection, connection.getPlayer(), server.getChannelRegistrar().getFromId(pm.getChannel()),
pm.getData());
server.getEventManager().fire(event)
@@ -86,10 +103,13 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
if (!connection.getPlayer().isActive()) {
// Connection was left open accidentally. Close it so as to avoid "You logged in from another location"
// errors.
connection.getMinecraftConnection().close();
connection.disconnect();
return;
}
connection.getPlayer().getConnection().write(buf.retain());
if (connection.hasCompletedJoin()) {
connection.getPlayer().getConnection().write(buf.retain());
}
}
@Override
@@ -97,16 +117,29 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
connection.getPlayer().handleConnectionException(connection.getServerInfo(), throwable);
}
public VelocityServer getServer() {
return server;
}
@Override
public void disconnected() {
if (connection.isGracefulDisconnect()) {
return;
}
connection.getPlayer().handleConnectionException(connection.getServerInfo(), Disconnect.create(ConnectionMessages.UNEXPECTED_DISCONNECT));
}
private boolean canForwardPluginMessage(PluginMessage message) {
ClientPlaySessionHandler playerHandler =
(ClientPlaySessionHandler) connection.getPlayer().getConnection().getSessionHandler();
boolean isMCMessage;
boolean isMCOrFMLMessage;
if (connection.getMinecraftConnection().getProtocolVersion() <= ProtocolConstants.MINECRAFT_1_12_2) {
isMCMessage = message.getChannel().startsWith("MC|");
String channel = message.getChannel();
isMCOrFMLMessage = channel.startsWith("MC|") || channel.startsWith(VelocityConstants.FORGE_LEGACY_HANDSHAKE_CHANNEL);
} else {
isMCMessage = message.getChannel().startsWith("minecraft:");
isMCOrFMLMessage = message.getChannel().startsWith("minecraft:");
}
return isMCMessage || playerHandler.getClientPluginMsgChannels().contains(message.getChannel()) ||
return isMCOrFMLMessage || playerHandler.getClientPluginMsgChannels().contains(message.getChannel()) ||
server.getChannelRegistrar().registered(message.getChannel());
}
}

View File

@@ -84,6 +84,10 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
connection.getPlayer().getConnection().setSessionHandler(new ClientPlaySessionHandler(server, connection.getPlayer()));
} else {
// The previous server connection should become obsolete.
// Before we remove it, if the server we are departing is modded, we must always reset the client state.
if (existingConnection.isLegacyForge()) {
connection.getPlayer().sendLegacyForgeHandshakeResetPacket();
}
existingConnection.disconnect();
}
@@ -119,7 +123,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
}
}
static ByteBuf createForwardingData(byte[] hmacSecret, String address, GameProfile profile) {
private static ByteBuf createForwardingData(byte[] hmacSecret, String address, GameProfile profile) {
ByteBuf dataToForward = Unpooled.buffer();
ByteBuf finalData = Unpooled.buffer();
try {

View File

@@ -33,7 +33,6 @@ import static com.velocitypowered.proxy.network.Connections.HANDLER;
import static com.velocitypowered.proxy.network.Connections.MINECRAFT_DECODER;
import static com.velocitypowered.proxy.network.Connections.MINECRAFT_ENCODER;
import static com.velocitypowered.proxy.network.Connections.READ_TIMEOUT;
import static com.velocitypowered.proxy.network.Connections.SERVER_READ_TIMEOUT_SECONDS;
public class VelocityServerConnection implements MinecraftConnectionAssociation, ServerConnection {
static final AttributeKey<CompletableFuture<ConnectionRequestBuilder.Result>> CONNECTION_NOTIFIER =
@@ -43,6 +42,9 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
private final ConnectedPlayer proxyPlayer;
private final VelocityServer server;
private MinecraftConnection minecraftConnection;
private boolean legacyForge = false;
private boolean hasCompletedJoin = false;
private boolean gracefulDisconnect = false;
public VelocityServerConnection(ServerInfo target, ConnectedPlayer proxyPlayer, VelocityServer server) {
this.serverInfo = target;
@@ -57,7 +59,7 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline()
.addLast(READ_TIMEOUT, new ReadTimeoutHandler(SERVER_READ_TIMEOUT_SECONDS, TimeUnit.SECONDS))
.addLast(READ_TIMEOUT, new ReadTimeoutHandler(server.getConfiguration().getReadTimeout(), TimeUnit.SECONDS))
.addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder())
.addLast(FRAME_ENCODER, MinecraftVarintLengthEncoder.INSTANCE)
.addLast(MINECRAFT_DECODER, new MinecraftDecoder(ProtocolConstants.Direction.CLIENTBOUND))
@@ -107,6 +109,8 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
handshake.setProtocolVersion(proxyPlayer.getConnection().getProtocolVersion());
if (forwardingMode == PlayerInfoForwarding.LEGACY) {
handshake.setServerAddress(createBungeeForwardingAddress());
} else if (proxyPlayer.getConnection().isLegacyForge()) {
handshake.setServerAddress(handshake.getServerAddress() + "\0FML\0");
} else {
handshake.setServerAddress(serverInfo.getAddress().getHostString());
}
@@ -122,6 +126,12 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
minecraftConnection.write(login);
}
public void writeIfJoined(PluginMessage message) {
if (hasCompletedJoin) {
minecraftConnection.write(message);
}
}
public MinecraftConnection getMinecraftConnection() {
return minecraftConnection;
}
@@ -136,8 +146,11 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
}
public void disconnect() {
minecraftConnection.close();
minecraftConnection = null;
if (minecraftConnection != null) {
minecraftConnection.close();
minecraftConnection = null;
gracefulDisconnect = true;
}
}
@Override
@@ -154,4 +167,24 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
message.setData(data);
minecraftConnection.write(message);
}
public boolean isLegacyForge() {
return legacyForge;
}
public void setLegacyForge(boolean modded) {
legacyForge = modded;
}
public boolean hasCompletedJoin() {
return hasCompletedJoin;
}
public void setHasCompletedJoin(boolean hasCompletedJoin) {
this.hasCompletedJoin = hasCompletedJoin;
}
public boolean isGracefulDisconnect() {
return gracefulDisconnect;
}
}

View File

@@ -3,6 +3,8 @@ package com.velocitypowered.proxy.connection.client;
import com.velocitypowered.api.event.connection.DisconnectEvent;
import com.velocitypowered.api.event.connection.PluginMessageEvent;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.VelocityConstants;
import com.velocitypowered.proxy.connection.backend.VelocityServerConnection;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.packet.*;
@@ -23,7 +25,7 @@ import java.util.*;
*/
public class ClientPlaySessionHandler implements MinecraftSessionHandler {
private static final Logger logger = LogManager.getLogger(ClientPlaySessionHandler.class);
private static final int MAX_PLUGIN_CHANNELS = 128;
private static final int MAX_PLUGIN_CHANNELS = 1024;
private final ConnectedPlayer player;
private long lastPingID = -1;
@@ -31,6 +33,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
private boolean spawned = false;
private final List<UUID> serverBossBars = new ArrayList<>();
private final Set<String> clientPluginMsgChannels = new HashSet<>();
private final Queue<PluginMessage> loginPluginMessages = new ArrayDeque<>();
private final VelocityServer server;
public ClientPlaySessionHandler(VelocityServer server, ConnectedPlayer player) {
@@ -51,6 +54,12 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
@Override
public void handle(MinecraftPacket packet) {
VelocityServerConnection serverConnection = player.getConnectedServer();
if (serverConnection == null) {
// No server connection yet, probably transitioning.
return;
}
if (packet instanceof KeepAlive) {
KeepAlive keepAlive = (KeepAlive) packet;
if (keepAlive.getRandomId() != lastPingID) {
@@ -60,6 +69,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
}
player.setPing(System.currentTimeMillis() - lastPingSent);
resetPingData();
serverConnection.getMinecraftConnection().write(packet);
return;
}
if (packet instanceof ClientSettings) {
@@ -106,7 +117,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
player.getConnection().write(response);
} else {
player.getConnectedServer().getMinecraftConnection().write(packet);
serverConnection.getMinecraftConnection().write(packet);
}
} catch (Exception e) {
logger.error("Unable to provide tab list completions for " + player.getUsername() + " for command '" + req.getCommand() + "'", e);
@@ -121,12 +132,22 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
}
// If we don't want to handle this packet, just forward it on.
player.getConnectedServer().getMinecraftConnection().write(packet);
if (serverConnection.hasCompletedJoin()) {
serverConnection.getMinecraftConnection().write(packet);
}
}
@Override
public void handleUnknown(ByteBuf buf) {
player.getConnectedServer().getMinecraftConnection().write(buf.retain());
VelocityServerConnection serverConnection = player.getConnectedServer();
if (serverConnection == null) {
// No server connection yet, probably transitioning.
return;
}
if (serverConnection.hasCompletedJoin()) {
serverConnection.getMinecraftConnection().write(buf.retain());
}
}
@Override
@@ -144,12 +165,31 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
.build());
}
@Override
public void writabilityChanged() {
VelocityServerConnection server = player.getConnectedServer();
if (server != null) {
boolean writable = player.getConnection().getChannel().isWritable();
server.getMinecraftConnection().getChannel().config().setAutoRead(writable);
}
}
public void handleBackendJoinGame(JoinGame joinGame) {
resetPingData(); // reset ping data;
resetPingData(); // reset ping data
if (!spawned) {
// nothing special to do here
// Nothing special to do with regards to spawning the player
spawned = true;
player.getConnection().delayedWrite(joinGame);
// We have something special to do for legacy Forge servers - during first connection the FML handshake
// will transition to complete regardless. Thus, we need to ensure that a reset packet is ALWAYS sent on
// first switch.
//
// As we know that calling this branch only happens on first join, we set that if we are a Forge
// client that we must reset on the next switch.
//
// The call will handle if the player is not a Forge player appropriately.
player.getConnection().setCanSendLegacyFMLResetPacket(true);
} else {
// Ah, this is the meat and potatoes of the whole venture!
//
@@ -193,17 +233,38 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
channel, toRegister));
}
// If we had plugin messages queued during login/FML handshake, send them now.
PluginMessage pm;
while ((pm = loginPluginMessages.poll()) != null) {
player.getConnectedServer().getMinecraftConnection().delayedWrite(pm);
}
// Flush everything
player.getConnection().flush();
player.getConnectedServer().getMinecraftConnection().flush();
player.getConnectedServer().setHasCompletedJoin(true);
if (player.getConnectedServer().isLegacyForge()) {
// We only need to indicate we can send a reset packet if we complete a handshake, that is,
// logged onto a Forge server.
//
// The special case is if we log onto a Vanilla server as our first server, FML will treat this
// as complete and **will** need a reset packet sending at some point. We will handle this
// during initial player connection if the player is detected to be forge.
//
// This is why we use an if statement rather than the result of VelocityServerConnection#isLegacyForge()
// because we don't want to set it false if this is a first connection to a Vanilla server.
//
// See LoginSessionHandler#handle for where the counterpart to this method is
player.getConnection().setCanSendLegacyFMLResetPacket(true);
}
}
public List<UUID> getServerBossBars() {
return serverBossBars;
}
public void handleClientPluginMessage(PluginMessage packet) {
if (packet.getChannel().equals("REGISTER") || packet.getChannel().equals("minecraft:register")) {
private void handleClientPluginMessage(PluginMessage packet) {
if (PluginMessageUtil.isMCRegister(packet)) {
List<String> actuallyRegistered = new ArrayList<>();
List<String> channels = PluginMessageUtil.getChannels(packet);
for (String channel : channels) {
@@ -220,28 +281,32 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
PluginMessage newRegisterPacket = PluginMessageUtil.constructChannelsPacket(packet.getChannel(), actuallyRegistered);
player.getConnectedServer().getMinecraftConnection().write(newRegisterPacket);
}
return;
}
if (packet.getChannel().equals("UNREGISTER") || packet.getChannel().equals("minecraft:unregister")) {
} else if (PluginMessageUtil.isMCUnregister(packet)) {
List<String> channels = PluginMessageUtil.getChannels(packet);
clientPluginMsgChannels.removeAll(channels);
}
if (PluginMessageUtil.isMCBrand(packet)) {
player.getConnectedServer().getMinecraftConnection().write(packet);
} else if (PluginMessageUtil.isMCBrand(packet)) {
player.getConnectedServer().getMinecraftConnection().write(PluginMessageUtil.rewriteMCBrand(packet));
return;
} else if (player.getConnectedServer().isLegacyForge() && !player.getConnectedServer().hasCompletedJoin()) {
if (packet.getChannel().equals(VelocityConstants.FORGE_LEGACY_HANDSHAKE_CHANNEL)) {
// Always forward the FML handshake to the remote server.
player.getConnectedServer().getMinecraftConnection().write(packet);
} else {
// The client is trying to send messages too early. This is primarily caused by mods, but it's further
// aggravated by Velocity. To work around these issues, we will queue any non-FML handshake messages to
// be sent once the JoinGame packet has been received by the proxy.
loginPluginMessages.add(packet);
}
} else {
PluginMessageEvent event = new PluginMessageEvent(player, player.getConnectedServer(),
server.getChannelRegistrar().getFromId(packet.getChannel()), packet.getData());
server.getEventManager().fire(event)
.thenAcceptAsync(pme -> {
if (pme.getResult().isAllowed()) {
player.getConnectedServer().getMinecraftConnection().write(packet);
}
}, player.getConnectedServer().getMinecraftConnection().getChannel().eventLoop());
}
PluginMessageEvent event = new PluginMessageEvent(player, player.getConnectedServer(),
server.getChannelRegistrar().getFromId(packet.getChannel()), packet.getData());
server.getEventManager().fire(event)
.thenAcceptAsync(pme -> {
if (pme.getResult().isAllowed()) {
player.getConnectedServer().getMinecraftConnection().write(packet);
}
}, player.getConnectedServer().getMinecraftConnection().getChannel().eventLoop());
}
public Set<String> getClientPluginMsgChannels() {

View File

@@ -12,7 +12,7 @@ public class ClientSettingsWrapper implements PlayerSettings {
private final SkinParts parts;
private Locale locale = null;
public ClientSettingsWrapper(ClientSettings settings) {
ClientSettingsWrapper(ClientSettings settings) {
this.settings = settings;
this.parts = new SkinParts((byte) settings.getSkinParts());
}

View File

@@ -2,6 +2,7 @@ package com.velocitypowered.proxy.connection.client;
import com.google.common.base.Preconditions;
import com.google.gson.JsonObject;
import com.velocitypowered.api.event.player.KickedFromServerEvent;
import com.velocitypowered.api.event.player.PlayerSettingsChangedEvent;
import com.velocitypowered.api.event.player.ServerPreConnectEvent;
import com.velocitypowered.api.permission.PermissionFunction;
@@ -14,6 +15,7 @@ import com.velocitypowered.api.util.MessagePosition;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation;
import com.velocitypowered.proxy.connection.VelocityConstants;
import com.velocitypowered.proxy.connection.util.ConnectionMessages;
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults;
import com.velocitypowered.api.util.GameProfile;
@@ -36,6 +38,7 @@ import net.kyori.text.serializer.PlainComponentSerializer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.net.InetSocketAddress;
import java.util.List;
@@ -190,7 +193,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
logger.error("{}: unable to connect to server {}", this, info.getName(), throwable);
userMessage = "Exception connecting to server " + info.getName();
}
handleConnectionException(info, TextComponent.builder()
handleConnectionException(info, null, TextComponent.builder()
.content(userMessage + ": ")
.color(TextColor.RED)
.append(TextComponent.of(error, TextColor.WHITE))
@@ -202,17 +205,23 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
String plainTextReason = PASS_THRU_TRANSLATE.serialize(disconnectReason);
if (connectedServer != null && connectedServer.getServerInfo().equals(info)) {
logger.error("{}: kicked from server {}: {}", this, info.getName(), plainTextReason);
handleConnectionException(info, disconnectReason, TextComponent.builder()
.content("Kicked from " + info.getName() + ": ")
.color(TextColor.RED)
.append(disconnectReason)
.build());
} else {
logger.error("{}: disconnected while connecting to {}: {}", this, info.getName(), plainTextReason);
handleConnectionException(info, disconnectReason, TextComponent.builder()
.content("Unable to connect to " + info.getName() + ": ")
.color(TextColor.RED)
.append(disconnectReason)
.build());
}
handleConnectionException(info, TextComponent.builder()
.content("Unable to connect to " + info.getName() + ": ")
.color(TextColor.RED)
.append(disconnectReason)
.build());
}
public void handleConnectionException(ServerInfo info, Component disconnectReason) {
private void handleConnectionException(ServerInfo info, @Nullable Component kickReason, Component friendlyReason) {
boolean alreadyConnected = connectedServer != null && connectedServer.getServerInfo().equals(info);;
connectionInFlight = null;
if (connectedServer == null) {
// The player isn't yet connected to a server.
@@ -220,14 +229,29 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
if (nextServer.isPresent()) {
createConnectionRequest(nextServer.get()).fireAndForget();
} else {
connection.closeWith(Disconnect.create(disconnectReason));
connection.closeWith(Disconnect.create(friendlyReason));
}
} else if (connectedServer.getServerInfo().equals(info)) {
// Already connected to the server being disconnected from.
// TODO: ServerKickEvent
connection.closeWith(Disconnect.create(disconnectReason));
if (kickReason != null) {
server.getEventManager().fire(new KickedFromServerEvent(this, info, kickReason, !alreadyConnected, friendlyReason))
.thenAcceptAsync(event -> {
if (event.getResult() instanceof KickedFromServerEvent.DisconnectPlayer) {
KickedFromServerEvent.DisconnectPlayer res = (KickedFromServerEvent.DisconnectPlayer) event.getResult();
connection.closeWith(Disconnect.create(res.getReason()));
} else if (event.getResult() instanceof KickedFromServerEvent.RedirectPlayer) {
KickedFromServerEvent.RedirectPlayer res = (KickedFromServerEvent.RedirectPlayer) event.getResult();
createConnectionRequest(res.getServer()).fireAndForget();
} else {
// In case someone gets creative, assume we want to disconnect the player.
connection.closeWith(Disconnect.create(friendlyReason));
}
}, connection.getChannel().eventLoop());
} else {
connection.closeWith(Disconnect.create(friendlyReason));
}
} else {
connection.write(Chat.create(disconnectReason));
connection.write(Chat.create(friendlyReason));
}
}
@@ -256,7 +280,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
}
// Otherwise, initiate the connection.
ServerPreConnectEvent event = new ServerPreConnectEvent(this, ServerPreConnectEvent.ServerResult.allowed(request.getServer()));
ServerPreConnectEvent event = new ServerPreConnectEvent(this, request.getServer());
return server.getEventManager().fire(event)
.thenCompose((newEvent) -> {
if (!newEvent.getResult().isAllowed()) {
@@ -265,7 +289,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
);
}
return new VelocityServerConnection(newEvent.getResult().getInfo().get(), this, server).connect();
return new VelocityServerConnection(newEvent.getResult().getServer().get(), this, server).connect();
});
}
@@ -276,6 +300,16 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
this.connectedServer = serverConnection;
}
public void sendLegacyForgeHandshakeResetPacket() {
if (connection.canSendLegacyFMLResetPacket()) {
PluginMessage resetPacket = new PluginMessage();
resetPacket.setChannel(VelocityConstants.FORGE_LEGACY_HANDSHAKE_CHANNEL);
resetPacket.setData(VelocityConstants.FORGE_LEGACY_HANDSHAKE_RESET_DATA);
connection.write(resetPacket);
connection.setCanSendLegacyFMLResetPacket(false);
}
}
public void close(TextComponent reason) {
connection.closeWith(Disconnect.create(reason));
}

View File

@@ -70,9 +70,12 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
return;
}
// Make sure legacy forwarding is not in use on this connection. Make sure that we do _not_ reject Forge,
// although Velocity does not yet support Forge.
if (handshake.getServerAddress().contains("\0") && !handshake.getServerAddress().endsWith("\0FML\0")) {
// Determine if we're using Forge (1.8 to 1.12, may not be the case in 1.13) and store that in the connection
boolean isForge = handshake.getServerAddress().endsWith("\0FML\0");
connection.setLegacyForge(isForge);
// Make sure legacy forwarding is not in use on this connection. Make sure that we do _not_ reject Forge
if (handshake.getServerAddress().contains("\0") && !isForge) {
connection.closeWith(Disconnect.create(TextComponent.of("Running Velocity behind Velocity is unsupported.")));
return;
}
@@ -105,6 +108,7 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
new ServerPing.Version(ProtocolConstants.MAXIMUM_GENERIC_VERSION, "Velocity " + ProtocolConstants.SUPPORTED_GENERIC_VERSION_STRING),
new ServerPing.Players(server.getPlayerCount(), configuration.getShowMaxPlayers(), ImmutableList.of()),
configuration.getMotdComponent(),
null,
null
);
ProxyPingEvent event = new ProxyPingEvent(new LegacyInboundConnection(connection), ping);

View File

@@ -2,12 +2,14 @@ package com.velocitypowered.proxy.connection.client;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.event.connection.LoginEvent;
import com.velocitypowered.api.event.connection.PostLoginEvent;
import com.velocitypowered.api.event.connection.PreLoginEvent;
import com.velocitypowered.api.event.connection.PreLoginEvent.PreLoginComponentResult;
import com.velocitypowered.api.event.permission.PermissionsSetupEvent;
import com.velocitypowered.api.event.player.GameProfileRequestEvent;
import com.velocitypowered.api.proxy.InboundConnection;
import com.velocitypowered.api.proxy.server.ServerInfo;
import com.velocitypowered.proxy.config.PlayerInfoForwarding;
import com.velocitypowered.proxy.connection.VelocityConstants;
import com.velocitypowered.api.util.GameProfile;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
@@ -31,11 +33,14 @@ import java.net.MalformedURLException;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ThreadLocalRandom;
public class LoginSessionHandler implements MinecraftSessionHandler {
private static final Logger logger = LogManager.getLogger(LoginSessionHandler.class);
private static final String MOJANG_SERVER_AUTH_URL =
"https://sessionserver.mojang.com/session/minecraft/hasJoined?username=%s&serverId=%s&ip=%s";
@@ -154,7 +159,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
return;
}
if (server.getConfiguration().isOnlineMode() || result.isOnlineModeAllowed()) {
if (!result.isForceOfflineMode() && (server.getConfiguration().isOnlineMode() || result.isOnlineModeAllowed())) {
// Request encryption.
EncryptionRequest request = generateRequest();
this.verify = Arrays.copyOf(request.getVerifyToken(), 4);
@@ -176,6 +181,12 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
}
private void initializePlayer(GameProfile profile, boolean onlineMode) {
if (inbound.isLegacyForge() && server.getConfiguration().getPlayerInfoForwardingMode() == PlayerInfoForwarding.LEGACY) {
// We want to add the FML token to the properties
List<GameProfile.Property> properties = new ArrayList<>(profile.getProperties());
properties.add(new GameProfile.Property("forgeClient", "true", ""));
profile = new GameProfile(profile.getId(), profile.getName(), properties);
}
GameProfileRequestEvent profileRequestEvent = new GameProfileRequestEvent(apiInbound, profile, onlineMode);
server.getEventManager().fire(profileRequestEvent).thenCompose(profileEvent -> {
@@ -235,7 +246,9 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
logger.info("{} has connected", player);
inbound.setSessionHandler(new InitialConnectSessionHandler(player));
player.createConnectionRequest(toTry.get()).fireAndForget();
server.getEventManager().fire(new PostLoginEvent(player)).thenRun(() -> {
player.createConnectionRequest(toTry.get()).fireAndForget();
});
}
@Override

View File

@@ -48,7 +48,8 @@ public class StatusSessionHandler implements MinecraftSessionHandler {
new ServerPing.Version(shownVersion, "Velocity " + ProtocolConstants.SUPPORTED_GENERIC_VERSION_STRING),
new ServerPing.Players(server.getPlayerCount(), configuration.getShowMaxPlayers(), ImmutableList.of()),
configuration.getMotdComponent(),
configuration.getFavicon()
configuration.getFavicon(),
configuration.isAnnounceForge() ? ServerPing.Modinfo.DEFAULT : null
);
ProxyPingEvent event = new ProxyPingEvent(inboundWrapper, initialPing);

View File

@@ -7,6 +7,7 @@ public class ConnectionMessages {
public static final TextComponent ALREADY_CONNECTED = TextComponent.of("You are already connected to this server!", TextColor.RED);
public static final TextComponent IN_PROGRESS = TextComponent.of("You are already connecting to a server!", TextColor.RED);
public static final TextComponent INTERNAL_SERVER_CONNECTION_ERROR = TextComponent.of("Internal server connection error");
public static final TextComponent UNEXPECTED_DISCONNECT = TextComponent.of("Unexpectedly disconnected from server - crash?");
private ConnectionMessages() {
throw new AssertionError();

View File

@@ -16,11 +16,7 @@ import com.velocitypowered.proxy.protocol.netty.MinecraftVarintFrameDecoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftVarintLengthEncoder;
import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.*;
import io.netty.channel.epoll.Epoll;
import io.netty.channel.epoll.EpollDatagramChannel;
import io.netty.channel.epoll.EpollEventLoopGroup;
@@ -48,6 +44,8 @@ import java.util.concurrent.TimeUnit;
import static com.velocitypowered.proxy.network.Connections.*;
public final class ConnectionManager {
private static final WriteBufferWaterMark SERVER_WRITE_MARK = new WriteBufferWaterMark(1 << 16, 1 << 18);
private static final Logger logger = LogManager.getLogger(ConnectionManager.class);
private final Set<Channel> endpoints = new HashSet<>();
@@ -72,11 +70,12 @@ public final class ConnectionManager {
final ServerBootstrap bootstrap = new ServerBootstrap()
.channel(this.transportType.serverSocketChannelClass)
.group(this.bossGroup, this.workerGroup)
.childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, SERVER_WRITE_MARK)
.childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(final Channel ch) {
ch.pipeline()
.addLast(READ_TIMEOUT, new ReadTimeoutHandler(CLIENT_READ_TIMEOUT_SECONDS, TimeUnit.SECONDS))
.addLast(READ_TIMEOUT, new ReadTimeoutHandler(server.getConfiguration().getReadTimeout(), TimeUnit.SECONDS))
.addLast(LEGACY_PING_DECODER, new LegacyPingDecoder())
.addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder())
.addLast(LEGACY_PING_ENCODER, LegacyPingEncoder.INSTANCE)
@@ -126,7 +125,9 @@ public final class ConnectionManager {
public Bootstrap createWorker() {
return new Bootstrap()
.channel(this.transportType.socketChannelClass)
.group(this.workerGroup);
.group(this.workerGroup)
.option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, server.getConfiguration().getConnectTimeout());
}
public void shutdown() {

View File

@@ -13,7 +13,4 @@ public interface Connections {
String MINECRAFT_DECODER = "minecraft-decoder";
String MINECRAFT_ENCODER = "minecraft-encoder";
String READ_TIMEOUT = "read-timeout";
int CLIENT_READ_TIMEOUT_SECONDS = 30; // client -> proxy
int SERVER_READ_TIMEOUT_SECONDS = 30; // proxy -> server
}

View File

@@ -1,5 +1,6 @@
package com.velocitypowered.proxy.protocol;
import com.google.common.base.Strings;
import com.google.common.primitives.ImmutableIntArray;
import com.velocitypowered.proxy.protocol.packet.*;
import io.netty.util.collection.IntObjectHashMap;
@@ -258,9 +259,18 @@ public enum StateRegistry {
@Override
public String toString() {
StringBuilder mappingAsString = new StringBuilder("{");
for (Object2IntMap.Entry<Class<? extends MinecraftPacket>> entry : packetClassToId.object2IntEntrySet()) {
mappingAsString.append(entry.getKey().getSimpleName()).append(" -> ")
.append("0x")
.append(Strings.padStart(Integer.toHexString(entry.getIntValue()), 2, '0'))
.append(", ");
}
mappingAsString.setLength(mappingAsString.length() - 2);
mappingAsString.append("}");
return "ProtocolVersion{" +
"id=" + id +
", packetClassToId=" + packetClassToId +
", packetClassToId=" + mappingAsString.toString() +
'}';
}
}

View File

@@ -38,11 +38,11 @@ public class MinecraftDecoder extends MessageToMessageDecoder<ByteBuf> {
packet.decode(msg, direction, protocolVersion.id);
} catch (Exception e) {
throw new CorruptedFrameException("Error decoding " + packet.getClass() + " Direction " + direction
+ " Protocol " + protocolVersion + " State " + state + " ID " + Integer.toHexString(packetId), e);
+ " Protocol " + protocolVersion.id + " State " + state + " ID " + Integer.toHexString(packetId), e);
}
if (msg.isReadable()) {
throw new CorruptedFrameException("Did not read full packet for " + packet.getClass() + " Direction " + direction
+ " Protocol " + protocolVersion + " State " + state + " ID " + Integer.toHexString(packetId));
+ " Protocol " + protocolVersion.id + " State " + state + " ID " + Integer.toHexString(packetId));
}
out.add(packet);
}

View File

@@ -2,8 +2,10 @@ package com.velocitypowered.proxy.protocol.netty;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.CorruptedFrameException;
import java.util.List;
@@ -15,12 +17,31 @@ public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder {
}
in.markReaderIndex();
int packetLength = ProtocolUtils.readVarInt(in);
if (in.readableBytes() < packetLength) {
in.resetReaderIndex();
return;
byte[] lenBuf = new byte[3];
for (int i = 0; i < lenBuf.length; i++) {
if (!in.isReadable()) {
in.resetReaderIndex();
return;
}
lenBuf[i] = in.readByte();
if (lenBuf[i] > 0) {
int packetLength = ProtocolUtils.readVarInt(Unpooled.wrappedBuffer(lenBuf));
if (packetLength == 0) {
return;
}
if (in.readableBytes() < packetLength) {
in.resetReaderIndex();
return;
}
out.add(in.readRetainedSlice(packetLength));
return;
}
}
out.add(in.readRetainedSlice(packetLength));
throw new CorruptedFrameException("VarInt too big");
}
}

View File

@@ -20,13 +20,20 @@ public enum PluginMessageUtil {
return message.getChannel().equals("MC|Brand") || message.getChannel().equals("minecraft:brand");
}
public static boolean isMCRegister(PluginMessage message) {
Preconditions.checkNotNull(message, "message");
return message.getChannel().equals("REGISTER") || message.getChannel().equals("minecraft:register");
}
public static boolean isMCUnregister(PluginMessage message) {
Preconditions.checkNotNull(message, "message");
return message.getChannel().equals("UNREGISTER") || message.getChannel().equals("minecraft:unregister");
}
public static List<String> getChannels(PluginMessage message) {
Preconditions.checkNotNull(message, "message");
Preconditions.checkArgument(message.getChannel().equals("REGISTER") ||
message.getChannel().equals("UNREGISTER") ||
message.getChannel().equals("minecraft:register") ||
message.getChannel().equals("minecraft:unregister"),
"Unknown channel type " + message.getChannel());
Preconditions.checkArgument(isMCRegister(message) || isMCUnregister(message),"Unknown channel type %s",
message.getChannel());
String channels = new String(message.getData(), StandardCharsets.UTF_8);
return ImmutableList.copyOf(channels.split("\0"));
}

View File

@@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import static org.junit.jupiter.api.Assertions.*;
@@ -33,4 +34,36 @@ class RecordingThreadFactoryTest {
Thread.sleep(10);
assertEquals(0, factory.size());
}
@Test
void cleanUpAfterExceptionThrown() throws Exception {
CountDownLatch started = new CountDownLatch(1);
CountDownLatch endThread = new CountDownLatch(1);
CountDownLatch hasEnded = new CountDownLatch(1);
RecordingThreadFactory factory = new RecordingThreadFactory((ThreadFactory) r -> {
Thread t = new Thread(r);
t.setUncaughtExceptionHandler((t1, e) -> hasEnded.countDown());
return t;
});
factory.newThread(() -> {
started.countDown();
assertTrue(factory.currentlyInFactory());
assertEquals(1, factory.size());
try {
endThread.await();
} catch (InterruptedException e) {
fail(e);
}
throw new RuntimeException("");
}).start();
started.await();
assertFalse(factory.currentlyInFactory());
assertEquals(1, factory.size());
endThread.countDown();
hasEnded.await();
// Wait a little bit to ensure the thread got shut down
Thread.sleep(10);
assertEquals(0, factory.size());
}
}