Merge remote-tracking branch 'upstream/dev/1.1.0'

This commit is contained in:
Gabik21
2019-08-29 19:47:04 +02:00
37 changed files with 708 additions and 538 deletions

View File

@@ -48,7 +48,6 @@ dependencies {
compile "io.netty:netty-handler:${nettyVersion}"
compile "io.netty:netty-transport-native-epoll:${nettyVersion}"
compile "io.netty:netty-transport-native-epoll:${nettyVersion}:linux-x86_64"
compile "io.netty:netty-transport-native-kqueue:${nettyVersion}:osx-x86_64"
compile "io.netty:netty-resolver-dns:${nettyVersion}"
compile "org.apache.logging.log4j:log4j-api:${log4jVersion}"
@@ -57,14 +56,18 @@ dependencies {
compile "org.apache.logging.log4j:log4j-iostreams:${log4jVersion}"
compile 'net.sf.jopt-simple:jopt-simple:5.0.4' // command-line options
compile 'net.minecrell:terminalconsoleappender:1.1.1'
runtime 'net.java.dev.jna:jna:4.5.2' // Needed for JLine
compile 'net.minecrell:terminalconsoleappender:1.2.0'
runtime 'org.jline:jline-terminal-jansi:3.12.1' // Needed for JLine
runtime 'com.lmax:disruptor:3.4.2' // Async loggers
compile 'it.unimi.dsi:fastutil:8.2.2'
compile 'it.unimi.dsi:fastutil:8.2.3'
compile 'net.kyori:event-method-asm:3.0.0'
compile 'com.mojang:brigadier:1.0.15'
compile 'org.asynchttpclient:async-http-client:2.10.1'
compile 'com.spotify:completable-futures:0.3.2'
testCompile "org.junit.jupiter:junit-jupiter-api:${junitVersion}"
testCompile "org.junit.jupiter:junit-jupiter-engine:${junitVersion}"

View File

@@ -3,12 +3,9 @@ package com.velocitypowered.proxy;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.velocitypowered.proxy.config.VelocityConfiguration;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufOutputStream;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.http.HttpHeaderNames;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
@@ -21,11 +18,14 @@ import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.GZIPOutputStream;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.asynchttpclient.ListenableFuture;
import org.asynchttpclient.Response;
/**
* bStats collects some data for plugin authors.
@@ -185,40 +185,44 @@ public class Metrics {
}
// Compress the data to save bandwidth
ByteBuf reqBody = createResponseBody(data);
server.getHttpClient().post(new URL(URL), reqBody, request -> {
request.headers().add(HttpHeaderNames.CONTENT_ENCODING, "gzip");
request.headers().add(HttpHeaderNames.ACCEPT, "application/json");
request.headers().add(HttpHeaderNames.CONTENT_TYPE, "application/json");
})
.whenCompleteAsync((resp, exc) -> {
if (logFailedRequests) {
if (exc != null) {
logger.error("Unable to send metrics to bStats", exc);
} else if (resp.getCode() != 429) {
logger.error("Got HTTP status code {} when sending metrics to bStats",
resp.getCode());
}
ListenableFuture<Response> future = server.getAsyncHttpClient()
.preparePost(URL)
.addHeader(HttpHeaderNames.CONTENT_ENCODING, "gzip")
.addHeader(HttpHeaderNames.ACCEPT, "application/json")
.addHeader(HttpHeaderNames.CONTENT_TYPE, "application/json")
.setBody(createResponseBody(data))
.execute();
future.addListener(() -> {
if (logFailedRequests) {
try {
Response r = future.get();
if (r.getStatusCode() != 429) {
logger.error("Got HTTP status code {} when sending metrics to bStats",
r.getStatusCode());
}
});
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
logger.error("Unable to send metrics to bStats", e);
}
}
}, null);
}
private static ByteBuf createResponseBody(JsonObject object) throws IOException {
ByteBuf buf = Unpooled.buffer();
private static byte[] createResponseBody(JsonObject object) throws IOException {
ByteArrayOutputStream os = new ByteArrayOutputStream();
try (Writer writer =
new BufferedWriter(
new OutputStreamWriter(
new GZIPOutputStream(new ByteBufOutputStream(buf)), StandardCharsets.UTF_8
new GZIPOutputStream(os), StandardCharsets.UTF_8
)
)
) {
VelocityServer.GSON.toJson(object, writer);
} catch (IOException e) {
buf.release();
throw e;
}
return buf;
return os.toByteArray();
}
/**

View File

@@ -30,7 +30,6 @@ import com.velocitypowered.proxy.config.VelocityConfiguration;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.console.VelocityConsole;
import com.velocitypowered.proxy.network.ConnectionManager;
import com.velocitypowered.proxy.network.http.NettyHttpClient;
import com.velocitypowered.proxy.plugin.VelocityEventManager;
import com.velocitypowered.proxy.plugin.VelocityPluginManager;
import com.velocitypowered.proxy.protocol.packet.Chat;
@@ -70,12 +69,15 @@ import java.util.function.IntFunction;
import java.util.stream.Collectors;
import net.kyori.text.Component;
import net.kyori.text.TextComponent;
import net.kyori.text.TranslatableComponent;
import net.kyori.text.serializer.gson.GsonComponentSerializer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.asynchttpclient.AsyncHttpClient;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
public class VelocityServer implements ProxyServer {
@@ -89,7 +91,6 @@ public class VelocityServer implements ProxyServer {
private final ConnectionManager cm;
private final ProxyOptions options;
private @MonotonicNonNull VelocityConfiguration configuration;
private @MonotonicNonNull NettyHttpClient httpClient;
private @MonotonicNonNull KeyPair serverKeyPair;
private final ServerMap servers;
private final VelocityCommandManager commandManager = new VelocityCommandManager();
@@ -201,7 +202,6 @@ public class VelocityServer implements ProxyServer {
}
ipAttemptLimiter = Ratelimiters.createWithMilliseconds(configuration.getLoginRatelimit());
httpClient = new NettyHttpClient(this);
loadPlugins();
// Go ahead and fire the proxy initialization event. We block since plugins should have a chance
@@ -264,15 +264,7 @@ public class VelocityServer implements ProxyServer {
logger.info("Loaded {} plugins", pluginManager.getPlugins().size());
}
public EventLoopGroup getWorkerGroup() {
return this.cm.getWorkerGroup();
}
public Bootstrap initializeGenericBootstrap() {
return this.cm.createWorker();
}
public Bootstrap initializeGenericBootstrap(EventLoopGroup group) {
public Bootstrap createBootstrap(@Nullable EventLoopGroup group) {
return this.cm.createWorker(group);
}
@@ -433,8 +425,8 @@ public class VelocityServer implements ProxyServer {
thread.start();
}
public NettyHttpClient getHttpClient() {
return ensureInitialized(httpClient);
public AsyncHttpClient getAsyncHttpClient() {
return ensureInitialized(cm).getHttpClient();
}
public Ratelimiter getIpAttemptLimiter() {
@@ -454,6 +446,9 @@ public class VelocityServer implements ProxyServer {
* @return {@code true} if we can register the connection, {@code false} if not
*/
public boolean canRegisterConnection(ConnectedPlayer connection) {
if (configuration.isOnlineMode() && configuration.isOnlineModeKickExistingPlayers()) {
return true;
}
String lowerName = connection.getUsername().toLowerCase(Locale.US);
return !(connectionsByName.containsKey(lowerName)
|| connectionsByUuid.containsKey(connection.getUniqueId()));
@@ -466,12 +461,24 @@ public class VelocityServer implements ProxyServer {
*/
public boolean registerConnection(ConnectedPlayer connection) {
String lowerName = connection.getUsername().toLowerCase(Locale.US);
if (connectionsByName.putIfAbsent(lowerName, connection) != null) {
return false;
}
if (connectionsByUuid.putIfAbsent(connection.getUniqueId(), connection) != null) {
connectionsByName.remove(lowerName, connection);
return false;
if (!this.configuration.isOnlineModeKickExistingPlayers()) {
if (connectionsByName.putIfAbsent(lowerName, connection) != null) {
return false;
}
if (connectionsByUuid.putIfAbsent(connection.getUniqueId(), connection) != null) {
connectionsByName.remove(lowerName, connection);
return false;
}
} else {
ConnectedPlayer existing = connectionsByUuid.get(connection.getUniqueId());
if (existing != null) {
existing.disconnect(TranslatableComponent.of("multiplayer.disconnect.duplicate_login"));
}
// We can now replace the entries as needed.
connectionsByName.put(lowerName, connection);
connectionsByUuid.put(connection.getUniqueId(), connection);
}
return true;
}
@@ -498,7 +505,7 @@ public class VelocityServer implements ProxyServer {
Preconditions.checkNotNull(component, "component");
Chat chat = Chat.createClientbound(component);
for (ConnectedPlayer player : connectionsByUuid.values()) {
player.getMinecraftConnection().write(chat);
player.getConnection().write(chat);
}
}

View File

@@ -0,0 +1,7 @@
package com.velocitypowered.proxy.config;
public enum PingPassthroughMode {
DISABLED,
MODS,
ALL
}

View File

@@ -72,11 +72,36 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
@ConfigKey("forwarding-secret")
private byte[] forwardingSecret = generateRandomString(12).getBytes(StandardCharsets.UTF_8);
@Comment({"Announce whether or not your server supports Forge. If you run a modded server, we",
"suggest turning this on."})
@Comment({
"Announce whether or not your server supports Forge. If you run a modded server, we",
"suggest turning this on.",
"",
"If your network runs one modpack consistently, consider using ping-passthrough = \"mods\"",
"instead for a nicer display in the server list."
})
@ConfigKey("announce-forge")
private boolean announceForge = false;
@Comment({"If enabled (default is false) and the proxy is in online mode, Velocity will kick",
"any existing player who is online if a duplicate connection attempt is made."})
@ConfigKey("kick-existing-players")
private boolean onlineModeKickExistingPlayers = false;
@Comment({
"Should Velocity pass server list ping requests to a backend server?",
"Available options:",
"- \"disabled\": No pass-through will be done. The velocity.toml and server-icon.png",
" will determine the initial server list ping response.",
"- \"mods\": Passes only the mod list from your backend server into the response.",
" The first server in your try list (or forced host) with a mod list will be",
" used. If no backend servers can be contacted, Velocity will not display any",
" mod information.",
"- \"all\": Passes everything from the backend server into the response. The Velocity",
" configuration is used if no servers could be contacted."
})
@ConfigKey("ping-passthrough")
private PingPassthroughMode pingPassthrough;
@Table("[servers]")
private final Servers servers;
@@ -109,7 +134,8 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
private VelocityConfiguration(String bind, String motd, int showMaxPlayers, boolean onlineMode,
boolean announceForge, PlayerInfoForwarding playerInfoForwardingMode, byte[] forwardingSecret,
Servers servers, ForcedHosts forcedHosts, Advanced advanced, Query query, Metrics metrics) {
boolean onlineModeKickExistingPlayers, PingPassthroughMode pingPassthrough, Servers servers,
ForcedHosts forcedHosts, Advanced advanced, Query query, Metrics metrics) {
this.bind = bind;
this.motd = motd;
this.showMaxPlayers = showMaxPlayers;
@@ -117,6 +143,8 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
this.announceForge = announceForge;
this.playerInfoForwardingMode = playerInfoForwardingMode;
this.forwardingSecret = forwardingSecret;
this.onlineModeKickExistingPlayers = onlineModeKickExistingPlayers;
this.pingPassthrough = pingPassthrough;
this.servers = servers;
this.forcedHosts = forcedHosts;
this.advanced = advanced;
@@ -365,10 +393,18 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
return advanced.isProxyProtocol();
}
public boolean useTcpFastOpen() {
return advanced.tcpFastOpen;
}
public Metrics getMetrics() {
return metrics;
}
public PingPassthroughMode getPingPassthrough() {
return pingPassthrough;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
@@ -416,6 +452,8 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
String forwardingModeName = toml.getString("player-info-forwarding-mode", "MODERN")
.toUpperCase(Locale.US);
String passThroughName = toml.getString("ping-passthrough", "DISABLED")
.toUpperCase(Locale.US);
return new VelocityConfiguration(
toml.getString("bind", "0.0.0.0:25577"),
@@ -425,6 +463,8 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
toml.getBoolean("announce-forge", false),
PlayerInfoForwarding.valueOf(forwardingModeName),
forwardingSecret,
toml.getBoolean("kick-existing-players", false),
PingPassthroughMode.valueOf(passThroughName),
servers,
forcedHosts,
advanced,
@@ -443,6 +483,10 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
return builder.toString();
}
public boolean isOnlineModeKickExistingPlayers() {
return onlineModeKickExistingPlayers;
}
private static class Servers {
@IsMap
@@ -596,6 +640,10 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
@ConfigKey("proxy-protocol")
private boolean proxyProtocol = false;
@Comment("Enables TCP fast open support on the proxy. Requires the proxy to run on Linux.")
@ConfigKey("tcp-fast-open")
private boolean tcpFastOpen = false;
private Advanced() {
}
@@ -607,6 +655,7 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
this.connectionTimeout = toml.getLong("connection-timeout", 5000L).intValue();
this.readTimeout = toml.getLong("read-timeout", 30000L).intValue();
this.proxyProtocol = toml.getBoolean("proxy-protocol", false);
this.tcpFastOpen = toml.getBoolean("tcp-fast-open", false);
}
}
@@ -634,6 +683,10 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
return proxyProtocol;
}
public boolean isTcpFastOpen() {
return tcpFastOpen;
}
@Override
public String toString() {
return "Advanced{"
@@ -643,6 +696,7 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
+ ", connectionTimeout=" + connectionTimeout
+ ", readTimeout=" + readTimeout
+ ", proxyProtocol=" + proxyProtocol
+ ", tcpFastOpen=" + tcpFastOpen
+ '}';
}
}

View File

@@ -155,6 +155,10 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
}
}
private void ensureInEventLoop() {
Preconditions.checkState(this.channel.eventLoop().inEventLoop(), "Not in event loop");
}
public EventLoop eventLoop() {
return channel.eventLoop();
}
@@ -233,6 +237,8 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
* @param autoReading whether or not we should read data automatically
*/
public void setAutoReading(boolean autoReading) {
ensureInEventLoop();
channel.config().setAutoRead(autoReading);
if (autoReading) {
// For some reason, the channel may not completely read its queued contents once autoread
@@ -249,6 +255,8 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
* @param state the new state
*/
public void setState(StateRegistry state) {
ensureInEventLoop();
this.state = state;
this.channel.pipeline().get(MinecraftEncoder.class).setState(state);
this.channel.pipeline().get(MinecraftDecoder.class).setState(state);
@@ -263,6 +271,8 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
* @param protocolVersion the protocol version to use
*/
public void setProtocolVersion(ProtocolVersion protocolVersion) {
ensureInEventLoop();
this.protocolVersion = protocolVersion;
this.nextProtocolVersion = protocolVersion;
if (protocolVersion != ProtocolVersion.LEGACY) {
@@ -284,6 +294,8 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
* @param sessionHandler the handler to use
*/
public void setSessionHandler(MinecraftSessionHandler sessionHandler) {
ensureInEventLoop();
if (this.sessionHandler != null) {
this.sessionHandler.deactivated();
}
@@ -302,6 +314,7 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
*/
public void setCompressionThreshold(int threshold) {
ensureOpen();
ensureInEventLoop();
if (threshold == -1) {
channel.pipeline().remove(COMPRESSION_DECODER);
@@ -325,6 +338,7 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
*/
public void enableEncryption(byte[] secret) throws GeneralSecurityException {
ensureOpen();
ensureInEventLoop();
SecretKey key = new SecretKeySpec(secret, "AES");
@@ -342,6 +356,7 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
}
public void setAssociation(MinecraftConnectionAssociation association) {
ensureInEventLoop();
this.association = association;
}

View File

@@ -36,7 +36,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
BackendPlaySessionHandler(VelocityServer server, VelocityServerConnection serverConn) {
this.server = server;
this.serverConn = serverConn;
this.playerConnection = serverConn.getPlayer().getMinecraftConnection();
this.playerConnection = serverConn.getPlayer().getConnection();
MinecraftSessionHandler psh = playerConnection.getSessionHandler();
if (!(psh instanceof ClientPlaySessionHandler)) {
@@ -190,7 +190,8 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
public void disconnected() {
serverConn.getServer().removePlayer(serverConn.getPlayer());
if (!serverConn.isGracefulDisconnect() && !exceptionTriggered) {
serverConn.getPlayer().disconnect(ConnectionMessages.UNEXPECTED_DISCONNECT);
serverConn.getPlayer().handleConnectionException(serverConn.getServer(),
Disconnect.create(ConnectionMessages.UNEXPECTED_DISCONNECT), true);
}
}
}

View File

@@ -117,32 +117,28 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
private static ByteBuf createForwardingData(byte[] hmacSecret, String address,
GameProfile profile) {
ByteBuf dataToForward = Unpooled.buffer();
ByteBuf finalData = Unpooled.buffer();
ByteBuf forwarded = Unpooled.buffer(2048);
try {
ProtocolUtils.writeVarInt(dataToForward, VelocityConstants.FORWARDING_VERSION);
ProtocolUtils.writeString(dataToForward, address);
ProtocolUtils.writeUuid(dataToForward, profile.getId());
ProtocolUtils.writeString(dataToForward, profile.getName());
ProtocolUtils.writeProperties(dataToForward, profile.getProperties());
ProtocolUtils.writeVarInt(forwarded, VelocityConstants.FORWARDING_VERSION);
ProtocolUtils.writeString(forwarded, address);
ProtocolUtils.writeUuid(forwarded, profile.getId());
ProtocolUtils.writeString(forwarded, profile.getName());
ProtocolUtils.writeProperties(forwarded, profile.getProperties());
SecretKey key = new SecretKeySpec(hmacSecret, "HmacSHA256");
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(key);
mac.update(dataToForward.array(), dataToForward.arrayOffset(), dataToForward.readableBytes());
mac.update(forwarded.array(), forwarded.arrayOffset(), forwarded.readableBytes());
byte[] sig = mac.doFinal();
finalData.writeBytes(sig);
finalData.writeBytes(dataToForward);
return finalData;
return Unpooled.wrappedBuffer(Unpooled.wrappedBuffer(sig), forwarded);
} catch (InvalidKeyException e) {
finalData.release();
forwarded.release();
throw new RuntimeException("Unable to authenticate data", e);
} catch (NoSuchAlgorithmException e) {
// Should never happen
finalData.release();
forwarded.release();
throw new AssertionError(e);
} finally {
dataToForward.release();
}
}
}

View File

@@ -84,13 +84,13 @@ public class TransitionSessionHandler implements MinecraftSessionHandler {
.whenCompleteAsync((x, error) -> {
// Strap on the ClientPlaySessionHandler if required.
ClientPlaySessionHandler playHandler;
if (serverConn.getPlayer().getMinecraftConnection().getSessionHandler()
if (serverConn.getPlayer().getConnection().getSessionHandler()
instanceof ClientPlaySessionHandler) {
playHandler = (ClientPlaySessionHandler) serverConn.getPlayer().getMinecraftConnection()
playHandler = (ClientPlaySessionHandler) serverConn.getPlayer().getConnection()
.getSessionHandler();
} else {
playHandler = new ClientPlaySessionHandler(server, serverConn.getPlayer());
serverConn.getPlayer().getMinecraftConnection().setSessionHandler(playHandler);
serverConn.getPlayer().getConnection().setSessionHandler(playHandler);
}
playHandler.handleBackendJoinGame(packet, serverConn);
@@ -167,7 +167,7 @@ public class TransitionSessionHandler implements MinecraftSessionHandler {
return true;
}
serverConn.getPlayer().getMinecraftConnection().write(packet.retain());
serverConn.getPlayer().getConnection().write(packet.retain());
return true;
}

View File

@@ -12,7 +12,6 @@ import static com.velocitypowered.proxy.network.Connections.READ_TIMEOUT;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.ConnectionRequestBuilder;
import com.velocitypowered.api.proxy.ServerConnection;
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
import com.velocitypowered.api.proxy.server.ServerInfo;
@@ -77,7 +76,7 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
CompletableFuture<Impl> result = new CompletableFuture<>();
// Note: we use the event loop for the connection the player is on. This reduces context
// switches.
server.initializeGenericBootstrap(proxyPlayer.getMinecraftConnection().eventLoop())
server.createBootstrap(proxyPlayer.getConnection().eventLoop())
.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
@@ -139,23 +138,24 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
PlayerInfoForwarding forwardingMode = server.getConfiguration().getPlayerInfoForwardingMode();
// Initiate the handshake.
ProtocolVersion protocolVersion = proxyPlayer.getMinecraftConnection().getNextProtocolVersion();
ProtocolVersion protocolVersion = proxyPlayer.getConnection().getNextProtocolVersion();
Handshake handshake = new Handshake();
handshake.setNextStatus(StateRegistry.LOGIN_ID);
handshake.setProtocolVersion(protocolVersion);
if (forwardingMode == PlayerInfoForwarding.LEGACY) {
handshake.setServerAddress(createLegacyForwardingAddress());
} else if (proxyPlayer.getMinecraftConnection().getType() == ConnectionTypes.LEGACY_FORGE) {
} else if (proxyPlayer.getConnection().getType() == ConnectionTypes.LEGACY_FORGE) {
handshake.setServerAddress(handshake.getServerAddress() + HANDSHAKE_HOSTNAME_TOKEN);
} else {
handshake.setServerAddress(registeredServer.getServerInfo().getAddress().getHostString());
}
handshake.setPort(registeredServer.getServerInfo().getAddress().getPort());
mc.write(handshake);
mc.delayedWrite(handshake);
mc.setProtocolVersion(protocolVersion);
mc.setState(StateRegistry.LOGIN);
mc.write(new ServerLogin(proxyPlayer.getUsername()));
mc.delayedWrite(new ServerLogin(proxyPlayer.getUsername()));
mc.flush();
}
public @Nullable MinecraftConnection getConnection() {

View File

@@ -7,6 +7,7 @@ import static com.velocitypowered.proxy.protocol.util.PluginMessageUtil.construc
import com.velocitypowered.api.event.connection.PluginMessageEvent;
import com.velocitypowered.api.event.player.PlayerChatEvent;
import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent;
import com.velocitypowered.api.event.player.TabCompleteEvent;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
import com.velocitypowered.proxy.VelocityServer;
@@ -59,7 +60,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
private final List<UUID> serverBossBars = new ArrayList<>();
private final Queue<PluginMessage> loginPluginMessages = new ArrayDeque<>();
private final VelocityServer server;
private @Nullable TabCompleteRequest legacyCommandTabComplete;
private @Nullable TabCompleteRequest outstandingTabComplete;
/**
* Constructs a client play session handler.
@@ -77,7 +78,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
.getProtocolVersion());
if (!channels.isEmpty()) {
PluginMessage register = constructChannelsPacket(player.getProtocolVersion(), channels);
player.getMinecraftConnection().write(register);
player.getConnection().write(register);
player.getKnownChannels().addAll(channels);
}
}
@@ -155,61 +156,11 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
public boolean handle(TabCompleteRequest packet) {
boolean isCommand = !packet.isAssumeCommand() && packet.getCommand().startsWith("/");
if (!isCommand) {
// We can't deal with anything else.
return false;
}
// In 1.13+, we need to do additional work for the richer suggestions available.
String command = packet.getCommand().substring(1);
int spacePos = command.indexOf(' ');
if (spacePos == -1) {
return false;
}
String commandLabel = command.substring(0, spacePos);
if (!server.getCommandManager().hasCommand(commandLabel)) {
if (player.getProtocolVersion().compareTo(MINECRAFT_1_13) < 0) {
// Outstanding tab completes are recorded for use with 1.12 clients and below to provide
// tab list completion support for command names. In 1.13, Brigadier handles everything for
// us.
legacyCommandTabComplete = packet;
}
return false;
}
List<String> suggestions = server.getCommandManager().offerSuggestions(player, command);
if (suggestions.isEmpty()) {
return false;
}
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;
if (isCommand) {
return this.handleCommandTabComplete(packet);
} else {
length = packet.getCommand().length() - startPos;
return this.handleRegularTabComplete(packet);
}
resp.setStart(startPos);
resp.setLength(length);
resp.getOffers().addAll(offers);
player.getMinecraftConnection().write(resp);
return true;
}
@Override
@@ -325,7 +276,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
public void writabilityChanged() {
VelocityServerConnection serverConn = player.getConnectedServer();
if (serverConn != null) {
boolean writable = player.getMinecraftConnection().getChannel().isWritable();
boolean writable = player.getConnection().getChannel().isWritable();
MinecraftConnection smc = serverConn.getConnection();
if (smc != null) {
smc.setAutoReading(writable);
@@ -345,7 +296,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
if (!spawned) {
// Nothing special to do with regards to spawning the player
spawned = true;
player.getMinecraftConnection().delayedWrite(joinGame);
player.getConnection().delayedWrite(joinGame);
// Required for Legacy Forge
player.getPhase().onFirstJoin(player);
@@ -365,12 +316,12 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
// 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.
player.getMinecraftConnection().delayedWrite(joinGame);
player.getConnection().delayedWrite(joinGame);
int tempDim = joinGame.getDimension() == 0 ? -1 : 0;
player.getMinecraftConnection().delayedWrite(
player.getConnection().delayedWrite(
new Respawn(tempDim, joinGame.getDifficulty(), joinGame.getGamemode(),
joinGame.getLevelType()));
player.getMinecraftConnection().delayedWrite(
player.getConnection().delayedWrite(
new Respawn(joinGame.getDimension(), joinGame.getDifficulty(), joinGame.getGamemode(),
joinGame.getLevelType()));
}
@@ -381,7 +332,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
BossBar deletePacket = new BossBar();
deletePacket.setUuid(serverBossBar);
deletePacket.setAction(BossBar.REMOVE);
player.getMinecraftConnection().delayedWrite(deletePacket);
player.getConnection().delayedWrite(deletePacket);
}
serverBossBars.clear();
@@ -399,12 +350,12 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
// Clear any title from the previous server.
if (player.getProtocolVersion().compareTo(MINECRAFT_1_8) >= 0) {
player.getMinecraftConnection()
player.getConnection()
.delayedWrite(TitlePacket.resetForProtocolVersion(player.getProtocolVersion()));
}
// Flush everything
player.getMinecraftConnection().flush();
player.getConnection().flush();
serverMc.flush();
destination.completeJoin();
}
@@ -413,29 +364,116 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
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);
int spacePos = command.indexOf(' ');
if (spacePos == -1) {
spacePos = command.length();
}
String commandLabel = command.substring(0, spacePos);
if (!server.getCommandManager().hasCommand(commandLabel)) {
if (player.getProtocolVersion().compareTo(MINECRAFT_1_13) < 0) {
// Outstanding tab completes are recorded for use with 1.12 clients and below to provide
// additional tab completion support.
outstandingTabComplete = packet;
}
return false;
}
List<String> suggestions = server.getCommandManager().offerSuggestions(player, command);
if (suggestions.isEmpty()) {
return false;
}
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;
}
resp.setStart(startPos);
resp.setLength(length);
resp.getOffers().addAll(offers);
player.getConnection().write(resp);
return true;
}
private boolean handleRegularTabComplete(TabCompleteRequest packet) {
if (player.getProtocolVersion().compareTo(MINECRAFT_1_13) < 0) {
// Outstanding tab completes are recorded for use with 1.12 clients and below to provide
// additional tab completion support.
outstandingTabComplete = packet;
}
return false;
}
/**
* Handles additional tab complete for 1.12 and lower clients.
* Handles additional tab complete.
*
* @param response the tab complete response from the backend
*/
public void handleTabCompleteResponse(TabCompleteResponse response) {
if (legacyCommandTabComplete != null) {
String command = legacyCommandTabComplete.getCommand().substring(1);
try {
List<String> offers = server.getCommandManager().offerSuggestions(player, command);
for (String offer : offers) {
response.getOffers().add(new Offer(offer, null));
}
response.getOffers().sort(null);
} catch (Exception e) {
logger.error("Unable to provide tab list completions for {} for command '{}'",
player.getUsername(),
command, e);
if (outstandingTabComplete != null) {
if (outstandingTabComplete.isAssumeCommand()) {
return; // used for command blocks which can't run Velocity commands anyway
}
legacyCommandTabComplete = null;
if (outstandingTabComplete.getCommand().startsWith("/")) {
this.finishCommandTabComplete(outstandingTabComplete, response);
} else {
this.finishRegularTabComplete(outstandingTabComplete, response);
}
outstandingTabComplete = null;
}
}
player.getMinecraftConnection().write(response);
private void finishCommandTabComplete(TabCompleteRequest request, TabCompleteResponse response) {
String command = request.getCommand().substring(1);
try {
List<String> offers = server.getCommandManager().offerSuggestions(player, command);
for (String offer : offers) {
response.getOffers().add(new Offer(offer, null));
}
response.getOffers().sort(null);
player.getConnection().write(response);
} catch (Exception e) {
logger.error("Unable to provide tab list completions for {} for command '{}'",
player.getUsername(),
command, e);
}
}
private void finishRegularTabComplete(TabCompleteRequest request, TabCompleteResponse response) {
List<String> offers = new ArrayList<>();
for (Offer offer : response.getOffers()) {
offers.add(offer.getText());
}
server.getEventManager().fire(new TabCompleteEvent(player, request.getCommand(), offers))
.thenAcceptAsync(e -> {
response.getOffers().clear();
for (String s : e.getSuggestions()) {
response.getOffers().add(new Offer(s));
}
player.getConnection().write(response);
}, player.getConnection().eventLoop());
}
/**

View File

@@ -59,7 +59,6 @@ import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ThreadLocalRandom;
import net.kyori.text.Component;
import net.kyori.text.TextComponent;
@@ -85,7 +84,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
/**
* The actual Minecraft connection. This is actually a wrapper object around the Netty channel.
*/
private final MinecraftConnection minecraftConnection;
private final MinecraftConnection connection;
private final @Nullable InetSocketAddress virtualHost;
private GameProfile profile;
private PermissionFunction permissionFunction;
@@ -104,18 +103,18 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
private @MonotonicNonNull List<String> serversToTry = null;
ConnectedPlayer(VelocityServer server, GameProfile profile,
MinecraftConnection minecraftConnection, @Nullable InetSocketAddress virtualHost) {
MinecraftConnection connection, @Nullable InetSocketAddress virtualHost) {
this.server = server;
if (minecraftConnection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) {
this.tabList = new VelocityTabList(minecraftConnection);
if (connection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) {
this.tabList = new VelocityTabList(connection);
} else {
this.tabList = new VelocityTabListLegacy(minecraftConnection);
this.tabList = new VelocityTabListLegacy(connection);
}
this.profile = profile;
this.minecraftConnection = minecraftConnection;
this.connection = connection;
this.virtualHost = virtualHost;
this.permissionFunction = PermissionFunction.ALWAYS_UNDEFINED;
this.connectionPhase = minecraftConnection.getType().getInitialClientPhase();
this.connectionPhase = connection.getType().getInitialClientPhase();
this.knownChannels = CappedSet.create(MAX_PLUGIN_CHANNELS);
}
@@ -139,8 +138,8 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
return profile;
}
public MinecraftConnection getMinecraftConnection() {
return minecraftConnection;
public MinecraftConnection getConnection() {
return connection;
}
@Override
@@ -175,7 +174,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
@Override
public InetSocketAddress getRemoteAddress() {
return (InetSocketAddress) minecraftConnection.getRemoteAddress();
return (InetSocketAddress) connection.getRemoteAddress();
}
@Override
@@ -189,12 +188,12 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
@Override
public boolean isActive() {
return minecraftConnection.getChannel().isActive();
return connection.getChannel().isActive();
}
@Override
public ProtocolVersion getProtocolVersion() {
return minecraftConnection.getProtocolVersion();
return connection.getProtocolVersion();
}
@Override
@@ -210,7 +209,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
TitlePacket pkt = new TitlePacket();
pkt.setAction(TitlePacket.SET_ACTION_BAR);
pkt.setComponent(GsonComponentSerializer.INSTANCE.serialize(component));
minecraftConnection.write(pkt);
connection.write(pkt);
return;
} else {
// Due to issues with action bar packets, we'll need to convert the text message into a
@@ -226,7 +225,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
Chat chat = new Chat();
chat.setType(pos);
chat.setMessage(json);
minecraftConnection.write(chat);
connection.write(chat);
}
@Override
@@ -263,23 +262,23 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
public void disconnect(Component reason) {
logger.info("{} has disconnected: {}", this,
LegacyComponentSerializer.legacy().serialize(reason));
minecraftConnection.closeWith(Disconnect.create(reason));
connection.closeWith(Disconnect.create(reason));
}
@Override
public void sendTitle(Title title) {
Preconditions.checkNotNull(title, "title");
ProtocolVersion protocolVersion = minecraftConnection.getProtocolVersion();
ProtocolVersion protocolVersion = connection.getProtocolVersion();
if (title.equals(Titles.reset())) {
minecraftConnection.write(TitlePacket.resetForProtocolVersion(protocolVersion));
connection.write(TitlePacket.resetForProtocolVersion(protocolVersion));
} else if (title.equals(Titles.hide())) {
minecraftConnection.write(TitlePacket.hideForProtocolVersion(protocolVersion));
connection.write(TitlePacket.hideForProtocolVersion(protocolVersion));
} else if (title instanceof TextTitle) {
TextTitle tt = (TextTitle) title;
if (tt.isResetBeforeSend()) {
minecraftConnection.delayedWrite(TitlePacket.resetForProtocolVersion(protocolVersion));
connection.delayedWrite(TitlePacket.resetForProtocolVersion(protocolVersion));
}
Optional<Component> titleText = tt.getTitle();
@@ -287,7 +286,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
TitlePacket titlePkt = new TitlePacket();
titlePkt.setAction(TitlePacket.SET_TITLE);
titlePkt.setComponent(GsonComponentSerializer.INSTANCE.serialize(titleText.get()));
minecraftConnection.delayedWrite(titlePkt);
connection.delayedWrite(titlePkt);
}
Optional<Component> subtitleText = tt.getSubtitle();
@@ -295,7 +294,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
TitlePacket titlePkt = new TitlePacket();
titlePkt.setAction(TitlePacket.SET_SUBTITLE);
titlePkt.setComponent(GsonComponentSerializer.INSTANCE.serialize(subtitleText.get()));
minecraftConnection.delayedWrite(titlePkt);
connection.delayedWrite(titlePkt);
}
if (tt.areTimesSet()) {
@@ -303,9 +302,9 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
timesPkt.setFadeIn(tt.getFadeIn());
timesPkt.setStay(tt.getStay());
timesPkt.setFadeOut(tt.getFadeOut());
minecraftConnection.delayedWrite(timesPkt);
connection.delayedWrite(timesPkt);
}
minecraftConnection.flush();
connection.flush();
} else {
throw new IllegalArgumentException("Unknown title class " + title.getClass().getName());
}
@@ -457,9 +456,9 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
if (newResult == null || !newResult) {
disconnect(friendlyReason);
} else {
sendMessage(VelocityMessages.MOVED_TO_NEW_SERVER);
sendMessage(VelocityMessages.MOVED_TO_NEW_SERVER.append(friendlyReason));
}
}, minecraftConnection.eventLoop());
}, connection.eventLoop());
} else if (event.getResult() instanceof Notify) {
Notify res = (Notify) event.getResult();
if (event.kickedDuringServerConnect()) {
@@ -471,7 +470,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
// In case someone gets creative, assume we want to disconnect the player.
disconnect(friendlyReason);
}
}, minecraftConnection.eventLoop());
}, connection.eventLoop());
}
/**
@@ -584,7 +583,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
Preconditions.checkNotNull(identifier, "identifier");
Preconditions.checkNotNull(data, "data");
PluginMessage message = new PluginMessage(identifier.getId(), Unpooled.wrappedBuffer(data));
minecraftConnection.write(message);
connection.write(message);
return true;
}
@@ -603,7 +602,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
ResourcePackRequest request = new ResourcePackRequest();
request.setUrl(url);
request.setHash("");
minecraftConnection.write(request);
connection.write(request);
}
@Override
@@ -615,7 +614,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
ResourcePackRequest request = new ResourcePackRequest();
request.setUrl(url);
request.setHash(ByteBufUtil.hexDump(hash));
minecraftConnection.write(request);
connection.write(request);
}
/**
@@ -624,10 +623,10 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
* ID last sent by the server.
*/
public void sendKeepAlive() {
if (minecraftConnection.getState() == StateRegistry.PLAY) {
if (connection.getState() == StateRegistry.PLAY) {
KeepAlive keepAlive = new KeepAlive();
keepAlive.setRandomId(ThreadLocalRandom.current().nextLong());
minecraftConnection.write(keepAlive);
connection.write(keepAlive);
}
}
@@ -751,8 +750,10 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
if (status != null && !status.isSafe()) {
// If it's not safe to continue the connection we need to shut it down.
handleConnectionException(status.getAttemptedConnection(), throwable, true);
} else if ((status != null && !status.isSuccessful())) {
resetInFlightConnection();
}
})
}, connection.eventLoop())
.thenApply(x -> x);
}
@@ -785,7 +786,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
// The only remaining value is successful (no need to do anything!)
break;
}
}, minecraftConnection.eventLoop())
}, connection.eventLoop())
.thenApply(Result::isSuccessful);
}

