geforkt von Mirrors/Velocity
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/caches/
|
||||||
- $HOME/.gradle/wrapper/
|
- $HOME/.gradle/wrapper/
|
||||||
jdk:
|
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.
|
and you can configure it from there.
|
||||||
|
|
||||||
Alternatively, you can get the proxy JAR from the [downloads](https://www.velocitypowered.com/downloads)
|
Alternatively, you can get the proxy JAR from the [downloads](https://www.velocitypowered.com/downloads)
|
||||||
page.
|
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.
|
|
@ -24,7 +24,7 @@ dependencies {
|
|||||||
compile "net.kyori:text-serializer-plain:${textVersion}"
|
compile "net.kyori:text-serializer-plain:${textVersion}"
|
||||||
compile 'com.moandjiezana.toml:toml4j:0.7.2'
|
compile 'com.moandjiezana.toml:toml4j:0.7.2'
|
||||||
compile "org.slf4j:slf4j-api:${slf4jVersion}"
|
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.checkerframework:checker-qual:${checkerFrameworkVersion}"
|
||||||
|
|
||||||
compile "org.spongepowered:configurate-hocon:${configurateVersion}"
|
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(477, "1.14"),
|
||||||
MINECRAFT_1_14_1(480, "1.14.1"),
|
MINECRAFT_1_14_1(480, "1.14.1"),
|
||||||
MINECRAFT_1_14_2(485, "1.14.2"),
|
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 int protocol;
|
||||||
private final String name;
|
private final String name;
|
||||||
|
@ -182,6 +182,19 @@ public final class ServerPing {
|
|||||||
return this;
|
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() {
|
public Builder clearMods() {
|
||||||
this.mods.clear();
|
this.mods.clear();
|
||||||
return this;
|
return this;
|
||||||
|
@ -20,11 +20,11 @@ allprojects {
|
|||||||
|
|
||||||
ext {
|
ext {
|
||||||
// dependency versions
|
// dependency versions
|
||||||
textVersion = '3.0.1'
|
textVersion = '3.0.2'
|
||||||
junitVersion = '5.3.0-M1'
|
junitVersion = '5.3.0-M1'
|
||||||
slf4jVersion = '1.7.25'
|
slf4jVersion = '1.7.25'
|
||||||
log4jVersion = '2.11.2'
|
log4jVersion = '2.11.2'
|
||||||
nettyVersion = '4.1.37.Final'
|
nettyVersion = '4.1.38.Final'
|
||||||
guavaVersion = '25.1-jre'
|
guavaVersion = '25.1-jre'
|
||||||
checkerFrameworkVersion = '2.7.0'
|
checkerFrameworkVersion = '2.7.0'
|
||||||
configurateVersion = '3.6'
|
configurateVersion = '3.6'
|
||||||
|
@ -48,7 +48,6 @@ dependencies {
|
|||||||
compile "io.netty:netty-handler:${nettyVersion}"
|
compile "io.netty:netty-handler:${nettyVersion}"
|
||||||
compile "io.netty:netty-transport-native-epoll:${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-epoll:${nettyVersion}:linux-x86_64"
|
||||||
compile "io.netty:netty-transport-native-kqueue:${nettyVersion}:osx-x86_64"
|
|
||||||
compile "io.netty:netty-resolver-dns:${nettyVersion}"
|
compile "io.netty:netty-resolver-dns:${nettyVersion}"
|
||||||
|
|
||||||
compile "org.apache.logging.log4j:log4j-api:${log4jVersion}"
|
compile "org.apache.logging.log4j:log4j-api:${log4jVersion}"
|
||||||
@ -57,14 +56,18 @@ dependencies {
|
|||||||
compile "org.apache.logging.log4j:log4j-iostreams:${log4jVersion}"
|
compile "org.apache.logging.log4j:log4j-iostreams:${log4jVersion}"
|
||||||
|
|
||||||
compile 'net.sf.jopt-simple:jopt-simple:5.0.4' // command-line options
|
compile 'net.sf.jopt-simple:jopt-simple:5.0.4' // command-line options
|
||||||
compile 'net.minecrell:terminalconsoleappender:1.1.1'
|
compile 'net.minecrell:terminalconsoleappender:1.2.0'
|
||||||
runtime 'net.java.dev.jna:jna:4.5.2' // Needed for JLine
|
runtime 'org.jline:jline-terminal-jansi:3.12.1' // Needed for JLine
|
||||||
runtime 'com.lmax:disruptor:3.4.2' // Async loggers
|
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 'net.kyori:event-method-asm:3.0.0'
|
||||||
|
|
||||||
compile 'com.mojang:brigadier:1.0.15'
|
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-api:${junitVersion}"
|
||||||
testCompile "org.junit.jupiter:junit-jupiter-engine:${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.JsonArray;
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
import com.velocitypowered.proxy.config.VelocityConfiguration;
|
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 io.netty.handler.codec.http.HttpHeaderNames;
|
||||||
import java.io.BufferedWriter;
|
import java.io.BufferedWriter;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStreamWriter;
|
import java.io.OutputStreamWriter;
|
||||||
import java.io.Writer;
|
import java.io.Writer;
|
||||||
@ -21,11 +18,14 @@ import java.util.Map;
|
|||||||
import java.util.Timer;
|
import java.util.Timer;
|
||||||
import java.util.TimerTask;
|
import java.util.TimerTask;
|
||||||
import java.util.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.util.zip.GZIPOutputStream;
|
import java.util.zip.GZIPOutputStream;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.asynchttpclient.ListenableFuture;
|
||||||
|
import org.asynchttpclient.Response;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* bStats collects some data for plugin authors.
|
* bStats collects some data for plugin authors.
|
||||||
@ -185,40 +185,44 @@ public class Metrics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Compress the data to save bandwidth
|
// Compress the data to save bandwidth
|
||||||
ByteBuf reqBody = createResponseBody(data);
|
ListenableFuture<Response> future = server.getAsyncHttpClient()
|
||||||
|
.preparePost(URL)
|
||||||
server.getHttpClient().post(new URL(URL), reqBody, request -> {
|
.addHeader(HttpHeaderNames.CONTENT_ENCODING, "gzip")
|
||||||
request.headers().add(HttpHeaderNames.CONTENT_ENCODING, "gzip");
|
.addHeader(HttpHeaderNames.ACCEPT, "application/json")
|
||||||
request.headers().add(HttpHeaderNames.ACCEPT, "application/json");
|
.addHeader(HttpHeaderNames.CONTENT_TYPE, "application/json")
|
||||||
request.headers().add(HttpHeaderNames.CONTENT_TYPE, "application/json");
|
.setBody(createResponseBody(data))
|
||||||
})
|
.execute();
|
||||||
.whenCompleteAsync((resp, exc) -> {
|
future.addListener(() -> {
|
||||||
if (logFailedRequests) {
|
if (logFailedRequests) {
|
||||||
if (exc != null) {
|
try {
|
||||||
logger.error("Unable to send metrics to bStats", exc);
|
Response r = future.get();
|
||||||
} else if (resp.getCode() != 429) {
|
if (r.getStatusCode() != 429) {
|
||||||
logger.error("Got HTTP status code {} when sending metrics to bStats",
|
logger.error("Got HTTP status code {} when sending metrics to bStats",
|
||||||
resp.getCode());
|
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 {
|
private static byte[] createResponseBody(JsonObject object) throws IOException {
|
||||||
ByteBuf buf = Unpooled.buffer();
|
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||||
try (Writer writer =
|
try (Writer writer =
|
||||||
new BufferedWriter(
|
new BufferedWriter(
|
||||||
new OutputStreamWriter(
|
new OutputStreamWriter(
|
||||||
new GZIPOutputStream(new ByteBufOutputStream(buf)), StandardCharsets.UTF_8
|
new GZIPOutputStream(os), StandardCharsets.UTF_8
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
VelocityServer.GSON.toJson(object, writer);
|
VelocityServer.GSON.toJson(object, writer);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
buf.release();
|
|
||||||
throw e;
|
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.connection.client.ConnectedPlayer;
|
||||||
import com.velocitypowered.proxy.console.VelocityConsole;
|
import com.velocitypowered.proxy.console.VelocityConsole;
|
||||||
import com.velocitypowered.proxy.network.ConnectionManager;
|
import com.velocitypowered.proxy.network.ConnectionManager;
|
||||||
import com.velocitypowered.proxy.network.http.NettyHttpClient;
|
|
||||||
import com.velocitypowered.proxy.plugin.VelocityEventManager;
|
import com.velocitypowered.proxy.plugin.VelocityEventManager;
|
||||||
import com.velocitypowered.proxy.plugin.VelocityPluginManager;
|
import com.velocitypowered.proxy.plugin.VelocityPluginManager;
|
||||||
import com.velocitypowered.proxy.protocol.packet.Chat;
|
import com.velocitypowered.proxy.protocol.packet.Chat;
|
||||||
@ -70,12 +69,15 @@ import java.util.function.IntFunction;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import net.kyori.text.Component;
|
import net.kyori.text.Component;
|
||||||
import net.kyori.text.TextComponent;
|
import net.kyori.text.TextComponent;
|
||||||
|
import net.kyori.text.TranslatableComponent;
|
||||||
import net.kyori.text.serializer.gson.GsonComponentSerializer;
|
import net.kyori.text.serializer.gson.GsonComponentSerializer;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.asynchttpclient.AsyncHttpClient;
|
||||||
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
|
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
|
||||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||||
|
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||||
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||||
|
|
||||||
public class VelocityServer implements ProxyServer {
|
public class VelocityServer implements ProxyServer {
|
||||||
@ -89,7 +91,6 @@ public class VelocityServer implements ProxyServer {
|
|||||||
private final ConnectionManager cm;
|
private final ConnectionManager cm;
|
||||||
private final ProxyOptions options;
|
private final ProxyOptions options;
|
||||||
private @MonotonicNonNull VelocityConfiguration configuration;
|
private @MonotonicNonNull VelocityConfiguration configuration;
|
||||||
private @MonotonicNonNull NettyHttpClient httpClient;
|
|
||||||
private @MonotonicNonNull KeyPair serverKeyPair;
|
private @MonotonicNonNull KeyPair serverKeyPair;
|
||||||
private final ServerMap servers;
|
private final ServerMap servers;
|
||||||
private final VelocityCommandManager commandManager = new VelocityCommandManager();
|
private final VelocityCommandManager commandManager = new VelocityCommandManager();
|
||||||
@ -201,7 +202,6 @@ public class VelocityServer implements ProxyServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ipAttemptLimiter = Ratelimiters.createWithMilliseconds(configuration.getLoginRatelimit());
|
ipAttemptLimiter = Ratelimiters.createWithMilliseconds(configuration.getLoginRatelimit());
|
||||||
httpClient = new NettyHttpClient(this);
|
|
||||||
loadPlugins();
|
loadPlugins();
|
||||||
|
|
||||||
// Go ahead and fire the proxy initialization event. We block since plugins should have a chance
|
// 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());
|
logger.info("Loaded {} plugins", pluginManager.getPlugins().size());
|
||||||
}
|
}
|
||||||
|
|
||||||
public EventLoopGroup getWorkerGroup() {
|
public Bootstrap createBootstrap(@Nullable EventLoopGroup group) {
|
||||||
return this.cm.getWorkerGroup();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Bootstrap initializeGenericBootstrap() {
|
|
||||||
return this.cm.createWorker();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Bootstrap initializeGenericBootstrap(EventLoopGroup group) {
|
|
||||||
return this.cm.createWorker(group);
|
return this.cm.createWorker(group);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -433,8 +425,8 @@ public class VelocityServer implements ProxyServer {
|
|||||||
thread.start();
|
thread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
public NettyHttpClient getHttpClient() {
|
public AsyncHttpClient getAsyncHttpClient() {
|
||||||
return ensureInitialized(httpClient);
|
return ensureInitialized(cm).getHttpClient();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Ratelimiter getIpAttemptLimiter() {
|
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
|
* @return {@code true} if we can register the connection, {@code false} if not
|
||||||
*/
|
*/
|
||||||
public boolean canRegisterConnection(ConnectedPlayer connection) {
|
public boolean canRegisterConnection(ConnectedPlayer connection) {
|
||||||
|
if (configuration.isOnlineMode() && configuration.isOnlineModeKickExistingPlayers()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
String lowerName = connection.getUsername().toLowerCase(Locale.US);
|
String lowerName = connection.getUsername().toLowerCase(Locale.US);
|
||||||
return !(connectionsByName.containsKey(lowerName)
|
return !(connectionsByName.containsKey(lowerName)
|
||||||
|| connectionsByUuid.containsKey(connection.getUniqueId()));
|
|| connectionsByUuid.containsKey(connection.getUniqueId()));
|
||||||
@ -466,12 +461,24 @@ public class VelocityServer implements ProxyServer {
|
|||||||
*/
|
*/
|
||||||
public boolean registerConnection(ConnectedPlayer connection) {
|
public boolean registerConnection(ConnectedPlayer connection) {
|
||||||
String lowerName = connection.getUsername().toLowerCase(Locale.US);
|
String lowerName = connection.getUsername().toLowerCase(Locale.US);
|
||||||
if (connectionsByName.putIfAbsent(lowerName, connection) != null) {
|
|
||||||
return false;
|
if (!this.configuration.isOnlineModeKickExistingPlayers()) {
|
||||||
}
|
if (connectionsByName.putIfAbsent(lowerName, connection) != null) {
|
||||||
if (connectionsByUuid.putIfAbsent(connection.getUniqueId(), connection) != null) {
|
return false;
|
||||||
connectionsByName.remove(lowerName, connection);
|
}
|
||||||
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;
|
return true;
|
||||||
}
|
}
|
||||||
@ -498,7 +505,7 @@ public class VelocityServer implements ProxyServer {
|
|||||||
Preconditions.checkNotNull(component, "component");
|
Preconditions.checkNotNull(component, "component");
|
||||||
Chat chat = Chat.createClientbound(component);
|
Chat chat = Chat.createClientbound(component);
|
||||||
for (ConnectedPlayer player : connectionsByUuid.values()) {
|
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")
|
@ConfigKey("forwarding-secret")
|
||||||
private byte[] forwardingSecret = generateRandomString(12).getBytes(StandardCharsets.UTF_8);
|
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",
|
@Comment({
|
||||||
"suggest turning this on."})
|
"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")
|
@ConfigKey("announce-forge")
|
||||||
private boolean announceForge = false;
|
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]")
|
@Table("[servers]")
|
||||||
private final Servers 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,
|
private VelocityConfiguration(String bind, String motd, int showMaxPlayers, boolean onlineMode,
|
||||||
boolean announceForge, PlayerInfoForwarding playerInfoForwardingMode, byte[] forwardingSecret,
|
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.bind = bind;
|
||||||
this.motd = motd;
|
this.motd = motd;
|
||||||
this.showMaxPlayers = showMaxPlayers;
|
this.showMaxPlayers = showMaxPlayers;
|
||||||
@ -117,6 +143,8 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
|
|||||||
this.announceForge = announceForge;
|
this.announceForge = announceForge;
|
||||||
this.playerInfoForwardingMode = playerInfoForwardingMode;
|
this.playerInfoForwardingMode = playerInfoForwardingMode;
|
||||||
this.forwardingSecret = forwardingSecret;
|
this.forwardingSecret = forwardingSecret;
|
||||||
|
this.onlineModeKickExistingPlayers = onlineModeKickExistingPlayers;
|
||||||
|
this.pingPassthrough = pingPassthrough;
|
||||||
this.servers = servers;
|
this.servers = servers;
|
||||||
this.forcedHosts = forcedHosts;
|
this.forcedHosts = forcedHosts;
|
||||||
this.advanced = advanced;
|
this.advanced = advanced;
|
||||||
@ -365,10 +393,18 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
|
|||||||
return advanced.isProxyProtocol();
|
return advanced.isProxyProtocol();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean useTcpFastOpen() {
|
||||||
|
return advanced.tcpFastOpen;
|
||||||
|
}
|
||||||
|
|
||||||
public Metrics getMetrics() {
|
public Metrics getMetrics() {
|
||||||
return metrics;
|
return metrics;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PingPassthroughMode getPingPassthrough() {
|
||||||
|
return pingPassthrough;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return MoreObjects.toStringHelper(this)
|
return MoreObjects.toStringHelper(this)
|
||||||
@ -416,6 +452,8 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
|
|||||||
|
|
||||||
String forwardingModeName = toml.getString("player-info-forwarding-mode", "MODERN")
|
String forwardingModeName = toml.getString("player-info-forwarding-mode", "MODERN")
|
||||||
.toUpperCase(Locale.US);
|
.toUpperCase(Locale.US);
|
||||||
|
String passThroughName = toml.getString("ping-passthrough", "DISABLED")
|
||||||
|
.toUpperCase(Locale.US);
|
||||||
|
|
||||||
return new VelocityConfiguration(
|
return new VelocityConfiguration(
|
||||||
toml.getString("bind", "0.0.0.0:25577"),
|
toml.getString("bind", "0.0.0.0:25577"),
|
||||||
@ -425,6 +463,8 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
|
|||||||
toml.getBoolean("announce-forge", false),
|
toml.getBoolean("announce-forge", false),
|
||||||
PlayerInfoForwarding.valueOf(forwardingModeName),
|
PlayerInfoForwarding.valueOf(forwardingModeName),
|
||||||
forwardingSecret,
|
forwardingSecret,
|
||||||
|
toml.getBoolean("kick-existing-players", false),
|
||||||
|
PingPassthroughMode.valueOf(passThroughName),
|
||||||
servers,
|
servers,
|
||||||
forcedHosts,
|
forcedHosts,
|
||||||
advanced,
|
advanced,
|
||||||
@ -443,6 +483,10 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
|
|||||||
return builder.toString();
|
return builder.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isOnlineModeKickExistingPlayers() {
|
||||||
|
return onlineModeKickExistingPlayers;
|
||||||
|
}
|
||||||
|
|
||||||
private static class Servers {
|
private static class Servers {
|
||||||
|
|
||||||
@IsMap
|
@IsMap
|
||||||
@ -596,6 +640,10 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
|
|||||||
@ConfigKey("proxy-protocol")
|
@ConfigKey("proxy-protocol")
|
||||||
private boolean proxyProtocol = false;
|
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() {
|
private Advanced() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -607,6 +655,7 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
|
|||||||
this.connectionTimeout = toml.getLong("connection-timeout", 5000L).intValue();
|
this.connectionTimeout = toml.getLong("connection-timeout", 5000L).intValue();
|
||||||
this.readTimeout = toml.getLong("read-timeout", 30000L).intValue();
|
this.readTimeout = toml.getLong("read-timeout", 30000L).intValue();
|
||||||
this.proxyProtocol = toml.getBoolean("proxy-protocol", false);
|
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;
|
return proxyProtocol;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isTcpFastOpen() {
|
||||||
|
return tcpFastOpen;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "Advanced{"
|
return "Advanced{"
|
||||||
@ -643,6 +696,7 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
|
|||||||
+ ", connectionTimeout=" + connectionTimeout
|
+ ", connectionTimeout=" + connectionTimeout
|
||||||
+ ", readTimeout=" + readTimeout
|
+ ", readTimeout=" + readTimeout
|
||||||
+ ", proxyProtocol=" + proxyProtocol
|
+ ", 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() {
|
public EventLoop eventLoop() {
|
||||||
return channel.eventLoop();
|
return channel.eventLoop();
|
||||||
}
|
}
|
||||||
@ -233,6 +237,8 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
|||||||
* @param autoReading whether or not we should read data automatically
|
* @param autoReading whether or not we should read data automatically
|
||||||
*/
|
*/
|
||||||
public void setAutoReading(boolean autoReading) {
|
public void setAutoReading(boolean autoReading) {
|
||||||
|
ensureInEventLoop();
|
||||||
|
|
||||||
channel.config().setAutoRead(autoReading);
|
channel.config().setAutoRead(autoReading);
|
||||||
if (autoReading) {
|
if (autoReading) {
|
||||||
// For some reason, the channel may not completely read its queued contents once autoread
|
// 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
|
* @param state the new state
|
||||||
*/
|
*/
|
||||||
public void setState(StateRegistry state) {
|
public void setState(StateRegistry state) {
|
||||||
|
ensureInEventLoop();
|
||||||
|
|
||||||
this.state = state;
|
this.state = state;
|
||||||
this.channel.pipeline().get(MinecraftEncoder.class).setState(state);
|
this.channel.pipeline().get(MinecraftEncoder.class).setState(state);
|
||||||
this.channel.pipeline().get(MinecraftDecoder.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
|
* @param protocolVersion the protocol version to use
|
||||||
*/
|
*/
|
||||||
public void setProtocolVersion(ProtocolVersion protocolVersion) {
|
public void setProtocolVersion(ProtocolVersion protocolVersion) {
|
||||||
|
ensureInEventLoop();
|
||||||
|
|
||||||
this.protocolVersion = protocolVersion;
|
this.protocolVersion = protocolVersion;
|
||||||
this.nextProtocolVersion = protocolVersion;
|
this.nextProtocolVersion = protocolVersion;
|
||||||
if (protocolVersion != ProtocolVersion.LEGACY) {
|
if (protocolVersion != ProtocolVersion.LEGACY) {
|
||||||
@ -284,6 +294,8 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
|||||||
* @param sessionHandler the handler to use
|
* @param sessionHandler the handler to use
|
||||||
*/
|
*/
|
||||||
public void setSessionHandler(MinecraftSessionHandler sessionHandler) {
|
public void setSessionHandler(MinecraftSessionHandler sessionHandler) {
|
||||||
|
ensureInEventLoop();
|
||||||
|
|
||||||
if (this.sessionHandler != null) {
|
if (this.sessionHandler != null) {
|
||||||
this.sessionHandler.deactivated();
|
this.sessionHandler.deactivated();
|
||||||
}
|
}
|
||||||
@ -302,6 +314,7 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
|||||||
*/
|
*/
|
||||||
public void setCompressionThreshold(int threshold) {
|
public void setCompressionThreshold(int threshold) {
|
||||||
ensureOpen();
|
ensureOpen();
|
||||||
|
ensureInEventLoop();
|
||||||
|
|
||||||
if (threshold == -1) {
|
if (threshold == -1) {
|
||||||
channel.pipeline().remove(COMPRESSION_DECODER);
|
channel.pipeline().remove(COMPRESSION_DECODER);
|
||||||
@ -325,6 +338,7 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
|||||||
*/
|
*/
|
||||||
public void enableEncryption(byte[] secret) throws GeneralSecurityException {
|
public void enableEncryption(byte[] secret) throws GeneralSecurityException {
|
||||||
ensureOpen();
|
ensureOpen();
|
||||||
|
ensureInEventLoop();
|
||||||
|
|
||||||
SecretKey key = new SecretKeySpec(secret, "AES");
|
SecretKey key = new SecretKeySpec(secret, "AES");
|
||||||
|
|
||||||
@ -342,6 +356,7 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void setAssociation(MinecraftConnectionAssociation association) {
|
public void setAssociation(MinecraftConnectionAssociation association) {
|
||||||
|
ensureInEventLoop();
|
||||||
this.association = association;
|
this.association = association;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
BackendPlaySessionHandler(VelocityServer server, VelocityServerConnection serverConn) {
|
BackendPlaySessionHandler(VelocityServer server, VelocityServerConnection serverConn) {
|
||||||
this.server = server;
|
this.server = server;
|
||||||
this.serverConn = serverConn;
|
this.serverConn = serverConn;
|
||||||
this.playerConnection = serverConn.getPlayer().getMinecraftConnection();
|
this.playerConnection = serverConn.getPlayer().getConnection();
|
||||||
|
|
||||||
MinecraftSessionHandler psh = playerConnection.getSessionHandler();
|
MinecraftSessionHandler psh = playerConnection.getSessionHandler();
|
||||||
if (!(psh instanceof ClientPlaySessionHandler)) {
|
if (!(psh instanceof ClientPlaySessionHandler)) {
|
||||||
@ -190,7 +190,8 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
public void disconnected() {
|
public void disconnected() {
|
||||||
serverConn.getServer().removePlayer(serverConn.getPlayer());
|
serverConn.getServer().removePlayer(serverConn.getPlayer());
|
||||||
if (!serverConn.isGracefulDisconnect() && !exceptionTriggered) {
|
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,
|
private static ByteBuf createForwardingData(byte[] hmacSecret, String address,
|
||||||
GameProfile profile) {
|
GameProfile profile) {
|
||||||
ByteBuf dataToForward = Unpooled.buffer();
|
ByteBuf forwarded = Unpooled.buffer(2048);
|
||||||
ByteBuf finalData = Unpooled.buffer();
|
|
||||||
try {
|
try {
|
||||||
ProtocolUtils.writeVarInt(dataToForward, VelocityConstants.FORWARDING_VERSION);
|
ProtocolUtils.writeVarInt(forwarded, VelocityConstants.FORWARDING_VERSION);
|
||||||
ProtocolUtils.writeString(dataToForward, address);
|
ProtocolUtils.writeString(forwarded, address);
|
||||||
ProtocolUtils.writeUuid(dataToForward, profile.getId());
|
ProtocolUtils.writeUuid(forwarded, profile.getId());
|
||||||
ProtocolUtils.writeString(dataToForward, profile.getName());
|
ProtocolUtils.writeString(forwarded, profile.getName());
|
||||||
ProtocolUtils.writeProperties(dataToForward, profile.getProperties());
|
ProtocolUtils.writeProperties(forwarded, profile.getProperties());
|
||||||
|
|
||||||
SecretKey key = new SecretKeySpec(hmacSecret, "HmacSHA256");
|
SecretKey key = new SecretKeySpec(hmacSecret, "HmacSHA256");
|
||||||
Mac mac = Mac.getInstance("HmacSHA256");
|
Mac mac = Mac.getInstance("HmacSHA256");
|
||||||
mac.init(key);
|
mac.init(key);
|
||||||
mac.update(dataToForward.array(), dataToForward.arrayOffset(), dataToForward.readableBytes());
|
mac.update(forwarded.array(), forwarded.arrayOffset(), forwarded.readableBytes());
|
||||||
byte[] sig = mac.doFinal();
|
byte[] sig = mac.doFinal();
|
||||||
finalData.writeBytes(sig);
|
|
||||||
finalData.writeBytes(dataToForward);
|
return Unpooled.wrappedBuffer(Unpooled.wrappedBuffer(sig), forwarded);
|
||||||
return finalData;
|
|
||||||
} catch (InvalidKeyException e) {
|
} catch (InvalidKeyException e) {
|
||||||
finalData.release();
|
forwarded.release();
|
||||||
throw new RuntimeException("Unable to authenticate data", e);
|
throw new RuntimeException("Unable to authenticate data", e);
|
||||||
} catch (NoSuchAlgorithmException e) {
|
} catch (NoSuchAlgorithmException e) {
|
||||||
// Should never happen
|
// Should never happen
|
||||||
finalData.release();
|
forwarded.release();
|
||||||
throw new AssertionError(e);
|
throw new AssertionError(e);
|
||||||
} finally {
|
|
||||||
dataToForward.release();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -84,13 +84,13 @@ public class TransitionSessionHandler implements MinecraftSessionHandler {
|
|||||||
.whenCompleteAsync((x, error) -> {
|
.whenCompleteAsync((x, error) -> {
|
||||||
// Strap on the ClientPlaySessionHandler if required.
|
// Strap on the ClientPlaySessionHandler if required.
|
||||||
ClientPlaySessionHandler playHandler;
|
ClientPlaySessionHandler playHandler;
|
||||||
if (serverConn.getPlayer().getMinecraftConnection().getSessionHandler()
|
if (serverConn.getPlayer().getConnection().getSessionHandler()
|
||||||
instanceof ClientPlaySessionHandler) {
|
instanceof ClientPlaySessionHandler) {
|
||||||
playHandler = (ClientPlaySessionHandler) serverConn.getPlayer().getMinecraftConnection()
|
playHandler = (ClientPlaySessionHandler) serverConn.getPlayer().getConnection()
|
||||||
.getSessionHandler();
|
.getSessionHandler();
|
||||||
} else {
|
} else {
|
||||||
playHandler = new ClientPlaySessionHandler(server, serverConn.getPlayer());
|
playHandler = new ClientPlaySessionHandler(server, serverConn.getPlayer());
|
||||||
serverConn.getPlayer().getMinecraftConnection().setSessionHandler(playHandler);
|
serverConn.getPlayer().getConnection().setSessionHandler(playHandler);
|
||||||
}
|
}
|
||||||
playHandler.handleBackendJoinGame(packet, serverConn);
|
playHandler.handleBackendJoinGame(packet, serverConn);
|
||||||
|
|
||||||
@ -167,7 +167,7 @@ public class TransitionSessionHandler implements MinecraftSessionHandler {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
serverConn.getPlayer().getMinecraftConnection().write(packet.retain());
|
serverConn.getPlayer().getConnection().write(packet.retain());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,7 +12,6 @@ import static com.velocitypowered.proxy.network.Connections.READ_TIMEOUT;
|
|||||||
|
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
import com.velocitypowered.api.network.ProtocolVersion;
|
import com.velocitypowered.api.network.ProtocolVersion;
|
||||||
import com.velocitypowered.api.proxy.ConnectionRequestBuilder;
|
|
||||||
import com.velocitypowered.api.proxy.ServerConnection;
|
import com.velocitypowered.api.proxy.ServerConnection;
|
||||||
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
||||||
import com.velocitypowered.api.proxy.server.ServerInfo;
|
import com.velocitypowered.api.proxy.server.ServerInfo;
|
||||||
@ -77,7 +76,7 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
|
|||||||
CompletableFuture<Impl> result = new CompletableFuture<>();
|
CompletableFuture<Impl> result = new CompletableFuture<>();
|
||||||
// Note: we use the event loop for the connection the player is on. This reduces context
|
// Note: we use the event loop for the connection the player is on. This reduces context
|
||||||
// switches.
|
// switches.
|
||||||
server.initializeGenericBootstrap(proxyPlayer.getMinecraftConnection().eventLoop())
|
server.createBootstrap(proxyPlayer.getConnection().eventLoop())
|
||||||
.handler(new ChannelInitializer<Channel>() {
|
.handler(new ChannelInitializer<Channel>() {
|
||||||
@Override
|
@Override
|
||||||
protected void initChannel(Channel ch) throws Exception {
|
protected void initChannel(Channel ch) throws Exception {
|
||||||
@ -139,23 +138,24 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
|
|||||||
PlayerInfoForwarding forwardingMode = server.getConfiguration().getPlayerInfoForwardingMode();
|
PlayerInfoForwarding forwardingMode = server.getConfiguration().getPlayerInfoForwardingMode();
|
||||||
|
|
||||||
// Initiate the handshake.
|
// Initiate the handshake.
|
||||||
ProtocolVersion protocolVersion = proxyPlayer.getMinecraftConnection().getNextProtocolVersion();
|
ProtocolVersion protocolVersion = proxyPlayer.getConnection().getNextProtocolVersion();
|
||||||
Handshake handshake = new Handshake();
|
Handshake handshake = new Handshake();
|
||||||
handshake.setNextStatus(StateRegistry.LOGIN_ID);
|
handshake.setNextStatus(StateRegistry.LOGIN_ID);
|
||||||
handshake.setProtocolVersion(protocolVersion);
|
handshake.setProtocolVersion(protocolVersion);
|
||||||
if (forwardingMode == PlayerInfoForwarding.LEGACY) {
|
if (forwardingMode == PlayerInfoForwarding.LEGACY) {
|
||||||
handshake.setServerAddress(createLegacyForwardingAddress());
|
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);
|
handshake.setServerAddress(handshake.getServerAddress() + HANDSHAKE_HOSTNAME_TOKEN);
|
||||||
} else {
|
} else {
|
||||||
handshake.setServerAddress(registeredServer.getServerInfo().getAddress().getHostString());
|
handshake.setServerAddress(registeredServer.getServerInfo().getAddress().getHostString());
|
||||||
}
|
}
|
||||||
handshake.setPort(registeredServer.getServerInfo().getAddress().getPort());
|
handshake.setPort(registeredServer.getServerInfo().getAddress().getPort());
|
||||||
mc.write(handshake);
|
mc.delayedWrite(handshake);
|
||||||
|
|
||||||
mc.setProtocolVersion(protocolVersion);
|
mc.setProtocolVersion(protocolVersion);
|
||||||
mc.setState(StateRegistry.LOGIN);
|
mc.setState(StateRegistry.LOGIN);
|
||||||
mc.write(new ServerLogin(proxyPlayer.getUsername()));
|
mc.delayedWrite(new ServerLogin(proxyPlayer.getUsername()));
|
||||||
|
mc.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
public @Nullable MinecraftConnection getConnection() {
|
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.connection.PluginMessageEvent;
|
||||||
import com.velocitypowered.api.event.player.PlayerChatEvent;
|
import com.velocitypowered.api.event.player.PlayerChatEvent;
|
||||||
import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent;
|
import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent;
|
||||||
|
import com.velocitypowered.api.event.player.TabCompleteEvent;
|
||||||
import com.velocitypowered.api.network.ProtocolVersion;
|
import com.velocitypowered.api.network.ProtocolVersion;
|
||||||
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
import com.velocitypowered.proxy.VelocityServer;
|
||||||
@ -59,7 +60,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
private final List<UUID> serverBossBars = new ArrayList<>();
|
private final List<UUID> serverBossBars = new ArrayList<>();
|
||||||
private final Queue<PluginMessage> loginPluginMessages = new ArrayDeque<>();
|
private final Queue<PluginMessage> loginPluginMessages = new ArrayDeque<>();
|
||||||
private final VelocityServer server;
|
private final VelocityServer server;
|
||||||
private @Nullable TabCompleteRequest legacyCommandTabComplete;
|
private @Nullable TabCompleteRequest outstandingTabComplete;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a client play session handler.
|
* Constructs a client play session handler.
|
||||||
@ -77,7 +78,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
.getProtocolVersion());
|
.getProtocolVersion());
|
||||||
if (!channels.isEmpty()) {
|
if (!channels.isEmpty()) {
|
||||||
PluginMessage register = constructChannelsPacket(player.getProtocolVersion(), channels);
|
PluginMessage register = constructChannelsPacket(player.getProtocolVersion(), channels);
|
||||||
player.getMinecraftConnection().write(register);
|
player.getConnection().write(register);
|
||||||
player.getKnownChannels().addAll(channels);
|
player.getKnownChannels().addAll(channels);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -155,61 +156,11 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
public boolean handle(TabCompleteRequest packet) {
|
public boolean handle(TabCompleteRequest packet) {
|
||||||
boolean isCommand = !packet.isAssumeCommand() && packet.getCommand().startsWith("/");
|
boolean isCommand = !packet.isAssumeCommand() && packet.getCommand().startsWith("/");
|
||||||
|
|
||||||
if (!isCommand) {
|
if (isCommand) {
|
||||||
// We can't deal with anything else.
|
return this.handleCommandTabComplete(packet);
|
||||||
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;
|
|
||||||
} else {
|
} 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
|
@Override
|
||||||
@ -325,7 +276,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
public void writabilityChanged() {
|
public void writabilityChanged() {
|
||||||
VelocityServerConnection serverConn = player.getConnectedServer();
|
VelocityServerConnection serverConn = player.getConnectedServer();
|
||||||
if (serverConn != null) {
|
if (serverConn != null) {
|
||||||
boolean writable = player.getMinecraftConnection().getChannel().isWritable();
|
boolean writable = player.getConnection().getChannel().isWritable();
|
||||||
MinecraftConnection smc = serverConn.getConnection();
|
MinecraftConnection smc = serverConn.getConnection();
|
||||||
if (smc != null) {
|
if (smc != null) {
|
||||||
smc.setAutoReading(writable);
|
smc.setAutoReading(writable);
|
||||||
@ -345,7 +296,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
if (!spawned) {
|
if (!spawned) {
|
||||||
// Nothing special to do with regards to spawning the player
|
// Nothing special to do with regards to spawning the player
|
||||||
spawned = true;
|
spawned = true;
|
||||||
player.getMinecraftConnection().delayedWrite(joinGame);
|
player.getConnection().delayedWrite(joinGame);
|
||||||
|
|
||||||
// Required for Legacy Forge
|
// Required for Legacy Forge
|
||||||
player.getPhase().onFirstJoin(player);
|
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
|
// 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
|
// to perform entity ID rewrites, eliminating potential issues from rewriting packets and
|
||||||
// improving compatibility with mods.
|
// improving compatibility with mods.
|
||||||
player.getMinecraftConnection().delayedWrite(joinGame);
|
player.getConnection().delayedWrite(joinGame);
|
||||||
int tempDim = joinGame.getDimension() == 0 ? -1 : 0;
|
int tempDim = joinGame.getDimension() == 0 ? -1 : 0;
|
||||||
player.getMinecraftConnection().delayedWrite(
|
player.getConnection().delayedWrite(
|
||||||
new Respawn(tempDim, joinGame.getDifficulty(), joinGame.getGamemode(),
|
new Respawn(tempDim, joinGame.getDifficulty(), joinGame.getGamemode(),
|
||||||
joinGame.getLevelType()));
|
joinGame.getLevelType()));
|
||||||
player.getMinecraftConnection().delayedWrite(
|
player.getConnection().delayedWrite(
|
||||||
new Respawn(joinGame.getDimension(), joinGame.getDifficulty(), joinGame.getGamemode(),
|
new Respawn(joinGame.getDimension(), joinGame.getDifficulty(), joinGame.getGamemode(),
|
||||||
joinGame.getLevelType()));
|
joinGame.getLevelType()));
|
||||||
}
|
}
|
||||||
@ -381,7 +332,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
BossBar deletePacket = new BossBar();
|
BossBar deletePacket = new BossBar();
|
||||||
deletePacket.setUuid(serverBossBar);
|
deletePacket.setUuid(serverBossBar);
|
||||||
deletePacket.setAction(BossBar.REMOVE);
|
deletePacket.setAction(BossBar.REMOVE);
|
||||||
player.getMinecraftConnection().delayedWrite(deletePacket);
|
player.getConnection().delayedWrite(deletePacket);
|
||||||
}
|
}
|
||||||
serverBossBars.clear();
|
serverBossBars.clear();
|
||||||
|
|
||||||
@ -399,12 +350,12 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
|
|
||||||
// Clear any title from the previous server.
|
// Clear any title from the previous server.
|
||||||
if (player.getProtocolVersion().compareTo(MINECRAFT_1_8) >= 0) {
|
if (player.getProtocolVersion().compareTo(MINECRAFT_1_8) >= 0) {
|
||||||
player.getMinecraftConnection()
|
player.getConnection()
|
||||||
.delayedWrite(TitlePacket.resetForProtocolVersion(player.getProtocolVersion()));
|
.delayedWrite(TitlePacket.resetForProtocolVersion(player.getProtocolVersion()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flush everything
|
// Flush everything
|
||||||
player.getMinecraftConnection().flush();
|
player.getConnection().flush();
|
||||||
serverMc.flush();
|
serverMc.flush();
|
||||||
destination.completeJoin();
|
destination.completeJoin();
|
||||||
}
|
}
|
||||||
@ -413,29 +364,116 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
return serverBossBars;
|
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
|
* @param response the tab complete response from the backend
|
||||||
*/
|
*/
|
||||||
public void handleTabCompleteResponse(TabCompleteResponse response) {
|
public void handleTabCompleteResponse(TabCompleteResponse response) {
|
||||||
if (legacyCommandTabComplete != null) {
|
if (outstandingTabComplete != null) {
|
||||||
String command = legacyCommandTabComplete.getCommand().substring(1);
|
if (outstandingTabComplete.isAssumeCommand()) {
|
||||||
try {
|
return; // used for command blocks which can't run Velocity commands anyway
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
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.UUID;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.CompletionException;
|
import java.util.concurrent.CompletionException;
|
||||||
import java.util.concurrent.ExecutionException;
|
|
||||||
import java.util.concurrent.ThreadLocalRandom;
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
import net.kyori.text.Component;
|
import net.kyori.text.Component;
|
||||||
import net.kyori.text.TextComponent;
|
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.
|
* 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 final @Nullable InetSocketAddress virtualHost;
|
||||||
private GameProfile profile;
|
private GameProfile profile;
|
||||||
private PermissionFunction permissionFunction;
|
private PermissionFunction permissionFunction;
|
||||||
@ -104,18 +103,18 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
private @MonotonicNonNull List<String> serversToTry = null;
|
private @MonotonicNonNull List<String> serversToTry = null;
|
||||||
|
|
||||||
ConnectedPlayer(VelocityServer server, GameProfile profile,
|
ConnectedPlayer(VelocityServer server, GameProfile profile,
|
||||||
MinecraftConnection minecraftConnection, @Nullable InetSocketAddress virtualHost) {
|
MinecraftConnection connection, @Nullable InetSocketAddress virtualHost) {
|
||||||
this.server = server;
|
this.server = server;
|
||||||
if (minecraftConnection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) {
|
if (connection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) {
|
||||||
this.tabList = new VelocityTabList(minecraftConnection);
|
this.tabList = new VelocityTabList(connection);
|
||||||
} else {
|
} else {
|
||||||
this.tabList = new VelocityTabListLegacy(minecraftConnection);
|
this.tabList = new VelocityTabListLegacy(connection);
|
||||||
}
|
}
|
||||||
this.profile = profile;
|
this.profile = profile;
|
||||||
this.minecraftConnection = minecraftConnection;
|
this.connection = connection;
|
||||||
this.virtualHost = virtualHost;
|
this.virtualHost = virtualHost;
|
||||||
this.permissionFunction = PermissionFunction.ALWAYS_UNDEFINED;
|
this.permissionFunction = PermissionFunction.ALWAYS_UNDEFINED;
|
||||||
this.connectionPhase = minecraftConnection.getType().getInitialClientPhase();
|
this.connectionPhase = connection.getType().getInitialClientPhase();
|
||||||
this.knownChannels = CappedSet.create(MAX_PLUGIN_CHANNELS);
|
this.knownChannels = CappedSet.create(MAX_PLUGIN_CHANNELS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,8 +138,8 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
return profile;
|
return profile;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MinecraftConnection getMinecraftConnection() {
|
public MinecraftConnection getConnection() {
|
||||||
return minecraftConnection;
|
return connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -175,7 +174,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public InetSocketAddress getRemoteAddress() {
|
public InetSocketAddress getRemoteAddress() {
|
||||||
return (InetSocketAddress) minecraftConnection.getRemoteAddress();
|
return (InetSocketAddress) connection.getRemoteAddress();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -189,12 +188,12 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isActive() {
|
public boolean isActive() {
|
||||||
return minecraftConnection.getChannel().isActive();
|
return connection.getChannel().isActive();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ProtocolVersion getProtocolVersion() {
|
public ProtocolVersion getProtocolVersion() {
|
||||||
return minecraftConnection.getProtocolVersion();
|
return connection.getProtocolVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -210,7 +209,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
TitlePacket pkt = new TitlePacket();
|
TitlePacket pkt = new TitlePacket();
|
||||||
pkt.setAction(TitlePacket.SET_ACTION_BAR);
|
pkt.setAction(TitlePacket.SET_ACTION_BAR);
|
||||||
pkt.setComponent(GsonComponentSerializer.INSTANCE.serialize(component));
|
pkt.setComponent(GsonComponentSerializer.INSTANCE.serialize(component));
|
||||||
minecraftConnection.write(pkt);
|
connection.write(pkt);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
// Due to issues with action bar packets, we'll need to convert the text message into a
|
// 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 chat = new Chat();
|
||||||
chat.setType(pos);
|
chat.setType(pos);
|
||||||
chat.setMessage(json);
|
chat.setMessage(json);
|
||||||
minecraftConnection.write(chat);
|
connection.write(chat);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -263,23 +262,23 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
public void disconnect(Component reason) {
|
public void disconnect(Component reason) {
|
||||||
logger.info("{} has disconnected: {}", this,
|
logger.info("{} has disconnected: {}", this,
|
||||||
LegacyComponentSerializer.legacy().serialize(reason));
|
LegacyComponentSerializer.legacy().serialize(reason));
|
||||||
minecraftConnection.closeWith(Disconnect.create(reason));
|
connection.closeWith(Disconnect.create(reason));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void sendTitle(Title title) {
|
public void sendTitle(Title title) {
|
||||||
Preconditions.checkNotNull(title, "title");
|
Preconditions.checkNotNull(title, "title");
|
||||||
|
|
||||||
ProtocolVersion protocolVersion = minecraftConnection.getProtocolVersion();
|
ProtocolVersion protocolVersion = connection.getProtocolVersion();
|
||||||
if (title.equals(Titles.reset())) {
|
if (title.equals(Titles.reset())) {
|
||||||
minecraftConnection.write(TitlePacket.resetForProtocolVersion(protocolVersion));
|
connection.write(TitlePacket.resetForProtocolVersion(protocolVersion));
|
||||||
} else if (title.equals(Titles.hide())) {
|
} else if (title.equals(Titles.hide())) {
|
||||||
minecraftConnection.write(TitlePacket.hideForProtocolVersion(protocolVersion));
|
connection.write(TitlePacket.hideForProtocolVersion(protocolVersion));
|
||||||
} else if (title instanceof TextTitle) {
|
} else if (title instanceof TextTitle) {
|
||||||
TextTitle tt = (TextTitle) title;
|
TextTitle tt = (TextTitle) title;
|
||||||
|
|
||||||
if (tt.isResetBeforeSend()) {
|
if (tt.isResetBeforeSend()) {
|
||||||
minecraftConnection.delayedWrite(TitlePacket.resetForProtocolVersion(protocolVersion));
|
connection.delayedWrite(TitlePacket.resetForProtocolVersion(protocolVersion));
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<Component> titleText = tt.getTitle();
|
Optional<Component> titleText = tt.getTitle();
|
||||||
@ -287,7 +286,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
TitlePacket titlePkt = new TitlePacket();
|
TitlePacket titlePkt = new TitlePacket();
|
||||||
titlePkt.setAction(TitlePacket.SET_TITLE);
|
titlePkt.setAction(TitlePacket.SET_TITLE);
|
||||||
titlePkt.setComponent(GsonComponentSerializer.INSTANCE.serialize(titleText.get()));
|
titlePkt.setComponent(GsonComponentSerializer.INSTANCE.serialize(titleText.get()));
|
||||||
minecraftConnection.delayedWrite(titlePkt);
|
connection.delayedWrite(titlePkt);
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<Component> subtitleText = tt.getSubtitle();
|
Optional<Component> subtitleText = tt.getSubtitle();
|
||||||
@ -295,7 +294,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
TitlePacket titlePkt = new TitlePacket();
|
TitlePacket titlePkt = new TitlePacket();
|
||||||
titlePkt.setAction(TitlePacket.SET_SUBTITLE);
|
titlePkt.setAction(TitlePacket.SET_SUBTITLE);
|
||||||
titlePkt.setComponent(GsonComponentSerializer.INSTANCE.serialize(subtitleText.get()));
|
titlePkt.setComponent(GsonComponentSerializer.INSTANCE.serialize(subtitleText.get()));
|
||||||
minecraftConnection.delayedWrite(titlePkt);
|
connection.delayedWrite(titlePkt);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tt.areTimesSet()) {
|
if (tt.areTimesSet()) {
|
||||||
@ -303,9 +302,9 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
timesPkt.setFadeIn(tt.getFadeIn());
|
timesPkt.setFadeIn(tt.getFadeIn());
|
||||||
timesPkt.setStay(tt.getStay());
|
timesPkt.setStay(tt.getStay());
|
||||||
timesPkt.setFadeOut(tt.getFadeOut());
|
timesPkt.setFadeOut(tt.getFadeOut());
|
||||||
minecraftConnection.delayedWrite(timesPkt);
|
connection.delayedWrite(timesPkt);
|
||||||
}
|
}
|
||||||
minecraftConnection.flush();
|
connection.flush();
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalArgumentException("Unknown title class " + title.getClass().getName());
|
throw new IllegalArgumentException("Unknown title class " + title.getClass().getName());
|
||||||
}
|
}
|
||||||
@ -457,9 +456,9 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
if (newResult == null || !newResult) {
|
if (newResult == null || !newResult) {
|
||||||
disconnect(friendlyReason);
|
disconnect(friendlyReason);
|
||||||
} else {
|
} 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) {
|
} else if (event.getResult() instanceof Notify) {
|
||||||
Notify res = (Notify) event.getResult();
|
Notify res = (Notify) event.getResult();
|
||||||
if (event.kickedDuringServerConnect()) {
|
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.
|
// In case someone gets creative, assume we want to disconnect the player.
|
||||||
disconnect(friendlyReason);
|
disconnect(friendlyReason);
|
||||||
}
|
}
|
||||||
}, minecraftConnection.eventLoop());
|
}, connection.eventLoop());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -584,7 +583,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
Preconditions.checkNotNull(identifier, "identifier");
|
Preconditions.checkNotNull(identifier, "identifier");
|
||||||
Preconditions.checkNotNull(data, "data");
|
Preconditions.checkNotNull(data, "data");
|
||||||
PluginMessage message = new PluginMessage(identifier.getId(), Unpooled.wrappedBuffer(data));
|
PluginMessage message = new PluginMessage(identifier.getId(), Unpooled.wrappedBuffer(data));
|
||||||
minecraftConnection.write(message);
|
connection.write(message);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -603,7 +602,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
ResourcePackRequest request = new ResourcePackRequest();
|
ResourcePackRequest request = new ResourcePackRequest();
|
||||||
request.setUrl(url);
|
request.setUrl(url);
|
||||||
request.setHash("");
|
request.setHash("");
|
||||||
minecraftConnection.write(request);
|
connection.write(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -615,7 +614,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
ResourcePackRequest request = new ResourcePackRequest();
|
ResourcePackRequest request = new ResourcePackRequest();
|
||||||
request.setUrl(url);
|
request.setUrl(url);
|
||||||
request.setHash(ByteBufUtil.hexDump(hash));
|
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.
|
* ID last sent by the server.
|
||||||
*/
|
*/
|
||||||
public void sendKeepAlive() {
|
public void sendKeepAlive() {
|
||||||
if (minecraftConnection.getState() == StateRegistry.PLAY) {
|
if (connection.getState() == StateRegistry.PLAY) {
|
||||||
KeepAlive keepAlive = new KeepAlive();
|
KeepAlive keepAlive = new KeepAlive();
|
||||||
keepAlive.setRandomId(ThreadLocalRandom.current().nextLong());
|
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 (status != null && !status.isSafe()) {
|
||||||
// If it's not safe to continue the connection we need to shut it down.
|
// If it's not safe to continue the connection we need to shut it down.
|
||||||
handleConnectionException(status.getAttemptedConnection(), throwable, true);
|
handleConnectionException(status.getAttemptedConnection(), throwable, true);
|
||||||
|
} else if ((status != null && !status.isSuccessful())) {
|
||||||
|
resetInFlightConnection();
|
||||||
}
|
}
|
||||||
})
|
}, connection.eventLoop())
|
||||||
.thenApply(x -> x);
|
.thenApply(x -> x);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -785,7 +786,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
// The only remaining value is successful (no need to do anything!)
|
// The only remaining value is successful (no need to do anything!)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}, minecraftConnection.eventLoop())
|
}, connection.eventLoop())
|
||||||
.thenApply(Result::isSuccessful);
|
.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.connection.VelocityConstants.VELOCITY_IP_FORWARDING_CHANNEL;
|
||||||
import static com.velocitypowered.proxy.util.EncryptionUtils.decryptRsa;
|
import static com.velocitypowered.proxy.util.EncryptionUtils.decryptRsa;
|
||||||
import static com.velocitypowered.proxy.util.EncryptionUtils.generateServerId;
|
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.base.Preconditions;
|
||||||
import com.google.common.net.UrlEscapers;
|
import com.google.common.net.UrlEscapers;
|
||||||
@ -43,10 +45,14 @@ import java.security.KeyPair;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.ThreadLocalRandom;
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
import net.kyori.text.Component;
|
import net.kyori.text.Component;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
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;
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
|
|
||||||
public class LoginSessionHandler implements MinecraftSessionHandler {
|
public class LoginSessionHandler implements MinecraftSessionHandler {
|
||||||
@ -124,46 +130,50 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
|||||||
String url = String.format(MOJANG_HASJOINED_URL,
|
String url = String.format(MOJANG_HASJOINED_URL,
|
||||||
urlFormParameterEscaper().escape(login.getUsername()), serverId,
|
urlFormParameterEscaper().escape(login.getUsername()), serverId,
|
||||||
urlFormParameterEscaper().escape(playerIp));
|
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
|
ListenableFuture<Response> hasJoinedResponse = server.getAsyncHttpClient().prepareGet(url)
|
||||||
// is enabled.
|
.execute();
|
||||||
try {
|
hasJoinedResponse.addListener(() -> {
|
||||||
mcConnection.enableEncryption(decryptedSharedSecret);
|
if (mcConnection.isClosed()) {
|
||||||
} catch (GeneralSecurityException e) {
|
// The player disconnected after we authenticated them.
|
||||||
throw new RuntimeException(e);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (profileResponse.getCode() == 200) {
|
// Go ahead and enable encryption. Once the client sends EncryptionResponse, encryption
|
||||||
// All went well, initialize the session.
|
// is enabled.
|
||||||
initializePlayer(GSON.fromJson(profileResponse.getBody(), GameProfile.class), true);
|
try {
|
||||||
} else if (profileResponse.getCode() == 204) {
|
mcConnection.enableEncryption(decryptedSharedSecret);
|
||||||
// Apparently an offline-mode user logged onto this online-mode proxy.
|
} catch (GeneralSecurityException e) {
|
||||||
inbound.disconnect(VelocityMessages.ONLINE_MODE_ONLY);
|
throw new RuntimeException(e);
|
||||||
} else {
|
}
|
||||||
// Something else went wrong
|
|
||||||
logger.error(
|
try {
|
||||||
"Got an unexpected error code {} whilst contacting Mojang to log in {} ({})",
|
Response profileResponse = hasJoinedResponse.get();
|
||||||
profileResponse.getCode(), login.getUsername(), playerIp);
|
if (profileResponse.getStatusCode() == 200) {
|
||||||
mcConnection.close();
|
// All went well, initialize the session.
|
||||||
}
|
initializePlayer(GSON.fromJson(profileResponse.getResponseBody(), GameProfile.class),
|
||||||
}, mcConnection.eventLoop())
|
true);
|
||||||
.exceptionally(exception -> {
|
} else if (profileResponse.getStatusCode() == 204) {
|
||||||
logger.error("Unable to enable encryption", exception);
|
// 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();
|
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) {
|
} catch (GeneralSecurityException e) {
|
||||||
logger.error("Unable to enable encryption", e);
|
logger.error("Unable to enable encryption", e);
|
||||||
mcConnection.close();
|
mcConnection.close();
|
||||||
} catch (MalformedURLException e) {
|
|
||||||
throw new AssertionError(e);
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -180,6 +190,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
|||||||
// The player was disconnected
|
// The player was disconnected
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
PreLoginComponentResult result = event.getResult();
|
PreLoginComponentResult result = event.getResult();
|
||||||
Optional<Component> disconnectReason = result.getReason();
|
Optional<Component> disconnectReason = result.getReason();
|
||||||
if (disconnectReason.isPresent()) {
|
if (disconnectReason.isPresent()) {
|
||||||
@ -278,7 +289,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
|||||||
player.disconnect(VelocityMessages.ALREADY_CONNECTED);
|
player.disconnect(VelocityMessages.ALREADY_CONNECTED);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
mcConnection.setSessionHandler(new InitialConnectSessionHandler(player));
|
mcConnection.setSessionHandler(new InitialConnectSessionHandler(player));
|
||||||
server.getEventManager().fire(new PostLoginEvent(player))
|
server.getEventManager().fire(new PostLoginEvent(player))
|
||||||
.thenRun(() -> player.createConnectionRequest(toTry.get()).fireAndForget());
|
.thenRun(() -> player.createConnectionRequest(toTry.get()).fireAndForget());
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
package com.velocitypowered.proxy.connection.client;
|
package com.velocitypowered.proxy.connection.client;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.spotify.futures.CompletableFutures;
|
||||||
import com.velocitypowered.api.event.proxy.ProxyPingEvent;
|
import com.velocitypowered.api.event.proxy.ProxyPingEvent;
|
||||||
import com.velocitypowered.api.network.ProtocolVersion;
|
import com.velocitypowered.api.network.ProtocolVersion;
|
||||||
import com.velocitypowered.api.proxy.InboundConnection;
|
import com.velocitypowered.api.proxy.InboundConnection;
|
||||||
|
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||||
import com.velocitypowered.api.proxy.server.ServerPing;
|
import com.velocitypowered.api.proxy.server.ServerPing;
|
||||||
import com.velocitypowered.api.util.ModInfo;
|
import com.velocitypowered.api.util.ModInfo;
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
import com.velocitypowered.proxy.VelocityServer;
|
||||||
|
import com.velocitypowered.proxy.config.PingPassthroughMode;
|
||||||
import com.velocitypowered.proxy.config.VelocityConfiguration;
|
import com.velocitypowered.proxy.config.VelocityConfiguration;
|
||||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||||
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
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.StatusPing;
|
||||||
import com.velocitypowered.proxy.protocol.packet.StatusRequest;
|
import com.velocitypowered.proxy.protocol.packet.StatusRequest;
|
||||||
import com.velocitypowered.proxy.protocol.packet.StatusResponse;
|
import com.velocitypowered.proxy.protocol.packet.StatusResponse;
|
||||||
|
import com.velocitypowered.proxy.server.VelocityRegisteredServer;
|
||||||
import io.netty.buffer.ByteBuf;
|
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 {
|
public class StatusSessionHandler implements MinecraftSessionHandler {
|
||||||
|
|
||||||
private final VelocityServer server;
|
private final VelocityServer server;
|
||||||
private final MinecraftConnection connection;
|
private final MinecraftConnection connection;
|
||||||
private final InboundConnection inboundWrapper;
|
private final InboundConnection inbound;
|
||||||
|
|
||||||
StatusSessionHandler(VelocityServer server, MinecraftConnection connection,
|
StatusSessionHandler(VelocityServer server, MinecraftConnection connection,
|
||||||
InboundConnection inboundWrapper) {
|
InboundConnection inbound) {
|
||||||
this.server = server;
|
this.server = server;
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
this.inboundWrapper = inboundWrapper;
|
this.inbound = inbound;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ServerPing createInitialPing() {
|
private ServerPing constructLocalPing(ProtocolVersion version) {
|
||||||
VelocityConfiguration configuration = server.getConfiguration();
|
VelocityConfiguration configuration = server.getConfiguration();
|
||||||
ProtocolVersion shownVersion = ProtocolVersion.isSupported(connection.getProtocolVersion())
|
|
||||||
? connection.getProtocolVersion() : ProtocolVersion.MAXIMUM_VERSION;
|
|
||||||
return new ServerPing(
|
return new ServerPing(
|
||||||
new ServerPing.Version(shownVersion.getProtocol(),
|
new ServerPing.Version(version.getProtocol(),
|
||||||
"Velocity " + ProtocolVersion.SUPPORTED_VERSION_STRING),
|
"Velocity " + ProtocolVersion.SUPPORTED_VERSION_STRING),
|
||||||
new ServerPing.Players(server.getPlayerCount(), configuration.getShowMaxPlayers(),
|
new ServerPing.Players(server.getPlayerCount(), configuration.getShowMaxPlayers(),
|
||||||
ImmutableList.of()),
|
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
|
@Override
|
||||||
public boolean handle(LegacyPing packet) {
|
public boolean handle(LegacyPing packet) {
|
||||||
ServerPing initialPing = createInitialPing();
|
getInitialPing()
|
||||||
ProxyPingEvent event = new ProxyPingEvent(inboundWrapper, initialPing);
|
.thenCompose(ping -> server.getEventManager().fire(new ProxyPingEvent(inbound, ping)))
|
||||||
server.getEventManager().fire(event)
|
.thenAcceptAsync(event -> {
|
||||||
.thenRunAsync(() -> {
|
|
||||||
connection.closeWith(LegacyDisconnect.fromServerPing(event.getPing(),
|
connection.closeWith(LegacyDisconnect.fromServerPing(event.getPing(),
|
||||||
packet.getVersion()));
|
packet.getVersion()));
|
||||||
}, connection.eventLoop());
|
}, connection.eventLoop());
|
||||||
@ -65,11 +139,10 @@ public class StatusSessionHandler implements MinecraftSessionHandler {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean handle(StatusRequest packet) {
|
public boolean handle(StatusRequest packet) {
|
||||||
ServerPing initialPing = createInitialPing();
|
getInitialPing()
|
||||||
ProxyPingEvent event = new ProxyPingEvent(inboundWrapper, initialPing);
|
.thenCompose(ping -> server.getEventManager().fire(new ProxyPingEvent(inbound, ping)))
|
||||||
server.getEventManager().fire(event)
|
.thenAcceptAsync(
|
||||||
.thenRunAsync(
|
(event) -> {
|
||||||
() -> {
|
|
||||||
StringBuilder json = new StringBuilder();
|
StringBuilder json = new StringBuilder();
|
||||||
VelocityServer.GSON.toJson(event.getPing(), json);
|
VelocityServer.GSON.toJson(event.getPing(), json);
|
||||||
connection.write(new StatusResponse(json));
|
connection.write(new StatusResponse(json));
|
||||||
|
@ -114,7 +114,7 @@ public enum LegacyForgeHandshakeBackendPhase implements BackendConnectionPhase {
|
|||||||
serverConnection.setConnectionPhase(newPhase);
|
serverConnection.setConnectionPhase(newPhase);
|
||||||
|
|
||||||
// Write the packet to the player, we don't need it now.
|
// Write the packet to the player, we don't need it now.
|
||||||
player.getMinecraftConnection().write(message.retain());
|
player.getConnection().write(message.retain());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,7 +135,7 @@ public enum LegacyForgeHandshakeClientPhase implements ClientConnectionPhase {
|
|||||||
COMPLETE(null) {
|
COMPLETE(null) {
|
||||||
@Override
|
@Override
|
||||||
public void resetConnectionPhase(ConnectedPlayer player) {
|
public void resetConnectionPhase(ConnectedPlayer player) {
|
||||||
player.getMinecraftConnection().write(LegacyForgeUtil.resetPacket());
|
player.getConnection().write(LegacyForgeUtil.resetPacket());
|
||||||
player.setPhase(LegacyForgeHandshakeClientPhase.NOT_STARTED);
|
player.setPhase(LegacyForgeHandshakeClientPhase.NOT_STARTED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
package com.velocitypowered.proxy.network;
|
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.google.common.base.Preconditions;
|
||||||
import com.velocitypowered.natives.util.Natives;
|
import com.velocitypowered.natives.util.Natives;
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
import com.velocitypowered.proxy.VelocityServer;
|
||||||
|
import com.velocitypowered.proxy.network.netty.DnsAddressResolverGroupNameResolverAdapter;
|
||||||
import com.velocitypowered.proxy.protocol.netty.GS4QueryHandler;
|
import com.velocitypowered.proxy.protocol.netty.GS4QueryHandler;
|
||||||
import io.netty.bootstrap.Bootstrap;
|
import io.netty.bootstrap.Bootstrap;
|
||||||
import io.netty.bootstrap.ServerBootstrap;
|
import io.netty.bootstrap.ServerBootstrap;
|
||||||
@ -11,17 +15,26 @@ import io.netty.channel.ChannelFutureListener;
|
|||||||
import io.netty.channel.ChannelOption;
|
import io.netty.channel.ChannelOption;
|
||||||
import io.netty.channel.EventLoopGroup;
|
import io.netty.channel.EventLoopGroup;
|
||||||
import io.netty.channel.WriteBufferWaterMark;
|
import io.netty.channel.WriteBufferWaterMark;
|
||||||
|
import io.netty.channel.epoll.EpollChannelOption;
|
||||||
import io.netty.resolver.dns.DnsAddressResolverGroup;
|
import io.netty.resolver.dns.DnsAddressResolverGroup;
|
||||||
import io.netty.resolver.dns.DnsNameResolverBuilder;
|
import io.netty.resolver.dns.DnsNameResolverBuilder;
|
||||||
|
import io.netty.util.concurrent.EventExecutor;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
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 {
|
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);
|
1 << 21);
|
||||||
private static final Logger LOGGER = LogManager.getLogger(ConnectionManager.class);
|
private static final Logger LOGGER = LogManager.getLogger(ConnectionManager.class);
|
||||||
private final Map<InetSocketAddress, Channel> endpoints = new HashMap<>();
|
private final Map<InetSocketAddress, Channel> endpoints = new HashMap<>();
|
||||||
@ -35,6 +48,7 @@ public final class ConnectionManager {
|
|||||||
public final ServerChannelInitializerHolder serverChannelInitializer;
|
public final ServerChannelInitializerHolder serverChannelInitializer;
|
||||||
|
|
||||||
private final DnsAddressResolverGroup resolverGroup;
|
private final DnsAddressResolverGroup resolverGroup;
|
||||||
|
private final AsyncHttpClient httpClient;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initalizes the {@code ConnectionManager}.
|
* Initalizes the {@code ConnectionManager}.
|
||||||
@ -48,12 +62,26 @@ public final class ConnectionManager {
|
|||||||
this.workerGroup = this.transportType.createEventLoopGroup(TransportType.Type.WORKER);
|
this.workerGroup = this.transportType.createEventLoopGroup(TransportType.Type.WORKER);
|
||||||
this.serverChannelInitializer = new ServerChannelInitializerHolder(
|
this.serverChannelInitializer = new ServerChannelInitializerHolder(
|
||||||
new ServerChannelInitializer(this.server));
|
new ServerChannelInitializer(this.server));
|
||||||
this.resolverGroup = new DnsAddressResolverGroup(
|
this.resolverGroup = new DnsAddressResolverGroup(new DnsNameResolverBuilder()
|
||||||
new DnsNameResolverBuilder()
|
.channelType(this.transportType.datagramChannelClass)
|
||||||
.channelType(this.transportType.datagramChannelClass)
|
.negativeTtl(15)
|
||||||
.negativeTtl(15)
|
.ndots(1));
|
||||||
.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() {
|
public void logChannelInformation() {
|
||||||
@ -75,6 +103,11 @@ public final class ConnectionManager {
|
|||||||
.childOption(ChannelOption.TCP_NODELAY, true)
|
.childOption(ChannelOption.TCP_NODELAY, true)
|
||||||
.childOption(ChannelOption.IP_TOS, 0x18)
|
.childOption(ChannelOption.IP_TOS, 0x18)
|
||||||
.localAddress(address);
|
.localAddress(address);
|
||||||
|
|
||||||
|
if (transportType == TransportType.EPOLL && server.getConfiguration().useTcpFastOpen()) {
|
||||||
|
bootstrap.option(EpollChannelOption.TCP_FASTOPEN, 3);
|
||||||
|
}
|
||||||
|
|
||||||
bootstrap.bind()
|
bootstrap.bind()
|
||||||
.addListener((ChannelFutureListener) future -> {
|
.addListener((ChannelFutureListener) future -> {
|
||||||
final Channel channel = future.channel();
|
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.
|
* 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}
|
* @return a new {@link Bootstrap}
|
||||||
*/
|
*/
|
||||||
public Bootstrap createWorker(EventLoopGroup group) {
|
public Bootstrap createWorker(@Nullable EventLoopGroup group) {
|
||||||
return new Bootstrap()
|
Bootstrap bootstrap = new Bootstrap()
|
||||||
.channel(this.transportType.socketChannelClass)
|
.channel(this.transportType.socketChannelClass)
|
||||||
.group(group)
|
|
||||||
.option(ChannelOption.TCP_NODELAY, true)
|
.option(ChannelOption.TCP_NODELAY, true)
|
||||||
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS,
|
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS,
|
||||||
this.server.getConfiguration().getConnectTimeout())
|
this.server.getConfiguration().getConnectTimeout())
|
||||||
|
.group(group == null ? this.workerGroup : group)
|
||||||
.resolver(this.resolverGroup);
|
.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;
|
return bossGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
public EventLoopGroup getWorkerGroup() {
|
|
||||||
return workerGroup;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ServerChannelInitializerHolder getServerChannelInitializer() {
|
public ServerChannelInitializerHolder getServerChannelInitializer() {
|
||||||
return this.serverChannelInitializer;
|
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.EpollEventLoopGroup;
|
||||||
import io.netty.channel.epoll.EpollServerSocketChannel;
|
import io.netty.channel.epoll.EpollServerSocketChannel;
|
||||||
import io.netty.channel.epoll.EpollSocketChannel;
|
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.nio.NioEventLoopGroup;
|
||||||
import io.netty.channel.socket.DatagramChannel;
|
import io.netty.channel.socket.DatagramChannel;
|
||||||
import io.netty.channel.socket.ServerSocketChannel;
|
import io.netty.channel.socket.ServerSocketChannel;
|
||||||
@ -27,10 +22,7 @@ enum TransportType {
|
|||||||
(name, type) -> new NioEventLoopGroup(0, createThreadFactory(name, type))),
|
(name, type) -> new NioEventLoopGroup(0, createThreadFactory(name, type))),
|
||||||
EPOLL("epoll", EpollServerSocketChannel.class, EpollSocketChannel.class,
|
EPOLL("epoll", EpollServerSocketChannel.class, EpollSocketChannel.class,
|
||||||
EpollDatagramChannel.class,
|
EpollDatagramChannel.class,
|
||||||
(name, type) -> new EpollEventLoopGroup(0, createThreadFactory(name, type))),
|
(name, type) -> new EpollEventLoopGroup(0, createThreadFactory(name, type)));
|
||||||
KQUEUE("Kqueue", KQueueServerSocketChannel.class, KQueueSocketChannel.class,
|
|
||||||
KQueueDatagramChannel.class,
|
|
||||||
(name, type) -> new KQueueEventLoopGroup(0, createThreadFactory(name, type)));
|
|
||||||
|
|
||||||
final String name;
|
final String name;
|
||||||
final Class<? extends ServerSocketChannel> serverSocketChannelClass;
|
final Class<? extends ServerSocketChannel> serverSocketChannelClass;
|
||||||
@ -64,10 +56,12 @@ enum TransportType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static TransportType bestType() {
|
public static TransportType bestType() {
|
||||||
|
if (Boolean.getBoolean("velocity.disable-native-transport")) {
|
||||||
|
return NIO;
|
||||||
|
}
|
||||||
|
|
||||||
if (Epoll.isAvailable()) {
|
if (Epoll.isAvailable()) {
|
||||||
return EPOLL;
|
return EPOLL;
|
||||||
} else if (KQueue.isAvailable()) {
|
|
||||||
return KQUEUE;
|
|
||||||
} else {
|
} else {
|
||||||
return NIO;
|
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",
|
checkFrame(expectedSize >= threshold, "Uncompressed size %s is greater than threshold %s",
|
||||||
expectedSize, threshold);
|
expectedSize, threshold);
|
||||||
checkFrame(expectedSize <= MAXIMUM_UNCOMPRESSED_SIZE, "Expected uncompressed size"
|
int initialCapacity = Math.min(expectedSize, MAXIMUM_UNCOMPRESSED_SIZE);
|
||||||
+ "%s is larger than protocol maximum of %s", expectedSize, MAXIMUM_UNCOMPRESSED_SIZE);
|
|
||||||
ByteBuf compatibleIn = ensureCompatible(ctx.alloc(), compressor, in);
|
ByteBuf compatibleIn = ensureCompatible(ctx.alloc(), compressor, in);
|
||||||
ByteBuf uncompressed = preferredBuffer(ctx.alloc(), compressor, expectedSize);
|
ByteBuf uncompressed = preferredBuffer(ctx.alloc(), compressor, initialCapacity);
|
||||||
try {
|
try {
|
||||||
compressor.inflate(compatibleIn, uncompressed, expectedSize);
|
compressor.inflate(compatibleIn, uncompressed, expectedSize);
|
||||||
out.add(uncompressed);
|
out.add(uncompressed);
|
||||||
|
@ -133,5 +133,9 @@ public class TabCompleteResponse implements MinecraftPacket {
|
|||||||
public int compareTo(Offer o) {
|
public int compareTo(Offer o) {
|
||||||
return this.text.compareTo(o.text);
|
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 CompletableFuture<ServerPing> result;
|
||||||
private final RegisteredServer server;
|
private final RegisteredServer server;
|
||||||
private final MinecraftConnection connection;
|
private final MinecraftConnection connection;
|
||||||
|
private final ProtocolVersion version;
|
||||||
private boolean completed = false;
|
private boolean completed = false;
|
||||||
|
|
||||||
PingSessionHandler(CompletableFuture<ServerPing> result, RegisteredServer server,
|
PingSessionHandler(CompletableFuture<ServerPing> result, RegisteredServer server,
|
||||||
MinecraftConnection connection) {
|
MinecraftConnection connection, ProtocolVersion version) {
|
||||||
this.result = result;
|
this.result = result;
|
||||||
this.server = server;
|
this.server = server;
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
|
this.version = version;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -33,11 +35,13 @@ public class PingSessionHandler implements MinecraftSessionHandler {
|
|||||||
handshake.setNextStatus(StateRegistry.STATUS_ID);
|
handshake.setNextStatus(StateRegistry.STATUS_ID);
|
||||||
handshake.setServerAddress(server.getServerInfo().getAddress().getHostString());
|
handshake.setServerAddress(server.getServerInfo().getAddress().getHostString());
|
||||||
handshake.setPort(server.getServerInfo().getAddress().getPort());
|
handshake.setPort(server.getServerInfo().getAddress().getPort());
|
||||||
handshake.setProtocolVersion(ProtocolVersion.MINIMUM_VERSION);
|
handshake.setProtocolVersion(version);
|
||||||
connection.write(handshake);
|
connection.delayedWrite(handshake);
|
||||||
|
|
||||||
connection.setState(StateRegistry.STATUS);
|
connection.setState(StateRegistry.STATUS);
|
||||||
connection.write(StatusRequest.INSTANCE);
|
connection.delayedWrite(StatusRequest.INSTANCE);
|
||||||
|
|
||||||
|
connection.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -9,6 +9,7 @@ import static com.velocitypowered.proxy.network.Connections.READ_TIMEOUT;
|
|||||||
|
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.velocitypowered.api.network.ProtocolVersion;
|
||||||
import com.velocitypowered.api.proxy.Player;
|
import com.velocitypowered.api.proxy.Player;
|
||||||
import com.velocitypowered.api.proxy.ServerConnection;
|
import com.velocitypowered.api.proxy.ServerConnection;
|
||||||
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
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.MinecraftConnection;
|
||||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
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.MinecraftDecoder;
|
||||||
import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder;
|
import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder;
|
||||||
import com.velocitypowered.proxy.protocol.netty.MinecraftVarintFrameDecoder;
|
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.ChannelFuture;
|
||||||
import io.netty.channel.ChannelFutureListener;
|
import io.netty.channel.ChannelFutureListener;
|
||||||
import io.netty.channel.ChannelInitializer;
|
import io.netty.channel.ChannelInitializer;
|
||||||
|
import io.netty.channel.EventLoop;
|
||||||
import io.netty.handler.timeout.ReadTimeoutHandler;
|
import io.netty.handler.timeout.ReadTimeoutHandler;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@ -59,11 +60,22 @@ public class VelocityRegisteredServer implements RegisteredServer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CompletableFuture<ServerPing> ping() {
|
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) {
|
if (server == null) {
|
||||||
throw new IllegalStateException("No Velocity proxy instance available");
|
throw new IllegalStateException("No Velocity proxy instance available");
|
||||||
}
|
}
|
||||||
CompletableFuture<ServerPing> pingFuture = new CompletableFuture<>();
|
CompletableFuture<ServerPing> pingFuture = new CompletableFuture<>();
|
||||||
server.initializeGenericBootstrap()
|
server.createBootstrap(loop)
|
||||||
.handler(new ChannelInitializer<Channel>() {
|
.handler(new ChannelInitializer<Channel>() {
|
||||||
@Override
|
@Override
|
||||||
protected void initChannel(Channel ch) throws Exception {
|
protected void initChannel(Channel ch) throws Exception {
|
||||||
@ -87,8 +99,8 @@ public class VelocityRegisteredServer implements RegisteredServer {
|
|||||||
public void operationComplete(ChannelFuture future) throws Exception {
|
public void operationComplete(ChannelFuture future) throws Exception {
|
||||||
if (future.isSuccess()) {
|
if (future.isSuccess()) {
|
||||||
MinecraftConnection conn = future.channel().pipeline().get(MinecraftConnection.class);
|
MinecraftConnection conn = future.channel().pipeline().get(MinecraftConnection.class);
|
||||||
conn.setSessionHandler(
|
conn.setSessionHandler(new PingSessionHandler(
|
||||||
new PingSessionHandler(pingFuture, VelocityRegisteredServer.this, conn));
|
pingFuture, VelocityRegisteredServer.this, conn, version));
|
||||||
} else {
|
} else {
|
||||||
pingFuture.completeExceptionally(future.cause());
|
pingFuture.completeExceptionally(future.cause());
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package com.velocitypowered.proxy.util;
|
package com.velocitypowered.proxy.util;
|
||||||
|
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
|
import com.google.common.net.InetAddresses;
|
||||||
|
import java.net.InetAddress;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
|
||||||
@ -19,7 +21,12 @@ public class AddressUtil {
|
|||||||
public static InetSocketAddress parseAddress(String ip) {
|
public static InetSocketAddress parseAddress(String ip) {
|
||||||
Preconditions.checkNotNull(ip, "ip");
|
Preconditions.checkNotNull(ip, "ip");
|
||||||
URI uri = URI.create("tcp://" + 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
|
public static final Component ALREADY_CONNECTED = TextComponent
|
||||||
.of("You are already connected to this proxy!", TextColor.RED);
|
.of("You are already connected to this proxy!", TextColor.RED);
|
||||||
public static final Component MOVED_TO_NEW_SERVER = TextComponent
|
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() {
|
private VelocityMessages() {
|
||||||
throw new AssertionError();
|
throw new AssertionError();
|
||||||
|
@ -271,6 +271,6 @@ public class VelocityBossBar implements com.velocitypowered.api.util.bossbar.Bos
|
|||||||
|
|
||||||
private void sendPacket(Player player, MinecraftPacket packet) {
|
private void sendPacket(Player player, MinecraftPacket packet) {
|
||||||
ConnectedPlayer connected = (ConnectedPlayer) player;
|
ConnectedPlayer connected = (ConnectedPlayer) player;
|
||||||
connected.getMinecraftConnection().write(packet);
|
connected.getConnection().write(packet);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1 +1,2 @@
|
|||||||
log4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
|
log4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
|
||||||
|
log4j.skipJansi=true
|
||||||
|
Laden…
In neuem Issue referenzieren
Einen Benutzer sperren