Mirror von
https://github.com/PaperMC/Velocity.git
synchronisiert 2024-12-23 23:00:35 +01:00
Merge remote-tracking branch 'upstream/dev/1.1.0'
Dieser Commit ist enthalten in:
Commit
18a56d8b1d
@ -7,4 +7,4 @@ cache:
|
||||
- $HOME/.gradle/caches/
|
||||
- $HOME/.gradle/wrapper/
|
||||
jdk:
|
||||
- oraclejdk8
|
||||
- openjdk8
|
13
README.md
13
README.md
@ -34,15 +34,4 @@ Once you've built Velocity, you can copy and run the `-all` JAR from
|
||||
and you can configure it from there.
|
||||
|
||||
Alternatively, you can get the proxy JAR from the [downloads](https://www.velocitypowered.com/downloads)
|
||||
page.
|
||||
|
||||
## Status
|
||||
|
||||
Velocity is currently in beta. Production networks are successfully running
|
||||
Velocity with many hundreds of concurrent players online, but your mileage
|
||||
may vary.
|
||||
|
||||
Velocity supports Minecraft 1.8-1.14.3. Velocity is best supported with Paper
|
||||
and SpongeVanilla. Minecraft Forge is fully supported but mod compatibility
|
||||
may vary. Generally, Velocity will support many mods better than BungeeCord
|
||||
or Waterfall do but compatibility can not always be ensured.
|
||||
page.
|
@ -24,7 +24,7 @@ dependencies {
|
||||
compile "net.kyori:text-serializer-plain:${textVersion}"
|
||||
compile 'com.moandjiezana.toml:toml4j:0.7.2'
|
||||
compile "org.slf4j:slf4j-api:${slf4jVersion}"
|
||||
compile 'com.google.inject:guice:4.2.0'
|
||||
compile 'com.google.inject:guice:4.2.2'
|
||||
compile "org.checkerframework:checker-qual:${checkerFrameworkVersion}"
|
||||
|
||||
compile "org.spongepowered:configurate-hocon:${configurateVersion}"
|
||||
|
@ -0,0 +1,62 @@
|
||||
package com.velocitypowered.api.event.player;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This event is fired after a tab complete response is sent by the remote server, for clients on
|
||||
* 1.12.2 and below. You have the opportunity to modify the response sent to the remote player.
|
||||
*/
|
||||
public class TabCompleteEvent {
|
||||
private final Player player;
|
||||
private final String partialMessage;
|
||||
private final List<String> suggestions;
|
||||
|
||||
/**
|
||||
* Constructs a new TabCompleteEvent instance.
|
||||
* @param player the player
|
||||
* @param partialMessage the partial message
|
||||
* @param suggestions the initial list of suggestions
|
||||
*/
|
||||
public TabCompleteEvent(Player player, String partialMessage, List<String> suggestions) {
|
||||
this.player = checkNotNull(player, "player");
|
||||
this.partialMessage = checkNotNull(partialMessage, "partialMessage");
|
||||
this.suggestions = new ArrayList<>(checkNotNull(suggestions, "suggestions"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the player requesting the tab completion.
|
||||
* @return the requesting player
|
||||
*/
|
||||
public Player getPlayer() {
|
||||
return player;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the message being partially completed.
|
||||
* @return the partial message
|
||||
*/
|
||||
public String getPartialMessage() {
|
||||
return partialMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the suggestions provided to the user, as a mutable list.
|
||||
* @return the suggestions
|
||||
*/
|
||||
public List<String> getSuggestions() {
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TabCompleteEvent{"
|
||||
+ "player=" + player
|
||||
+ ", partialMessage='" + partialMessage + '\''
|
||||
+ ", suggestions=" + suggestions
|
||||
+ '}';
|
||||
}
|
||||
}
|
@ -32,7 +32,8 @@ public enum ProtocolVersion {
|
||||
MINECRAFT_1_14(477, "1.14"),
|
||||
MINECRAFT_1_14_1(480, "1.14.1"),
|
||||
MINECRAFT_1_14_2(485, "1.14.2"),
|
||||
MINECRAFT_1_14_3(490, "1.14.3");
|
||||
MINECRAFT_1_14_3(490, "1.14.3"),
|
||||
MINECRAFT_1_14_4(498, "1.14.4");
|
||||
|
||||
private final int protocol;
|
||||
private final String name;
|
||||
|
@ -182,6 +182,19 @@ public final class ServerPing {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the modified {@code mods} list in the response.
|
||||
* @param mods the mods list to use
|
||||
* @return this build, for chaining
|
||||
*/
|
||||
public Builder mods(ModInfo mods) {
|
||||
Preconditions.checkNotNull(mods, "mods");
|
||||
this.modType = mods.getType();
|
||||
this.mods.clear();
|
||||
this.mods.addAll(mods.getMods());
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder clearMods() {
|
||||
this.mods.clear();
|
||||
return this;
|
||||
|
@ -20,11 +20,11 @@ allprojects {
|
||||
|
||||
ext {
|
||||
// dependency versions
|
||||
textVersion = '3.0.1'
|
||||
textVersion = '3.0.2'
|
||||
junitVersion = '5.3.0-M1'
|
||||
slf4jVersion = '1.7.25'
|
||||
log4jVersion = '2.11.2'
|
||||
nettyVersion = '4.1.37.Final'
|
||||
nettyVersion = '4.1.38.Final'
|
||||
guavaVersion = '25.1-jre'
|
||||
checkerFrameworkVersion = '2.7.0'
|
||||
configurateVersion = '3.6'
|
||||
|
@ -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}"
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,7 @@
|
||||
package com.velocitypowered.proxy.config;
|
||||
|
||||
public enum PingPassthroughMode {
|
||||
DISABLED,
|
||||
MODS,
|
||||
ALL
|
||||
}
|
@ -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
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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());
|
||||
|
@ -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));
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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 + '\''
|
||||
+ '}';
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -1 +1,2 @@
|
||||
log4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
|
||||
log4j.skipJansi=true
|
||||
|
Laden…
In neuem Issue referenzieren
Einen Benutzer sperren