Merge branch 'dev/1.1.0' into bungeequack-integrate

This commit is contained in:
Andrew Steinborn
2019-11-26 15:52:53 -05:00
47 changed files with 901 additions and 377 deletions

View File

@@ -72,7 +72,10 @@ public class VelocityCommand implements Command {
@Override
public List<String> suggest(CommandSource source, String @NonNull [] currentArgs) {
if (currentArgs.length == 0) {
return ImmutableList.copyOf(subcommands.keySet());
return subcommands.entrySet().stream()
.filter(e -> e.getValue().hasPermission(source, new String[0]))
.map(Map.Entry::getKey)
.collect(ImmutableList.toImmutableList());
}
if (currentArgs.length == 1) {
@@ -81,7 +84,7 @@ public class VelocityCommand implements Command {
currentArgs[0].length()))
.filter(e -> e.getValue().hasPermission(source, new String[0]))
.map(Map.Entry::getKey)
.collect(Collectors.toList());
.collect(ImmutableList.toImmutableList());
}
Command command = subcommands.get(currentArgs[0].toLowerCase(Locale.US));

View File

@@ -100,7 +100,7 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
" configuration is used if no servers could be contacted."
})
@ConfigKey("ping-passthrough")
private PingPassthroughMode pingPassthrough;
private PingPassthroughMode pingPassthrough = PingPassthroughMode.DISABLED;
@Table("[servers]")
private final Servers servers;
@@ -192,44 +192,38 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
}
if (servers.getServers().isEmpty()) {
logger.error("You have no servers configured. :(");
valid = false;
} else {
if (servers.getAttemptConnectionOrder().isEmpty()) {
logger.error("No fallback servers are configured!");
logger.warn("You don't have any servers configured.");
}
for (Map.Entry<String, String> entry : servers.getServers().entrySet()) {
try {
AddressUtil.parseAddress(entry.getValue());
} catch (IllegalArgumentException e) {
logger.error("Server {} does not have a valid IP address.", entry.getKey(), e);
valid = false;
}
}
for (Map.Entry<String, String> entry : servers.getServers().entrySet()) {
try {
AddressUtil.parseAddress(entry.getValue());
} catch (IllegalArgumentException e) {
logger.error("Server {} does not have a valid IP address.", entry.getKey(), e);
valid = false;
}
for (String s : servers.getAttemptConnectionOrder()) {
if (!servers.getServers().containsKey(s)) {
logger.error("Fallback server " + s + " is not registered in your configuration!");
valid = false;
}
}
for (Map.Entry<String, List<String>> entry : forcedHosts.getForcedHosts().entrySet()) {
if (entry.getValue().isEmpty()) {
logger.error("Forced host '{}' does not contain any servers", entry.getKey());
valid = false;
continue;
}
for (String s : servers.getAttemptConnectionOrder()) {
if (!servers.getServers().containsKey(s)) {
logger.error("Fallback server " + s + " is not registered in your configuration!");
for (String server : entry.getValue()) {
if (!servers.getServers().containsKey(server)) {
logger.error("Server '{}' for forced host '{}' does not exist", server, entry.getKey());
valid = false;
}
}
for (Map.Entry<String, List<String>> entry : forcedHosts.getForcedHosts().entrySet()) {
if (entry.getValue().isEmpty()) {
logger.error("Forced host '{}' does not contain any servers", entry.getKey());
valid = false;
continue;
}
for (String server : entry.getValue()) {
if (!servers.getServers().containsKey(server)) {
logger.error("Server '{}' for forced host '{}' does not exist", server, entry.getKey());
valid = false;
}
}
}
}
try {

View File

@@ -34,14 +34,4 @@ public interface ConnectionType {
*/
GameProfile addGameProfileTokensIfRequired(GameProfile original,
PlayerInfoForwarding forwardingType);
/**
* Tests whether the hostname is the handshake packet is valid.
*
* @param address The address to check
* @return true if valid.
*/
default boolean checkServerAddressIsValid(String address) {
return true;
}
}

View File

@@ -80,7 +80,8 @@ public class TransitionSessionHandler implements MinecraftSessionHandler {
// The goods are in hand! We got JoinGame. Let's transition completely to the new state.
smc.setAutoReading(false);
server.getEventManager()
.fire(new ServerConnectedEvent(serverConn.getPlayer(), serverConn.getServer()))
.fire(new ServerConnectedEvent(serverConn.getPlayer(), serverConn.getServer(),
existingConnection != null ? existingConnection.getServer() : null))
.whenCompleteAsync((x, error) -> {
// Strap on the ClientPlaySessionHandler if required.
ClientPlaySessionHandler playHandler;

View File

@@ -304,26 +304,20 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
// Clear tab list to avoid duplicate entries
player.getTabList().clearAll();
// In order to handle switching to another server, you will need to send three packets:
// In order to handle switching to another server, you will need to send two packets:
//
// - The join game packet from the backend server
// - A respawn packet with a different dimension
// - Another respawn with the correct dimension
//
// The two respawns with different dimensions are required, otherwise the client gets
// confused.
// - The join game packet from the backend server, with a different dimension
// - A respawn with the correct dimension
//
// Most notably, by having the client accept the join game packet, we can work around the need
// to perform entity ID rewrites, eliminating potential issues from rewriting packets and
// improving compatibility with mods.
int realDim = joinGame.getDimension();
joinGame.setDimension(getFakeTemporaryDimensionId(realDim));
player.getConnection().delayedWrite(joinGame);
int tempDim = joinGame.getDimension() == 0 ? -1 : 0;
player.getConnection().delayedWrite(
new Respawn(tempDim, joinGame.getDifficulty(), joinGame.getGamemode(),
joinGame.getLevelType()));
player.getConnection().delayedWrite(
new Respawn(joinGame.getDimension(), joinGame.getDifficulty(), joinGame.getGamemode(),
joinGame.getLevelType()));
new Respawn(realDim, joinGame.getPartialHashedSeed(), joinGame.getDifficulty(),
joinGame.getGamemode(), joinGame.getLevelType()));
}
// Remove previous boss bars. These don't get cleared when sending JoinGame, thus the need to
@@ -360,11 +354,14 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
destination.completeJoin();
}
private static int getFakeTemporaryDimensionId(int dim) {
return dim == 0 ? -1 : 0;
}
public List<UUID> getServerBossBars() {
return serverBossBars;
}
private boolean handleCommandTabComplete(TabCompleteRequest packet) {
// In 1.13+, we need to do additional work for the richer suggestions available.
String command = packet.getCommand().substring(1);
@@ -389,31 +386,19 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
}
List<Offer> offers = new ArrayList<>();
int longestLength = 0;
for (String suggestion : suggestions) {
offers.add(new Offer(suggestion));
if (suggestion.length() > longestLength) {
longestLength = suggestion.length();
}
}
TabCompleteResponse resp = new TabCompleteResponse();
resp.setTransactionId(packet.getTransactionId());
int startPos = packet.getCommand().lastIndexOf(' ') + 1;
int length;
if (startPos == 0) {
startPos = packet.getCommand().length() + 1;
length = longestLength;
} else {
length = packet.getCommand().length() - startPos;
if (startPos > 0) {
TabCompleteResponse resp = new TabCompleteResponse();
resp.setTransactionId(packet.getTransactionId());
resp.setStart(startPos);
resp.setLength(packet.getCommand().length() - startPos);
resp.getOffers().addAll(offers);
player.getConnection().write(resp);
}
resp.setStart(startPos);
resp.setLength(length);
resp.getOffers().addAll(offers);
player.getConnection().write(resp);
return true;
}

View File

@@ -425,33 +425,22 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
return;
}
if (connectedServer == null) {
Optional<RegisteredServer> nextServer = getNextServerToTry(rs);
if (nextServer.isPresent()) {
// There can't be any connection in flight now.
resetInFlightConnection();
createConnectionRequest(nextServer.get()).fireAndForget();
} else {
disconnect(friendlyReason);
}
boolean kickedFromCurrent = connectedServer == null || connectedServer.getServer().equals(rs);
ServerKickResult result;
if (kickedFromCurrent) {
Optional<RegisteredServer> next = getNextServerToTry(rs);
result = next.<ServerKickResult>map(RedirectPlayer::create)
.orElseGet(() -> DisconnectPlayer.create(friendlyReason));
} else {
boolean kickedFromCurrent = connectedServer.getServer().equals(rs);
ServerKickResult result;
if (kickedFromCurrent) {
Optional<RegisteredServer> next = getNextServerToTry(rs);
result = next.<ServerKickResult>map(RedirectPlayer::create)
.orElseGet(() -> DisconnectPlayer.create(friendlyReason));
} else {
// If we were kicked by going to another server, the connection should not be in flight
if (connectionInFlight != null && connectionInFlight.getServer().equals(rs)) {
resetInFlightConnection();
}
result = Notify.create(friendlyReason);
// If we were kicked by going to another server, the connection should not be in flight
if (connectionInFlight != null && connectionInFlight.getServer().equals(rs)) {
resetInFlightConnection();
}
KickedFromServerEvent originalEvent = new KickedFromServerEvent(this, rs, kickReason,
!kickedFromCurrent, result);
handleKickEvent(originalEvent, friendlyReason);
result = Notify.create(friendlyReason);
}
KickedFromServerEvent originalEvent = new KickedFromServerEvent(this, rs, kickReason,
!kickedFromCurrent, result);
handleKickEvent(originalEvent, friendlyReason);
}
private void handleKickEvent(KickedFromServerEvent originalEvent, Component friendlyReason) {
@@ -471,7 +460,11 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
if (newResult == null || !newResult) {
disconnect(friendlyReason);
} else {
sendMessage(VelocityMessages.MOVED_TO_NEW_SERVER.append(friendlyReason));
if (res.getMessage() == null) {
sendMessage(VelocityMessages.MOVED_TO_NEW_SERVER.append(friendlyReason));
} else {
sendMessage(res.getMessage());
}
}
}, connection.eventLoop());
} else if (event.getResult() instanceof Notify) {

View File

@@ -26,9 +26,14 @@ import java.util.Optional;
import net.kyori.text.TextComponent;
import net.kyori.text.TranslatableComponent;
import net.kyori.text.format.TextColor;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.checkerframework.checker.nullness.qual.Nullable;
public class HandshakeSessionHandler implements MinecraftSessionHandler {
private static final Logger LOGGER = LogManager.getLogger(HandshakeSessionHandler.class);
private final MinecraftConnection connection;
private final VelocityServer server;
@@ -58,58 +63,72 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
public boolean handle(Handshake handshake) {
InitialInboundConnection ic = new InitialInboundConnection(connection,
cleanVhost(handshake.getServerAddress()), handshake);
connection.setAssociation(ic);
switch (handshake.getNextStatus()) {
StateRegistry nextState = getStateForProtocol(handshake.getNextStatus());
if (nextState == null) {
LOGGER.error("{} provided invalid protocol {}", ic, handshake.getNextStatus());
connection.close();
} else {
connection.setState(nextState);
connection.setProtocolVersion(handshake.getProtocolVersion());
connection.setAssociation(ic);
switch (nextState) {
case STATUS:
connection.setSessionHandler(new StatusSessionHandler(server, connection, ic));
break;
case LOGIN:
this.handleLogin(handshake, ic);
break;
default:
// If you get this, it's a bug in Velocity.
throw new AssertionError("getStateForProtocol provided invalid state!");
}
}
return true;
}
private static @Nullable StateRegistry getStateForProtocol(int status) {
switch (status) {
case StateRegistry.STATUS_ID:
connection.setState(StateRegistry.STATUS);
connection.setProtocolVersion(handshake.getProtocolVersion());
connection.setSessionHandler(new StatusSessionHandler(server, connection, ic));
return true;
return StateRegistry.STATUS;
case StateRegistry.LOGIN_ID:
connection.setState(StateRegistry.LOGIN);
connection.setProtocolVersion(handshake.getProtocolVersion());
if (!ProtocolVersion.isSupported(handshake.getProtocolVersion())) {
connection.closeWith(Disconnect
.create(TranslatableComponent.of("multiplayer.disconnect.outdated_client")));
return true;
}
InetAddress address = ((InetSocketAddress) connection.getRemoteAddress()).getAddress();
if (!server.getIpAttemptLimiter().attempt(address)) {
connection.closeWith(
Disconnect.create(TextComponent.of("You are logging in too fast, try again later.")));
return true;
}
ConnectionType type = checkForForge(handshake);
connection.setType(type);
// Make sure legacy forwarding is not in use on this connection.
if (!type.checkServerAddressIsValid(handshake.getServerAddress())) {
connection.closeWith(Disconnect
.create(TextComponent.of("Running Velocity behind Velocity is unsupported.")));
return true;
}
// If the proxy is configured for modern forwarding, we must deny connections from 1.12.2
// and lower, otherwise IP information will never get forwarded.
if (server.getConfiguration().getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN
&& handshake.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_13) < 0) {
connection.closeWith(Disconnect
.create(TextComponent.of("This server is only compatible with 1.13 and above.")));
return true;
}
server.getEventManager().fireAndForget(new ConnectionHandshakeEvent(ic));
connection.setSessionHandler(new LoginSessionHandler(server, connection, ic));
return true;
return StateRegistry.LOGIN;
default:
throw new IllegalArgumentException("Invalid state " + handshake.getNextStatus());
return null;
}
}
private ConnectionType checkForForge(Handshake handshake) {
private void handleLogin(Handshake handshake, InitialInboundConnection ic) {
if (!ProtocolVersion.isSupported(handshake.getProtocolVersion())) {
connection.closeWith(Disconnect
.create(TranslatableComponent.of("multiplayer.disconnect.outdated_client")));
return;
}
InetAddress address = ((InetSocketAddress) connection.getRemoteAddress()).getAddress();
if (!server.getIpAttemptLimiter().attempt(address)) {
connection.closeWith(
Disconnect.create(TextComponent.of("You are logging in too fast, try again later.")));
return;
}
connection.setType(getHandshakeConnectionType(handshake));
// If the proxy is configured for modern forwarding, we must deny connections from 1.12.2
// and lower, otherwise IP information will never get forwarded.
if (server.getConfiguration().getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN
&& handshake.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_13) < 0) {
connection.closeWith(Disconnect
.create(TextComponent.of("This server is only compatible with 1.13 and above.")));
return;
}
server.getEventManager().fireAndForget(new ConnectionHandshakeEvent(ic));
connection.setSessionHandler(new LoginSessionHandler(server, connection, ic));
}
private ConnectionType getHandshakeConnectionType(Handshake handshake) {
// Determine if we're using Forge (1.8 to 1.12, may not be the case in 1.13).
if (handshake.getServerAddress().endsWith(LegacyForgeConstants.HANDSHAKE_HOSTNAME_TOKEN)
&& handshake.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_13) < 0) {
@@ -119,8 +138,8 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
// forge handshake attempts. Also sends a reset handshake packet on every transition.
return ConnectionTypes.UNDETERMINED_17;
} else {
// For later: See if we can determine Forge 1.13+ here, else this will need to be UNDETERMINED
// until later in the cycle (most likely determinable during the LOGIN phase)
// Note for future implementation: Forge 1.13+ identifies itself using a slightly different
// hostname token.
return ConnectionTypes.VANILLA;
}
}

View File

@@ -1,24 +1,21 @@
package com.velocitypowered.proxy.connection.client;
import static com.google.common.net.UrlEscapers.urlFormParameterEscaper;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_13;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8;
import static com.velocitypowered.proxy.VelocityServer.GSON;
import static com.velocitypowered.proxy.connection.VelocityConstants.EMPTY_BYTE_ARRAY;
import static com.velocitypowered.proxy.connection.VelocityConstants.VELOCITY_IP_FORWARDING_CHANNEL;
import static com.velocitypowered.proxy.util.EncryptionUtils.decryptRsa;
import static com.velocitypowered.proxy.util.EncryptionUtils.generateServerId;
import static org.asynchttpclient.Dsl.asyncHttpClient;
import static org.asynchttpclient.Dsl.config;
import com.google.common.base.Preconditions;
import com.google.common.net.UrlEscapers;
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.event.player.PlayerChooseInitialServerEvent;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import com.velocitypowered.api.util.GameProfile;
import com.velocitypowered.proxy.VelocityServer;
@@ -29,17 +26,12 @@ import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.packet.Disconnect;
import com.velocitypowered.proxy.protocol.packet.EncryptionRequest;
import com.velocitypowered.proxy.protocol.packet.EncryptionResponse;
import com.velocitypowered.proxy.protocol.packet.LoginPluginMessage;
import com.velocitypowered.proxy.protocol.packet.LoginPluginResponse;
import com.velocitypowered.proxy.protocol.packet.ServerLogin;
import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccess;
import com.velocitypowered.proxy.protocol.packet.SetCompression;
import com.velocitypowered.proxy.util.VelocityMessages;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.util.Arrays;
@@ -50,7 +42,6 @@ import java.util.concurrent.ThreadLocalRandom;
import net.kyori.text.Component;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.asynchttpclient.Dsl;
import org.asynchttpclient.ListenableFuture;
import org.asynchttpclient.Response;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@@ -66,7 +57,6 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
private final InitialInboundConnection inbound;
private @MonotonicNonNull ServerLogin login;
private byte[] verify = EMPTY_BYTE_ARRAY;
private int playerInfoId;
private @MonotonicNonNull ConnectedPlayer connectedPlayer;
LoginSessionHandler(VelocityServer server, MinecraftConnection mcConnection,
@@ -79,29 +69,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
@Override
public boolean handle(ServerLogin packet) {
this.login = packet;
if (mcConnection.getProtocolVersion().compareTo(MINECRAFT_1_13) >= 0) {
// To make sure the connecting client isn't Velocity, send a plugin message that Velocity will
// recognize and respond to.
playerInfoId = ThreadLocalRandom.current().nextInt();
mcConnection.write(new LoginPluginMessage(playerInfoId, VELOCITY_IP_FORWARDING_CHANNEL,
Unpooled.EMPTY_BUFFER));
} else {
beginPreLogin();
}
return true;
}
@Override
public boolean handle(LoginPluginResponse packet) {
if (packet.getId() == playerInfoId) {
if (packet.isSuccess()) {
// Uh oh, someone's trying to run Velocity behind Velocity. We don't want that happening.
inbound.disconnect(VelocityMessages.NO_PROXY_BEHIND_PROXY);
} else {
// Proceed with the regular login process.
beginPreLogin();
}
}
beginPreLogin();
return true;
}
@@ -254,12 +222,6 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
}
private void finishLogin(ConnectedPlayer player) {
Optional<RegisteredServer> toTry = player.getNextServerToTry();
if (!toTry.isPresent()) {
player.disconnect(VelocityMessages.NO_AVAILABLE_SERVERS);
return;
}
int threshold = server.getConfiguration().getCompressionThreshold();
if (threshold >= 0 && mcConnection.getProtocolVersion().compareTo(MINECRAFT_1_8) >= 0) {
mcConnection.write(new SetCompression(threshold));
@@ -292,11 +254,27 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
mcConnection.setSessionHandler(new InitialConnectSessionHandler(player));
server.getEventManager().fire(new PostLoginEvent(player))
.thenRun(() -> player.createConnectionRequest(toTry.get()).fireAndForget());
.thenRun(() -> connectToInitialServer(player));
}
}, mcConnection.eventLoop());
}
private void connectToInitialServer(ConnectedPlayer player) {
Optional<RegisteredServer> initialFromConfig = player.getNextServerToTry();
PlayerChooseInitialServerEvent event = new PlayerChooseInitialServerEvent(player,
initialFromConfig.orElse(null));
server.getEventManager().fire(event)
.thenRunAsync(() -> {
Optional<RegisteredServer> toTry = event.getInitialServer();
if (!toTry.isPresent()) {
player.disconnect(VelocityMessages.NO_AVAILABLE_SERVERS);
return;
}
player.createConnectionRequest(toTry.get()).fireAndForget();
}, mcConnection.eventLoop());
}
@Override
public void handleUnknown(ByteBuf buf) {
mcConnection.close();

View File

@@ -6,7 +6,7 @@ package com.velocitypowered.proxy.connection.forge.legacy;
public class LegacyForgeConstants {
/**
* Clients attempting to connect to 1.8+ Forge servers will have
* Clients attempting to connect to 1.8-1.12.2 Forge servers will have
* this token appended to the hostname in the initial handshake
* packet.
*/

View File

@@ -7,6 +7,11 @@ import io.netty.channel.epoll.EpollDatagramChannel;
import io.netty.channel.epoll.EpollEventLoopGroup;
import io.netty.channel.epoll.EpollServerSocketChannel;
import io.netty.channel.epoll.EpollSocketChannel;
import io.netty.channel.kqueue.KQueue;
import io.netty.channel.kqueue.KQueueDatagramChannel;
import io.netty.channel.kqueue.KQueueEventLoopGroup;
import io.netty.channel.kqueue.KQueueServerSocketChannel;
import io.netty.channel.kqueue.KQueueSocketChannel;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.DatagramChannel;
import io.netty.channel.socket.ServerSocketChannel;
@@ -22,7 +27,11 @@ enum TransportType {
(name, type) -> new NioEventLoopGroup(0, createThreadFactory(name, type))),
EPOLL("epoll", EpollServerSocketChannel.class, EpollSocketChannel.class,
EpollDatagramChannel.class,
(name, type) -> new EpollEventLoopGroup(0, createThreadFactory(name, type)));
(name, type) -> new EpollEventLoopGroup(0, createThreadFactory(name, type))),
KQUEUE("Kqueue", KQueueServerSocketChannel.class, KQueueSocketChannel.class,
KQueueDatagramChannel.class,
(name, type) -> new KQueueEventLoopGroup(0, createThreadFactory(name, type)));
final String name;
final Class<? extends ServerSocketChannel> serverSocketChannelClass;
@@ -62,6 +71,8 @@ enum TransportType {
if (Epoll.isAvailable()) {
return EPOLL;
} else if (KQueue.isAvailable()) {
return KQUEUE;
} else {
return NIO;
}

View File

@@ -3,6 +3,7 @@ package com.velocitypowered.proxy.plugin;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.base.Joiner;
import com.velocitypowered.api.plugin.PluginContainer;
import com.velocitypowered.api.plugin.PluginDescription;
import com.velocitypowered.api.plugin.PluginManager;
@@ -101,6 +102,8 @@ public class VelocityPluginManager implements PluginManager {
continue;
}
logger.info("Loaded plugin {} {} by {}", plugin.getId(), plugin.getVersion()
.orElse("<UNKNOWN>"), Joiner.on(", ").join(plugin.getAuthors()));
registerPlugin(pluginObject);
}
}

View File

@@ -5,6 +5,7 @@ import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_12;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_12_1;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_13;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_14;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_15;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_7_2;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9;
@@ -120,51 +121,61 @@ public enum StateRegistry {
map(0x1F, MINECRAFT_1_14, false));
clientbound.register(BossBar.class, BossBar::new,
map(0x0C, MINECRAFT_1_9, false));
map(0x0C, MINECRAFT_1_9, false),
map(0x0D, MINECRAFT_1_15, false));
clientbound.register(Chat.class, Chat::new,
map(0x02, MINECRAFT_1_7_2, true),
map(0x0F, MINECRAFT_1_9, true),
map(0x0E, MINECRAFT_1_13, true));
map(0x0E, MINECRAFT_1_13, true),
map(0x0F, MINECRAFT_1_15, true));
clientbound.register(TabCompleteResponse.class, TabCompleteResponse::new,
map(0x3A, MINECRAFT_1_7_2, false),
map(0x0E, MINECRAFT_1_9, false),
map(0x10, MINECRAFT_1_13, false));
map(0x10, MINECRAFT_1_13, false),
map(0x11, MINECRAFT_1_15, false));
clientbound.register(AvailableCommands.class, AvailableCommands::new,
map(0x11, MINECRAFT_1_13, false));
map(0x11, MINECRAFT_1_13, false),
map(0x12, MINECRAFT_1_15, false));
clientbound.register(PluginMessage.class, PluginMessage::new,
map(0x3F, MINECRAFT_1_7_2, false),
map(0x18, MINECRAFT_1_9, false),
map(0x19, MINECRAFT_1_13, false),
map(0x18, MINECRAFT_1_14, false));
map(0x18, MINECRAFT_1_14, false),
map(0x19, MINECRAFT_1_15, false));
clientbound.register(Disconnect.class, Disconnect::new,
map(0x40, MINECRAFT_1_7_2, false),
map(0x1A, MINECRAFT_1_9, false),
map(0x1B, MINECRAFT_1_13, false),
map(0x1A, MINECRAFT_1_14, false));
map(0x1A, MINECRAFT_1_14, false),
map(0x1B, MINECRAFT_1_15, false));
clientbound.register(KeepAlive.class, KeepAlive::new,
map(0x00, MINECRAFT_1_7_2, false),
map(0x1F, MINECRAFT_1_9, false),
map(0x21, MINECRAFT_1_13, false),
map(0x20, MINECRAFT_1_14, false));
map(0x20, MINECRAFT_1_14, false),
map(0x21, MINECRAFT_1_15, false));
clientbound.register(JoinGame.class, JoinGame::new,
map(0x01, MINECRAFT_1_7_2, false),
map(0x23, MINECRAFT_1_9, false),
map(0x25, MINECRAFT_1_13, false),
map(0x25, MINECRAFT_1_14, false));
map(0x25, MINECRAFT_1_14, false),
map(0x26, MINECRAFT_1_15, false));
clientbound.register(Respawn.class, Respawn::new,
map(0x07, MINECRAFT_1_7_2, true),
map(0x33, MINECRAFT_1_9, true),
map(0x34, MINECRAFT_1_12, true),
map(0x35, MINECRAFT_1_12_1, true),
map(0x38, MINECRAFT_1_13, true),
map(0x3A, MINECRAFT_1_14, true));
map(0x3A, MINECRAFT_1_14, true),
map(0x3B, MINECRAFT_1_15, true));
clientbound.register(ResourcePackRequest.class, ResourcePackRequest::new,
map(0x48, MINECRAFT_1_8, true),
map(0x32, MINECRAFT_1_9, true),
map(0x33, MINECRAFT_1_12, true),
map(0x34, MINECRAFT_1_12_1, true),
map(0x37, MINECRAFT_1_13, true),
map(0x39, MINECRAFT_1_14, true));
map(0x39, MINECRAFT_1_14, true),
map(0x3A, MINECRAFT_1_15, true));
clientbound.register(HeaderAndFooter.class, HeaderAndFooter::new,
map(0x47, MINECRAFT_1_8, true),
map(0x48, MINECRAFT_1_9, true),
@@ -172,20 +183,23 @@ public enum StateRegistry {
map(0x49, MINECRAFT_1_12, true),
map(0x4A, MINECRAFT_1_12_1, true),
map(0x4E, MINECRAFT_1_13, true),
map(0x53, MINECRAFT_1_14, true));
map(0x53, MINECRAFT_1_14, true),
map(0x54, MINECRAFT_1_15, true));
clientbound.register(TitlePacket.class, TitlePacket::new,
map(0x45, MINECRAFT_1_8, true),
map(0x45, MINECRAFT_1_9, true),
map(0x47, MINECRAFT_1_12, true),
map(0x48, MINECRAFT_1_12_1, true),
map(0x4B, MINECRAFT_1_13, true),
map(0x4F, MINECRAFT_1_14, true));
map(0x4F, MINECRAFT_1_14, true),
map(0x50, MINECRAFT_1_15, true));
clientbound.register(PlayerListItem.class, PlayerListItem::new,
map(0x38, MINECRAFT_1_7_2, false),
map(0x2D, MINECRAFT_1_9, false),
map(0x2E, MINECRAFT_1_12_1, false),
map(0x30, MINECRAFT_1_13, false),
map(0x33, MINECRAFT_1_14, false));
map(0x33, MINECRAFT_1_14, false),
map(0x34, MINECRAFT_1_15, false));
}
},
LOGIN {

View File

@@ -18,6 +18,7 @@ import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.socket.DatagramPacket;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
@@ -59,6 +60,7 @@ public class GS4QueryHandler extends SimpleChannelInboundHandler<DatagramPacket>
private final Cache<InetAddress, Integer> sessions = CacheBuilder.newBuilder()
.expireAfterWrite(30, TimeUnit.SECONDS)
.build();
private final SecureRandom random;
private volatile @MonotonicNonNull List<QueryResponse.PluginInformation> pluginInformationList
= null;
@@ -67,6 +69,7 @@ public class GS4QueryHandler extends SimpleChannelInboundHandler<DatagramPacket>
public GS4QueryHandler(VelocityServer server) {
this.server = server;
this.random = new SecureRandom();
}
private QueryResponse createInitialResponse() {
@@ -111,7 +114,7 @@ public class GS4QueryHandler extends SimpleChannelInboundHandler<DatagramPacket>
switch (type) {
case QUERY_TYPE_HANDSHAKE: {
// Generate new challenge token and put it into the sessions cache
int challengeToken = ThreadLocalRandom.current().nextInt();
int challengeToken = random.nextInt();
sessions.put(senderAddress, challengeToken);
// Respond with challenge token

View File

@@ -5,10 +5,10 @@ import com.velocitypowered.natives.encryption.VelocityCipher;
import com.velocitypowered.natives.util.MoreByteBufUtils;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.MessageToMessageDecoder;
import java.util.List;
public class MinecraftCipherDecoder extends ByteToMessageDecoder {
public class MinecraftCipherDecoder extends MessageToMessageDecoder<ByteBuf> {
private final VelocityCipher cipher;
@@ -18,16 +18,19 @@ public class MinecraftCipherDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
ByteBuf compatible = MoreByteBufUtils.ensureCompatible(ctx.alloc(), cipher, in);
ByteBuf compatible = MoreByteBufUtils.ensureCompatible(ctx.alloc(), cipher, in).slice();
try {
out.add(cipher.process(ctx, compatible));
} finally {
compatible.release();
cipher.process(compatible);
out.add(compatible);
in.skipBytes(in.readableBytes());
} catch (Exception e) {
compatible.release(); // compatible will never be used if we throw an exception
throw e;
}
}
@Override
protected void handlerRemoved0(ChannelHandlerContext ctx) throws Exception {
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
cipher.dispose();
}
}

View File

@@ -20,9 +20,11 @@ public class MinecraftCipherEncoder extends MessageToMessageEncoder<ByteBuf> {
protected void encode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
ByteBuf compatible = MoreByteBufUtils.ensureCompatible(ctx.alloc(), cipher, msg);
try {
out.add(cipher.process(ctx, compatible));
} finally {
compatible.release();
cipher.process(compatible);
out.add(compatible);
} catch (Exception e) {
compatible.release(); // compatible will never be used if we throw an exception
throw e;
}
}

View File

@@ -13,7 +13,8 @@ import java.util.List;
public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
private static final int MAXIMUM_UNCOMPRESSED_SIZE = 2 * 1024 * 1024; // 2MiB
private static final int SOFT_MAXIMUM_UNCOMPRESSED_SIZE = 2 * 1024 * 1024; // 2MiB
private static final int HARD_MAXIMUM_UNCOMPRESSED_SIZE = 16 * 1024 * 1024; // 16MiB
private final int threshold;
private final VelocityCompressor compressor;
@@ -25,21 +26,23 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
int expectedSize = ProtocolUtils.readVarInt(in);
if (expectedSize == 0) {
int claimedUncompressedSize = ProtocolUtils.readVarInt(in);
if (claimedUncompressedSize == 0) {
// Strip the now-useless uncompressed size, this message is already uncompressed.
out.add(in.retainedSlice());
in.skipBytes(in.readableBytes());
return;
}
checkFrame(expectedSize >= threshold, "Uncompressed size %s is less than threshold %s",
expectedSize, threshold);
int initialCapacity = Math.min(expectedSize, MAXIMUM_UNCOMPRESSED_SIZE);
checkFrame(claimedUncompressedSize >= threshold, "Uncompressed size %s is less than"
+ " threshold %s", claimedUncompressedSize, threshold);
int allowedMax = Math.min(claimedUncompressedSize, HARD_MAXIMUM_UNCOMPRESSED_SIZE);
int initialCapacity = Math.min(claimedUncompressedSize, SOFT_MAXIMUM_UNCOMPRESSED_SIZE);
ByteBuf compatibleIn = ensureCompatible(ctx.alloc(), compressor, in);
ByteBuf uncompressed = preferredBuffer(ctx.alloc(), compressor, initialCapacity);
try {
compressor.inflate(compatibleIn, uncompressed, expectedSize);
compressor.inflate(compatibleIn, uncompressed, allowedMax);
out.add(uncompressed);
} catch (Exception e) {
uncompressed.release();

View File

@@ -47,20 +47,21 @@ public class MinecraftDecoder extends MessageToMessageDecoder<ByteBuf> {
packet.decode(msg, direction, registry.version);
} catch (Exception e) {
throw new CorruptedFrameException(
"Error decoding " + packet.getClass() + " Direction " + direction
+ " Protocol " + registry.version + " State " + state + " ID " + Integer
.toHexString(packetId), e);
"Error decoding " + packet.getClass() + " " + getExtraConnectionDetail(packetId), e);
}
if (msg.isReadable()) {
throw new CorruptedFrameException(
"Did not read full packet for " + packet.getClass() + " Direction " + direction
+ " Protocol " + registry.version + " State " + state + " ID " + Integer
.toHexString(packetId));
throw new CorruptedFrameException("Did not read full packet for " + packet.getClass() + " "
+ getExtraConnectionDetail(packetId));
}
out.add(packet);
}
}
private String getExtraConnectionDetail(int packetId) {
return "Direction " + direction + " Protocol " + registry.version + " State " + state
+ " ID " + Integer.toHexString(packetId);
}
public void setProtocolVersion(ProtocolVersion protocolVersion) {
this.registry = direction.getProtocolRegistry(state, protocolVersion);
}

View File

@@ -27,16 +27,13 @@ public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder {
// Make sure reader index of length buffer is returned to the beginning
in.readerIndex(origReaderIndex);
int packetLength = ProtocolUtils.readVarInt(in);
if (packetLength == 0) {
return;
}
if (in.readableBytes() < packetLength) {
if (in.readableBytes() >= packetLength) {
out.add(in.readRetainedSlice(packetLength));
} else {
in.readerIndex(origReaderIndex);
return;
}
out.add(in.readRetainedSlice(packetLength));
return;
}
}

View File

@@ -1,5 +1,7 @@
package com.velocitypowered.proxy.protocol.netty;
import com.velocitypowered.natives.encryption.JavaVelocityCipher;
import com.velocitypowered.natives.util.Natives;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
@@ -11,6 +13,7 @@ import java.util.List;
public class MinecraftVarintLengthEncoder extends MessageToMessageEncoder<ByteBuf> {
public static final MinecraftVarintLengthEncoder INSTANCE = new MinecraftVarintLengthEncoder();
private static final boolean IS_JAVA_CIPHER = Natives.cipher.get() == JavaVelocityCipher.FACTORY;
private MinecraftVarintLengthEncoder() {
}
@@ -18,7 +21,7 @@ public class MinecraftVarintLengthEncoder extends MessageToMessageEncoder<ByteBu
@Override
protected void encode(ChannelHandlerContext ctx, ByteBuf buf, List<Object> list)
throws Exception {
ByteBuf lengthBuf = ctx.alloc().buffer(5); // the maximum size of a varint
ByteBuf lengthBuf = IS_JAVA_CIPHER ? ctx.alloc().heapBuffer(5) : ctx.alloc().directBuffer(5);
ProtocolUtils.writeVarInt(lengthBuf, buf.readableBytes());
list.add(lengthBuf);
list.add(buf.retain());

View File

@@ -12,11 +12,13 @@ public class JoinGame implements MinecraftPacket {
private int entityId;
private short gamemode;
private int dimension;
private long partialHashedSeed; // 1.15+
private short difficulty;
private short maxPlayers;
private @Nullable String levelType;
private int viewDistance; //1.14+
private boolean reducedDebugInfo;
private boolean mystery;
public int getEntityId() {
return entityId;
@@ -42,6 +44,10 @@ public class JoinGame implements MinecraftPacket {
this.dimension = dimension;
}
public long getPartialHashedSeed() {
return partialHashedSeed;
}
public short getDifficulty() {
return difficulty;
}
@@ -91,6 +97,7 @@ public class JoinGame implements MinecraftPacket {
+ "entityId=" + entityId
+ ", gamemode=" + gamemode
+ ", dimension=" + dimension
+ ", partialHashedSeed=" + partialHashedSeed
+ ", difficulty=" + difficulty
+ ", maxPlayers=" + maxPlayers
+ ", levelType='" + levelType + '\''
@@ -111,6 +118,9 @@ public class JoinGame implements MinecraftPacket {
if (version.compareTo(ProtocolVersion.MINECRAFT_1_13_2) <= 0) {
this.difficulty = buf.readUnsignedByte();
}
if (version.compareTo(ProtocolVersion.MINECRAFT_1_15) >= 0) {
this.partialHashedSeed = buf.readLong();
}
this.maxPlayers = buf.readUnsignedByte();
this.levelType = ProtocolUtils.readString(buf, 16);
if (version.compareTo(ProtocolVersion.MINECRAFT_1_14) >= 0) {
@@ -119,6 +129,9 @@ public class JoinGame implements MinecraftPacket {
if (version.compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) {
this.reducedDebugInfo = buf.readBoolean();
}
if (version.compareTo(ProtocolVersion.MINECRAFT_1_15) >= 0) {
this.mystery = buf.readBoolean();
}
}
@Override
@@ -133,6 +146,9 @@ public class JoinGame implements MinecraftPacket {
if (version.compareTo(ProtocolVersion.MINECRAFT_1_13_2) <= 0) {
buf.writeByte(difficulty);
}
if (version.compareTo(ProtocolVersion.MINECRAFT_1_15) >= 0) {
buf.writeLong(partialHashedSeed);
}
buf.writeByte(maxPlayers);
if (levelType == null) {
throw new IllegalStateException("No level type specified.");
@@ -144,6 +160,9 @@ public class JoinGame implements MinecraftPacket {
if (version.compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) {
buf.writeBoolean(reducedDebugInfo);
}
if (version.compareTo(ProtocolVersion.MINECRAFT_1_15) >= 0) {
buf.writeBoolean(mystery);
}
}
@Override

View File

@@ -9,6 +9,7 @@ import io.netty.buffer.ByteBuf;
public class Respawn implements MinecraftPacket {
private int dimension;
private long partialHashedSeed;
private short difficulty;
private short gamemode;
private String levelType = "";
@@ -16,8 +17,10 @@ public class Respawn implements MinecraftPacket {
public Respawn() {
}
public Respawn(int dimension, short difficulty, short gamemode, String levelType) {
public Respawn(int dimension, long partialHashedSeed, short difficulty, short gamemode,
String levelType) {
this.dimension = dimension;
this.partialHashedSeed = partialHashedSeed;
this.difficulty = difficulty;
this.gamemode = gamemode;
this.levelType = levelType;
@@ -31,6 +34,14 @@ public class Respawn implements MinecraftPacket {
this.dimension = dimension;
}
public long getPartialHashedSeed() {
return partialHashedSeed;
}
public void setPartialHashedSeed(long partialHashedSeed) {
this.partialHashedSeed = partialHashedSeed;
}
public short getDifficulty() {
return difficulty;
}
@@ -59,6 +70,7 @@ public class Respawn implements MinecraftPacket {
public String toString() {
return "Respawn{"
+ "dimension=" + dimension
+ ", partialHashedSeed=" + partialHashedSeed
+ ", difficulty=" + difficulty
+ ", gamemode=" + gamemode
+ ", levelType='" + levelType + '\''
@@ -71,6 +83,9 @@ public class Respawn implements MinecraftPacket {
if (version.compareTo(ProtocolVersion.MINECRAFT_1_13_2) <= 0) {
this.difficulty = buf.readUnsignedByte();
}
if (version.compareTo(ProtocolVersion.MINECRAFT_1_15) >= 0) {
this.partialHashedSeed = buf.readLong();
}
this.gamemode = buf.readUnsignedByte();
this.levelType = ProtocolUtils.readString(buf, 16);
}
@@ -81,6 +96,9 @@ public class Respawn implements MinecraftPacket {
if (version.compareTo(ProtocolVersion.MINECRAFT_1_13_2) <= 0) {
buf.writeByte(difficulty);
}
if (version.compareTo(ProtocolVersion.MINECRAFT_1_15) >= 0) {
buf.writeLong(partialHashedSeed);
}
buf.writeByte(gamemode);
ProtocolUtils.writeString(buf, levelType);
}

View File

@@ -17,7 +17,7 @@ public class VelocityNettyThreadFactory implements ThreadFactory {
@Override
public Thread newThread(Runnable r) {
String name = String.format(nameFormat, threadNumber.incrementAndGet());
String name = String.format(nameFormat, threadNumber.getAndIncrement());
return new FastThreadLocalThread(r, name);
}
}