View File

@@ -8,6 +8,8 @@ import static com.velocitypowered.proxy.connection.VelocityConstants.EMPTY_BYTE_
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;
@@ -43,10 +45,14 @@ import java.security.KeyPair;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
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;
public class LoginSessionHandler implements MinecraftSessionHandler {
@@ -124,46 +130,50 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
String url = String.format(MOJANG_HASJOINED_URL,
urlFormParameterEscaper().escape(login.getUsername()), serverId,
urlFormParameterEscaper().escape(playerIp));
server.getHttpClient()
.get(new URL(url), mcConnection.eventLoop())
.thenAcceptAsync(profileResponse -> {
if (mcConnection.isClosed()) {
// The player disconnected after we authenticated them.
return;
}
// Go ahead and enable encryption. Once the client sends EncryptionResponse, encryption
// is enabled.
try {
mcConnection.enableEncryption(decryptedSharedSecret);
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
ListenableFuture<Response> hasJoinedResponse = server.getAsyncHttpClient().prepareGet(url)
.execute();
hasJoinedResponse.addListener(() -> {
if (mcConnection.isClosed()) {
// The player disconnected after we authenticated them.
return;
}
if (profileResponse.getCode() == 200) {
// All went well, initialize the session.
initializePlayer(GSON.fromJson(profileResponse.getBody(), GameProfile.class), true);
} else if (profileResponse.getCode() == 204) {
// Apparently an offline-mode user logged onto this online-mode proxy.
inbound.disconnect(VelocityMessages.ONLINE_MODE_ONLY);
} else {
// Something else went wrong
logger.error(
"Got an unexpected error code {} whilst contacting Mojang to log in {} ({})",
profileResponse.getCode(), login.getUsername(), playerIp);
mcConnection.close();
}
}, mcConnection.eventLoop())
.exceptionally(exception -> {
logger.error("Unable to enable encryption", exception);
// Go ahead and enable encryption. Once the client sends EncryptionResponse, encryption
// is enabled.
try {
mcConnection.enableEncryption(decryptedSharedSecret);
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
try {
Response profileResponse = hasJoinedResponse.get();
if (profileResponse.getStatusCode() == 200) {
// All went well, initialize the session.
initializePlayer(GSON.fromJson(profileResponse.getResponseBody(), GameProfile.class),
true);
} else if (profileResponse.getStatusCode() == 204) {
// Apparently an offline-mode user logged onto this online-mode proxy.
inbound.disconnect(VelocityMessages.ONLINE_MODE_ONLY);
} else {
// Something else went wrong
logger.error(
"Got an unexpected error code {} whilst contacting Mojang to log in {} ({})",
profileResponse.getStatusCode(), login.getUsername(), playerIp);
mcConnection.close();
return null;
});
}
} catch (ExecutionException e) {
logger.error("Unable to authenticate with Mojang", e);
mcConnection.close();
} catch (InterruptedException e) {
// not much we can do usefully
Thread.currentThread().interrupt();
}
}, mcConnection.eventLoop());
} catch (GeneralSecurityException e) {
logger.error("Unable to enable encryption", e);
mcConnection.close();
} catch (MalformedURLException e) {
throw new AssertionError(e);
}
return true;
}
@@ -180,6 +190,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
// The player was disconnected
return;
}
PreLoginComponentResult result = event.getResult();
Optional<Component> disconnectReason = result.getReason();
if (disconnectReason.isPresent()) {
@@ -278,7 +289,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
player.disconnect(VelocityMessages.ALREADY_CONNECTED);
return;
}
mcConnection.setSessionHandler(new InitialConnectSessionHandler(player));
server.getEventManager().fire(new PostLoginEvent(player))
.thenRun(() -> player.createConnectionRequest(toTry.get()).fireAndForget());

View File

@@ -1,12 +1,15 @@
package com.velocitypowered.proxy.connection.client;
import com.google.common.collect.ImmutableList;
import com.spotify.futures.CompletableFutures;
import com.velocitypowered.api.event.proxy.ProxyPingEvent;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.InboundConnection;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import com.velocitypowered.api.proxy.server.ServerPing;
import com.velocitypowered.api.util.ModInfo;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.config.PingPassthroughMode;
import com.velocitypowered.proxy.config.VelocityConfiguration;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
@@ -15,27 +18,32 @@ import com.velocitypowered.proxy.protocol.packet.LegacyPing;
import com.velocitypowered.proxy.protocol.packet.StatusPing;
import com.velocitypowered.proxy.protocol.packet.StatusRequest;
import com.velocitypowered.proxy.protocol.packet.StatusResponse;
import com.velocitypowered.proxy.server.VelocityRegisteredServer;
import io.netty.buffer.ByteBuf;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
public class StatusSessionHandler implements MinecraftSessionHandler {
private final VelocityServer server;
private final MinecraftConnection connection;
private final InboundConnection inboundWrapper;
private final InboundConnection inbound;
StatusSessionHandler(VelocityServer server, MinecraftConnection connection,
InboundConnection inboundWrapper) {
InboundConnection inbound) {
this.server = server;
this.connection = connection;
this.inboundWrapper = inboundWrapper;
this.inbound = inbound;
}
private ServerPing createInitialPing() {
private ServerPing constructLocalPing(ProtocolVersion version) {
VelocityConfiguration configuration = server.getConfiguration();
ProtocolVersion shownVersion = ProtocolVersion.isSupported(connection.getProtocolVersion())
? connection.getProtocolVersion() : ProtocolVersion.MAXIMUM_VERSION;
return new ServerPing(
new ServerPing.Version(shownVersion.getProtocol(),
new ServerPing.Version(version.getProtocol(),
"Velocity " + ProtocolVersion.SUPPORTED_VERSION_STRING),
new ServerPing.Players(server.getPlayerCount(), configuration.getShowMaxPlayers(),
ImmutableList.of()),
@@ -45,12 +53,78 @@ public class StatusSessionHandler implements MinecraftSessionHandler {
);
}
private CompletableFuture<ServerPing> attemptPingPassthrough(PingPassthroughMode mode,
List<String> servers, ProtocolVersion pingingVersion) {
ServerPing fallback = constructLocalPing(pingingVersion);
List<CompletableFuture<ServerPing>> pings = new ArrayList<>();
for (String s : servers) {
Optional<RegisteredServer> rs = server.getServer(s);
if (!rs.isPresent()) {
continue;
}
VelocityRegisteredServer vrs = (VelocityRegisteredServer) rs.get();
pings.add(vrs.ping(connection.eventLoop(), pingingVersion));
}
if (pings.isEmpty()) {
return CompletableFuture.completedFuture(fallback);
}
CompletableFuture<List<ServerPing>> pingResponses = CompletableFutures.successfulAsList(pings,
(ex) -> fallback);
switch (mode) {
case ALL:
return pingResponses.thenApply(responses -> {
// Find the first non-fallback
for (ServerPing response : responses) {
if (response == fallback) {
continue;
}
return response;
}
return fallback;
});
case MODS:
return pingResponses.thenApply(responses -> {
// Find the first non-fallback that contains a mod list
for (ServerPing response : responses) {
if (response == fallback) {
continue;
}
Optional<ModInfo> modInfo = response.getModinfo();
if (modInfo.isPresent()) {
return fallback.asBuilder().mods(modInfo.get()).build();
}
}
return fallback;
});
default:
// Not possible, but covered for completeness.
return CompletableFuture.completedFuture(fallback);
}
}
private CompletableFuture<ServerPing> getInitialPing() {
VelocityConfiguration configuration = server.getConfiguration();
ProtocolVersion shownVersion = ProtocolVersion.isSupported(connection.getProtocolVersion())
? connection.getProtocolVersion() : ProtocolVersion.MAXIMUM_VERSION;
PingPassthroughMode passthrough = configuration.getPingPassthrough();
if (passthrough == PingPassthroughMode.DISABLED) {
return CompletableFuture.completedFuture(constructLocalPing(shownVersion));
} else {
String virtualHostStr = inbound.getVirtualHost().map(InetSocketAddress::getHostString)
.orElse("");
List<String> serversToTry = server.getConfiguration().getForcedHosts().getOrDefault(
virtualHostStr, server.getConfiguration().getAttemptConnectionOrder());
return attemptPingPassthrough(configuration.getPingPassthrough(), serversToTry, shownVersion);
}
}
@Override
public boolean handle(LegacyPing packet) {
ServerPing initialPing = createInitialPing();
ProxyPingEvent event = new ProxyPingEvent(inboundWrapper, initialPing);
server.getEventManager().fire(event)
.thenRunAsync(() -> {
getInitialPing()
.thenCompose(ping -> server.getEventManager().fire(new ProxyPingEvent(inbound, ping)))
.thenAcceptAsync(event -> {
connection.closeWith(LegacyDisconnect.fromServerPing(event.getPing(),
packet.getVersion()));
}, connection.eventLoop());
@@ -65,11 +139,10 @@ public class StatusSessionHandler implements MinecraftSessionHandler {
@Override
public boolean handle(StatusRequest packet) {
ServerPing initialPing = createInitialPing();
ProxyPingEvent event = new ProxyPingEvent(inboundWrapper, initialPing);
server.getEventManager().fire(event)
.thenRunAsync(
() -> {
getInitialPing()
.thenCompose(ping -> server.getEventManager().fire(new ProxyPingEvent(inbound, ping)))
.thenAcceptAsync(
(event) -> {
StringBuilder json = new StringBuilder();
VelocityServer.GSON.toJson(event.getPing(), json);
connection.write(new StatusResponse(json));

View File

@@ -114,7 +114,7 @@ public enum LegacyForgeHandshakeBackendPhase implements BackendConnectionPhase {
serverConnection.setConnectionPhase(newPhase);
// Write the packet to the player, we don't need it now.
player.getMinecraftConnection().write(message.retain());
player.getConnection().write(message.retain());
return true;
}

View File

@@ -135,7 +135,7 @@ public enum LegacyForgeHandshakeClientPhase implements ClientConnectionPhase {
COMPLETE(null) {
@Override
public void resetConnectionPhase(ConnectedPlayer player) {
player.getMinecraftConnection().write(LegacyForgeUtil.resetPacket());
player.getConnection().write(LegacyForgeUtil.resetPacket());
player.setPhase(LegacyForgeHandshakeClientPhase.NOT_STARTED);
}

View File

@@ -1,8 +1,12 @@
package com.velocitypowered.proxy.network;
import static org.asynchttpclient.Dsl.asyncHttpClient;
import static org.asynchttpclient.Dsl.config;
import com.google.common.base.Preconditions;
import com.velocitypowered.natives.util.Natives;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.network.netty.DnsAddressResolverGroupNameResolverAdapter;
import com.velocitypowered.proxy.protocol.netty.GS4QueryHandler;
import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
@@ -11,17 +15,26 @@ import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.WriteBufferWaterMark;
import io.netty.channel.epoll.EpollChannelOption;
import io.netty.resolver.dns.DnsAddressResolverGroup;
import io.netty.resolver.dns.DnsNameResolverBuilder;
import io.netty.util.concurrent.EventExecutor;
import java.net.InetSocketAddress;
import java.util.HashMap;
import java.util.Map;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.asynchttpclient.AsyncHttpClient;
import org.asynchttpclient.RequestBuilder;
import org.asynchttpclient.filter.FilterContext;
import org.asynchttpclient.filter.FilterContext.FilterContextBuilder;
import org.asynchttpclient.filter.FilterException;
import org.asynchttpclient.filter.RequestFilter;
import org.checkerframework.checker.nullness.qual.Nullable;
public final class ConnectionManager {
private static final WriteBufferWaterMark SERVER_WRITE_MARK = new WriteBufferWaterMark(1 << 21,
private static final WriteBufferWaterMark SERVER_WRITE_MARK = new WriteBufferWaterMark(1 << 20,
1 << 21);
private static final Logger LOGGER = LogManager.getLogger(ConnectionManager.class);
private final Map<InetSocketAddress, Channel> endpoints = new HashMap<>();
@@ -35,6 +48,7 @@ public final class ConnectionManager {
public final ServerChannelInitializerHolder serverChannelInitializer;
private final DnsAddressResolverGroup resolverGroup;
private final AsyncHttpClient httpClient;
/**
* Initalizes the {@code ConnectionManager}.
@@ -48,12 +62,26 @@ public final class ConnectionManager {
this.workerGroup = this.transportType.createEventLoopGroup(TransportType.Type.WORKER);
this.serverChannelInitializer = new ServerChannelInitializerHolder(
new ServerChannelInitializer(this.server));
this.resolverGroup = new DnsAddressResolverGroup(
new DnsNameResolverBuilder()
.channelType(this.transportType.datagramChannelClass)
.negativeTtl(15)
.ndots(1)
);
this.resolverGroup = new DnsAddressResolverGroup(new DnsNameResolverBuilder()
.channelType(this.transportType.datagramChannelClass)
.negativeTtl(15)
.ndots(1));
this.httpClient = asyncHttpClient(config()
.setEventLoopGroup(this.workerGroup)
.setUserAgent(server.getVersion().getName() + "/" + server.getVersion().getVersion())
.addRequestFilter(new RequestFilter() {
@Override
public <T> FilterContext<T> filter(FilterContext<T> ctx) throws FilterException {
return new FilterContextBuilder<>(ctx)
.request(new RequestBuilder(ctx.getRequest())
.setNameResolver(
new DnsAddressResolverGroupNameResolverAdapter(resolverGroup, workerGroup)
)
.build())
.build();
}
})
.build());
}
public void logChannelInformation() {
@@ -75,6 +103,11 @@ public final class ConnectionManager {
.childOption(ChannelOption.TCP_NODELAY, true)
.childOption(ChannelOption.IP_TOS, 0x18)
.localAddress(address);
if (transportType == TransportType.EPOLL && server.getConfiguration().useTcpFastOpen()) {
bootstrap.option(EpollChannelOption.TCP_FASTOPEN, 3);
}
bootstrap.bind()
.addListener((ChannelFutureListener) future -> {
final Channel channel = future.channel();
@@ -112,25 +145,25 @@ public final class ConnectionManager {
});
}
public Bootstrap createWorker() {
return this.createWorker(this.workerGroup);
}
/**
* Creates a TCP {@link Bootstrap} using Velocity's event loops.
*
* @param group the event loop group to use
* @param group the event loop group to use. Use {@code null} for the default worker group.
*
* @return a new {@link Bootstrap}
*/
public Bootstrap createWorker(EventLoopGroup group) {
return new Bootstrap()
public Bootstrap createWorker(@Nullable EventLoopGroup group) {
Bootstrap bootstrap = new Bootstrap()
.channel(this.transportType.socketChannelClass)
.group(group)
.option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS,
this.server.getConfiguration().getConnectTimeout())
.group(group == null ? this.workerGroup : group)
.resolver(this.resolverGroup);
if (transportType == TransportType.EPOLL && server.getConfiguration().useTcpFastOpen()) {
bootstrap.option(EpollChannelOption.TCP_FASTOPEN_CONNECT, true);
}
return bootstrap;
}
/**
@@ -164,11 +197,11 @@ public final class ConnectionManager {
return bossGroup;
}
public EventLoopGroup getWorkerGroup() {
return workerGroup;
}
public ServerChannelInitializerHolder getServerChannelInitializer() {
return this.serverChannelInitializer;
}
public AsyncHttpClient getHttpClient() {
return httpClient;
}
}

View File

@@ -7,11 +7,6 @@ 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;
@@ -27,10 +22,7 @@ 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))),
KQUEUE("Kqueue", KQueueServerSocketChannel.class, KQueueSocketChannel.class,
KQueueDatagramChannel.class,
(name, type) -> new KQueueEventLoopGroup(0, createThreadFactory(name, type)));
(name, type) -> new EpollEventLoopGroup(0, createThreadFactory(name, type)));
final String name;
final Class<? extends ServerSocketChannel> serverSocketChannelClass;
@@ -64,10 +56,12 @@ enum TransportType {
}
public static TransportType bestType() {
if (Boolean.getBoolean("velocity.disable-native-transport")) {
return NIO;
}
if (Epoll.isAvailable()) {
return EPOLL;
} else if (KQueue.isAvailable()) {
return KQUEUE;
} else {
return NIO;
}

View File

@@ -1,153 +0,0 @@
package com.velocitypowered.proxy.network.http;
import com.velocitypowered.proxy.VelocityServer;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoop;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpContentCompressor;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslHandler;
import java.net.InetSocketAddress;
import java.net.URL;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import javax.net.ssl.SSLEngine;
public class NettyHttpClient {
private final String userAgent;
private final VelocityServer server;
/**
* Initializes the HTTP client.
*
* @param server the Velocity server
*/
public NettyHttpClient(VelocityServer server) {
this.userAgent = server.getVersion().getName() + "/" + server.getVersion().getVersion();
this.server = server;
}
private ChannelFuture establishConnection(URL url, EventLoop loop) {
String host = url.getHost();
int port = url.getPort();
boolean ssl = url.getProtocol().equals("https");
if (port == -1) {
port = ssl ? 443 : 80;
}
InetSocketAddress address = InetSocketAddress.createUnresolved(host, port);
return server.initializeGenericBootstrap(loop)
.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
if (ssl) {
SslContext context = SslContextBuilder.forClient().protocols("TLSv1.2").build();
// Unbelievably, Java doesn't automatically check the CN to make sure we're talking
// to the right host! Therefore, we provide the intended host name and port, along
// with asking Java very nicely if it could check the hostname in the certificate
// for us.
SSLEngine engine = context.newEngine(ch.alloc(), address.getHostString(),
address.getPort());
engine.getSSLParameters().setEndpointIdentificationAlgorithm("HTTPS");
ch.pipeline().addLast("ssl", new SslHandler(engine));
}
ch.pipeline().addLast("http", new HttpClientCodec());
}
})
.connect(address);
}
/**
* Attempts an HTTP GET request to the specified URL.
* @param url the URL to fetch
* @param loop the event loop to use
* @return a future representing the response
*/
public CompletableFuture<SimpleHttpResponse> get(URL url, EventLoop loop) {
CompletableFuture<SimpleHttpResponse> reply = new CompletableFuture<>();
establishConnection(url, loop)
.addListener((ChannelFutureListener) future -> {
if (future.isSuccess()) {
Channel channel = future.channel();
channel.pipeline().addLast("collector", new SimpleHttpResponseCollector(reply));
String pathAndQuery = url.getPath();
if (url.getQuery() != null && url.getQuery().length() > 0) {
pathAndQuery += "?" + url.getQuery();
}
DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1,
HttpMethod.GET, pathAndQuery);
request.headers().add(HttpHeaderNames.HOST, url.getHost());
request.headers().add(HttpHeaderNames.USER_AGENT, userAgent);
channel.writeAndFlush(request, channel.voidPromise());
} else {
reply.completeExceptionally(future.cause());
}
});
return reply;
}
/**
* Attempts an HTTP POST request to the specified URL.
* @param url the URL to fetch
* @param body the body to post
* @param decorator a consumer that can modify the request as required
* @return a future representing the response
*/
public CompletableFuture<SimpleHttpResponse> post(URL url, ByteBuf body,
Consumer<HttpRequest> decorator) {
return post(url, server.getWorkerGroup().next(), body, decorator);
}
/**
* Attempts an HTTP POST request to the specified URL.
* @param url the URL to fetch
* @param loop the event loop to use
* @param body the body to post
* @param decorator a consumer that can modify the request as required
* @return a future representing the response
*/
public CompletableFuture<SimpleHttpResponse> post(URL url, EventLoop loop, ByteBuf body,
Consumer<HttpRequest> decorator) {
CompletableFuture<SimpleHttpResponse> reply = new CompletableFuture<>();
establishConnection(url, loop)
.addListener((ChannelFutureListener) future -> {
if (future.isSuccess()) {
Channel channel = future.channel();
channel.pipeline().addLast("collector", new SimpleHttpResponseCollector(reply));
String pathAndQuery = url.getPath();
if (url.getQuery() != null && url.getQuery().length() > 0) {
pathAndQuery += "?" + url.getQuery();
}
DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1,
HttpMethod.POST, pathAndQuery, body);
request.headers().add(HttpHeaderNames.HOST, url.getHost());
request.headers().add(HttpHeaderNames.USER_AGENT, userAgent);
request.headers().add(HttpHeaderNames.CONTENT_LENGTH, body.readableBytes());
decorator.accept(request);
channel.writeAndFlush(request, channel.voidPromise());
} else {
body.release();
reply.completeExceptionally(future.cause());
}
});
return reply;
}
}

View File

@@ -1,28 +0,0 @@
package com.velocitypowered.proxy.network.http;
public class SimpleHttpResponse {
private final int code;
private final String body;
SimpleHttpResponse(int code, String body) {
this.code = code;
this.body = body;
}
public int getCode() {
return code;
}
public String getBody() {
return body;
}
@Override
public String toString() {
return "SimpleHttpResponse{"
+ "code=" + code
+ ", body='" + body + '\''
+ '}';
}
}

View File

@@ -1,51 +0,0 @@
package com.velocitypowered.proxy.network.http;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.util.ReferenceCountUtil;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletableFuture;
class SimpleHttpResponseCollector extends ChannelInboundHandlerAdapter {
private final StringBuilder buffer = new StringBuilder();
private final CompletableFuture<SimpleHttpResponse> reply;
private int httpCode;
SimpleHttpResponseCollector(CompletableFuture<SimpleHttpResponse> reply) {
this.reply = reply;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
try {
if (msg instanceof HttpResponse) {
HttpResponse response = (HttpResponse) msg;
HttpResponseStatus status = response.status();
this.httpCode = status.code();
}
if (msg instanceof HttpContent) {
buffer.append(((HttpContent) msg).content().toString(StandardCharsets.UTF_8));
if (msg instanceof LastHttpContent) {
ctx.close();
reply.complete(new SimpleHttpResponse(httpCode, buffer.toString()));
}
}
} finally {
ReferenceCountUtil.release(msg);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
reply.completeExceptionally(cause);
}
}

View File

@@ -0,0 +1,73 @@
package com.velocitypowered.proxy.network.netty;
import io.netty.channel.EventLoopGroup;
import io.netty.resolver.InetNameResolver;
import io.netty.resolver.dns.DnsAddressResolverGroup;
import io.netty.util.concurrent.EventExecutor;
import io.netty.util.concurrent.FutureListener;
import io.netty.util.concurrent.ImmediateEventExecutor;
import io.netty.util.concurrent.Promise;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.List;
public class DnsAddressResolverGroupNameResolverAdapter extends InetNameResolver {
private final DnsAddressResolverGroup resolverGroup;
private final EventLoopGroup group;
/**
* Creates a DnsAddressResolverGroupNameResolverAdapter.
* @param resolverGroup the resolver group to use
* @param group the event loop group
*/
public DnsAddressResolverGroupNameResolverAdapter(
DnsAddressResolverGroup resolverGroup, EventLoopGroup group) {
super(ImmediateEventExecutor.INSTANCE);
this.resolverGroup = resolverGroup;
this.group = group;
}
@Override
protected void doResolve(String inetHost, Promise<InetAddress> promise) throws Exception {
EventExecutor executor = this.findExecutor();
resolverGroup.getResolver(executor).resolve(InetSocketAddress.createUnresolved(inetHost, 17))
.addListener((FutureListener<InetSocketAddress>) future -> {
if (future.isSuccess()) {
promise.trySuccess(future.getNow().getAddress());
} else {
promise.tryFailure(future.cause());
}
});
}
@Override
protected void doResolveAll(String inetHost, Promise<List<InetAddress>> promise)
throws Exception {
EventExecutor executor = this.findExecutor();
resolverGroup.getResolver(executor).resolveAll(InetSocketAddress.createUnresolved(inetHost, 17))
.addListener((FutureListener<List<InetSocketAddress>>) future -> {
if (future.isSuccess()) {
List<InetAddress> addresses = new ArrayList<>(future.getNow().size());
for (InetSocketAddress address : future.getNow()) {
addresses.add(address.getAddress());
}
promise.trySuccess(addresses);
} else {
promise.tryFailure(future.cause());
}
});
}
private EventExecutor findExecutor() {
for (EventExecutor executor : group) {
if (executor.inEventLoop()) {
return executor;
}
}
// otherwise, pick one
return group.next();
}
}

View File

@@ -35,10 +35,9 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
checkFrame(expectedSize >= threshold, "Uncompressed size %s is greater than threshold %s",
expectedSize, threshold);
checkFrame(expectedSize <= MAXIMUM_UNCOMPRESSED_SIZE, "Expected uncompressed size"
+ "%s is larger than protocol maximum of %s", expectedSize, MAXIMUM_UNCOMPRESSED_SIZE);
int initialCapacity = Math.min(expectedSize, MAXIMUM_UNCOMPRESSED_SIZE);
ByteBuf compatibleIn = ensureCompatible(ctx.alloc(), compressor, in);
ByteBuf uncompressed = preferredBuffer(ctx.alloc(), compressor, expectedSize);
ByteBuf uncompressed = preferredBuffer(ctx.alloc(), compressor, initialCapacity);
try {
compressor.inflate(compatibleIn, uncompressed, expectedSize);
out.add(uncompressed);

View File

@@ -133,5 +133,9 @@ public class TabCompleteResponse implements MinecraftPacket {
public int compareTo(Offer o) {
return this.text.compareTo(o.text);
}
public String getText() {
return text;
}
}
}

View File

@@ -18,13 +18,15 @@ public class PingSessionHandler implements MinecraftSessionHandler {
private final CompletableFuture<ServerPing> result;
private final RegisteredServer server;
private final MinecraftConnection connection;
private final ProtocolVersion version;
private boolean completed = false;
PingSessionHandler(CompletableFuture<ServerPing> result, RegisteredServer server,
MinecraftConnection connection) {
MinecraftConnection connection, ProtocolVersion version) {
this.result = result;
this.server = server;
this.connection = connection;
this.version = version;
}
@Override
@@ -33,11 +35,13 @@ public class PingSessionHandler implements MinecraftSessionHandler {
handshake.setNextStatus(StateRegistry.STATUS_ID);
handshake.setServerAddress(server.getServerInfo().getAddress().getHostString());
handshake.setPort(server.getServerInfo().getAddress().getPort());
handshake.setProtocolVersion(ProtocolVersion.MINIMUM_VERSION);
connection.write(handshake);
handshake.setProtocolVersion(version);
connection.delayedWrite(handshake);
connection.setState(StateRegistry.STATUS);
connection.write(StatusRequest.INSTANCE);
connection.delayedWrite(StatusRequest.INSTANCE);
connection.flush();
}
@Override

View File

@@ -9,6 +9,7 @@ import static com.velocitypowered.proxy.network.Connections.READ_TIMEOUT;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ServerConnection;
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
@@ -19,7 +20,6 @@ import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftVarintFrameDecoder;
@@ -28,6 +28,7 @@ import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoop;
import io.netty.handler.timeout.ReadTimeoutHandler;
import java.util.Collection;
import java.util.Set;
@@ -59,11 +60,22 @@ public class VelocityRegisteredServer implements RegisteredServer {
@Override
public CompletableFuture<ServerPing> ping() {
return ping(null, ProtocolVersion.UNKNOWN);
}
/**
* Pings the specified server using the specified event {@code loop}, claiming to be
* {@code version}.
* @param loop the event loop to use
* @param version the version to report
* @return the server list ping response
*/
public CompletableFuture<ServerPing> ping(@Nullable EventLoop loop, ProtocolVersion version) {
if (server == null) {
throw new IllegalStateException("No Velocity proxy instance available");
}
CompletableFuture<ServerPing> pingFuture = new CompletableFuture<>();
server.initializeGenericBootstrap()
server.createBootstrap(loop)
.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
@@ -87,8 +99,8 @@ public class VelocityRegisteredServer implements RegisteredServer {
public void operationComplete(ChannelFuture future) throws Exception {
if (future.isSuccess()) {
MinecraftConnection conn = future.channel().pipeline().get(MinecraftConnection.class);
conn.setSessionHandler(
new PingSessionHandler(pingFuture, VelocityRegisteredServer.this, conn));
conn.setSessionHandler(new PingSessionHandler(
pingFuture, VelocityRegisteredServer.this, conn, version));
} else {
pingFuture.completeExceptionally(future.cause());
}

View File

@@ -1,6 +1,8 @@
package com.velocitypowered.proxy.util;
import com.google.common.base.Preconditions;
import com.google.common.net.InetAddresses;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
@@ -19,7 +21,12 @@ public class AddressUtil {
public static InetSocketAddress parseAddress(String ip) {
Preconditions.checkNotNull(ip, "ip");
URI uri = URI.create("tcp://" + ip);
return InetSocketAddress.createUnresolved(uri.getHost(), uri.getPort());
try {
InetAddress ia = InetAddresses.forUriString(uri.getHost());
return new InetSocketAddress(ia, uri.getPort());
} catch (IllegalArgumentException e) {
return InetSocketAddress.createUnresolved(uri.getHost(), uri.getPort());
}
}
/**

View File

@@ -21,7 +21,7 @@ public class VelocityMessages {
public static final Component ALREADY_CONNECTED = TextComponent
.of("You are already connected to this proxy!", TextColor.RED);
public static final Component MOVED_TO_NEW_SERVER = TextComponent
.of("You were moved from the server you were on because you were kicked", TextColor.RED);
.of("The server you were on kicked you: ", TextColor.RED);
private VelocityMessages() {
throw new AssertionError();

View File

@@ -271,6 +271,6 @@ public class VelocityBossBar implements com.velocitypowered.api.util.bossbar.Bos
private void sendPacket(Player player, MinecraftPacket packet) {
ConnectedPlayer connected = (ConnectedPlayer) player;
connected.getMinecraftConnection().write(packet);
connected.getConnection().write(packet);
}
}

View File

@@ -1 +1,2 @@
log4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
log4j.skipJansi=true