13
0
geforkt von Mirrors/Velocity

Merge branch 'dev/1.1.0' into natives-java-improvements

# Conflicts:
#	native/src/main/java/com/velocitypowered/natives/encryption/JavaVelocityCipher.java
Dieser Commit ist enthalten in:
Andrew Steinborn 2019-09-07 00:06:13 -04:00
Commit 35856fec04
53 geänderte Dateien mit 1422 neuen und 856 gelöschten Zeilen

Datei anzeigen

@ -7,4 +7,4 @@ cache:
- $HOME/.gradle/caches/
- $HOME/.gradle/wrapper/
jdk:
- oraclejdk8
- openjdk8

Datei anzeigen

@ -34,15 +34,4 @@ Once you've built Velocity, you can copy and run the `-all` JAR from
and you can configure it from there.
Alternatively, you can get the proxy JAR from the [downloads](https://www.velocitypowered.com/downloads)
page.
## Status
Velocity is currently in beta. Production networks are successfully running
Velocity with many hundreds of concurrent players online, but your mileage
may vary.
Velocity supports Minecraft 1.8-1.14.2. Velocity is best supported with Paper
and SpongeVanilla. Minecraft Forge is fully supported but mod compatibility
may vary. Generally, Velocity will support many mods better than BungeeCord
or Waterfall do but compatibility can not always be ensured.
page.

Datei anzeigen

@ -24,9 +24,13 @@ dependencies {
compile "net.kyori:text-serializer-plain:${textVersion}"
compile 'com.moandjiezana.toml:toml4j:0.7.2'
compile "org.slf4j:slf4j-api:${slf4jVersion}"
compile 'com.google.inject:guice:4.2.0'
compile 'com.google.inject:guice:4.2.2'
compile "org.checkerframework:checker-qual:${checkerFrameworkVersion}"
compile "org.spongepowered:configurate-hocon:${configurateVersion}"
compile "org.spongepowered:configurate-yaml:${configurateVersion}"
compile "org.spongepowered:configurate-gson:${configurateVersion}"
testCompile "org.junit.jupiter:junit-jupiter-api:${junitVersion}"
testCompile "org.junit.jupiter:junit-jupiter-engine:${junitVersion}"
}

Datei anzeigen

@ -10,9 +10,22 @@ public interface CommandManager {
*
* @param command the command to register
* @param aliases the alias to use
*
* @deprecated This method requires at least one alias, but this is only enforced at runtime.
* Prefer {@link #register(String, Command, String...)} instead.
*/
@Deprecated
void register(Command command, String... aliases);
/**
* Registers the specified command with the manager with the specified aliases.
*
* @param alias the first alias to register
* @param command the command to register
* @param otherAliases the other aliases to use
*/
void register(String alias, Command command, String... otherAliases);
/**
* Unregisters a command.
*

Datei anzeigen

@ -0,0 +1,60 @@
package com.velocitypowered.api.command;
import com.google.common.collect.ImmutableList;
import java.util.List;
import org.checkerframework.checker.nullness.qual.NonNull;
/**
* A specialized sub-interface of {@code Command} which indicates that the proxy should pass a
* raw command to the command. This is useful for bolting on external command frameworks to
* Velocity.
*/
public interface RawCommand extends Command {
/**
* Executes the command for the specified {@link CommandSource}.
*
* @param source the source of this command
* @param commandLine the full command line after the command name
*/
void execute(CommandSource source, String commandLine);
default void execute(CommandSource source, String @NonNull [] args) {
execute(source, String.join(" ", args));
}
/**
* Provides tab complete suggestions for a command for a specified {@link CommandSource}.
*
* @param source the source to run the command for
* @param currentLine the current, partial command line for this command
* @return tab complete suggestions
*/
default List<String> suggest(CommandSource source, String currentLine) {
return ImmutableList.of();
}
@Override
default List<String> suggest(CommandSource source, String @NonNull [] currentArgs) {
return suggest(source, String.join(" ", currentArgs));
}
@Override
default boolean hasPermission(CommandSource source, String @NonNull [] args) {
return hasPermission(source, String.join(" ", args));
}
/**
* Tests to check if the {@code source} has permission to use this command with the provided
* {@code args}.
*
* <p>If this method returns false, the handling will be forwarded onto
* the players current server.</p>
*
* @param source the source of the command
* @param commandLine the arguments for this command
* @return whether the source has permission
*/
default boolean hasPermission(CommandSource source, String commandLine) {
return true;
}
}

Datei anzeigen

@ -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
+ '}';
}
}

Datei anzeigen

@ -29,7 +29,9 @@ public enum ProtocolVersion {
MINECRAFT_1_13_2(404, "1.13.2"),
MINECRAFT_1_14(477, "1.14"),
MINECRAFT_1_14_1(480, "1.14.1"),
MINECRAFT_1_14_2(485, "1.14.2");
MINECRAFT_1_14_2(485, "1.14.2"),
MINECRAFT_1_14_3(490, "1.14.3"),
MINECRAFT_1_14_4(498, "1.14.4");
private final int protocol;
private final String name;

Datei anzeigen

@ -37,12 +37,20 @@ public interface TabList {
* Removes the {@link TabListEntry} from the tab list with the {@link GameProfile} identified with
* the specified {@link UUID}.
*
* @param uuid of the
* @param uuid of the entry
* @return {@link Optional} containing the removed {@link TabListEntry} if present, otherwise
* {@link Optional#empty()}
*/
Optional<TabListEntry> removeEntry(UUID uuid);
/**
* Determines if the specified entry exists in the tab list.
*
* @param uuid the UUID of the entry
* @return {@code true} if it exists, {@code false} if it does not
*/
boolean containsEntry(UUID uuid);
/**
* Returns an immutable {@link Collection} of the {@link TabListEntry}s in the tab list.
*

Datei anzeigen

@ -182,6 +182,19 @@ public final class ServerPing {
return this;
}
/**
* Uses the modified {@code mods} list in the response.
* @param mods the mods list to use
* @return this build, for chaining
*/
public Builder mods(ModInfo mods) {
Preconditions.checkNotNull(mods, "mods");
this.modType = mods.getType();
this.mods.clear();
this.mods.addAll(mods.getMods());
return this;
}
public Builder clearMods() {
this.mods.clear();
return this;

Datei anzeigen

@ -13,20 +13,21 @@ plugins {
allprojects {
group 'com.velocitypowered'
version '1.0.0-SNAPSHOT'
version '1.1.0-SNAPSHOT'
sourceCompatibility = 1.8
targetCompatibility = 1.8
ext {
// dependency versions
textVersion = '3.0.1'
textVersion = '3.0.2'
junitVersion = '5.3.0-M1'
slf4jVersion = '1.7.25'
log4jVersion = '2.11.2'
nettyVersion = '4.1.35.Final'
nettyVersion = '4.1.38.Final'
guavaVersion = '25.1-jre'
checkerFrameworkVersion = '2.7.0'
configurateVersion = '3.6'
getCurrentShortRevision = {
new ByteArrayOutputStream().withStream { os ->
@ -53,6 +54,12 @@ allprojects {
maven {
url "https://libraries.minecraft.net"
}
// Configurate
maven {
name = 'sponge'
url = 'https://repo.spongepowered.org/maven'
}
}
test {

Datei anzeigen

@ -56,11 +56,16 @@ public class NativeVelocityCipher implements VelocityCipher {
int len = source.readableBytes();
ByteBuf out = ctx.alloc().directBuffer(len);
impl.process(this.ctx, source.memoryAddress() + source.readerIndex(), len,
out.memoryAddress(), encrypt);
source.skipBytes(len);
out.writerIndex(len);
return out;
try {
impl.process(this.ctx, source.memoryAddress() + source.readerIndex(), len,
out.memoryAddress(), encrypt);
source.skipBytes(len);
out.writerIndex(len);
return out;
} catch (Exception e) {
out.release();
throw e;
}
}
@Override

Datei anzeigen

@ -13,7 +13,12 @@ apply plugin: 'com.github.johnrengelman.shadow'
jar {
manifest {
def buildNumber = System.getenv("BUILD_NUMBER") ?: "unknown"
def version = "${project.version} (git-${project.ext.getCurrentShortRevision()}-b${buildNumber})"
def version
if (project.version.endsWith("-SNAPSHOT")) {
version = "${project.version} (git-${project.ext.getCurrentShortRevision()}-b${buildNumber})"
} else {
version = "${project.version}"
}
attributes 'Main-Class': 'com.velocitypowered.proxy.Velocity'
attributes 'Implementation-Title': "Velocity"
@ -43,7 +48,6 @@ dependencies {
compile "io.netty:netty-handler:${nettyVersion}"
compile "io.netty:netty-transport-native-epoll:${nettyVersion}"
compile "io.netty:netty-transport-native-epoll:${nettyVersion}:linux-x86_64"
compile "io.netty:netty-transport-native-kqueue:${nettyVersion}:osx-x86_64"
compile "io.netty:netty-resolver-dns:${nettyVersion}"
compile "org.apache.logging.log4j:log4j-api:${log4jVersion}"
@ -52,14 +56,18 @@ dependencies {
compile "org.apache.logging.log4j:log4j-iostreams:${log4jVersion}"
compile 'net.sf.jopt-simple:jopt-simple:5.0.4' // command-line options
compile 'net.minecrell:terminalconsoleappender:1.1.1'
runtime 'net.java.dev.jna:jna:4.5.2' // Needed for JLine
compile 'net.minecrell:terminalconsoleappender:1.2.0'
runtime 'org.jline:jline-terminal-jansi:3.12.1' // Needed for JLine
runtime 'com.lmax:disruptor:3.4.2' // Async loggers
compile 'it.unimi.dsi:fastutil:8.2.2'
compile 'it.unimi.dsi:fastutil:8.2.3'
compile 'net.kyori:event-method-asm:3.0.0'
compile 'com.mojang:brigadier:1.0.15'
compile 'org.asynchttpclient:async-http-client:2.10.1'
compile 'com.spotify:completable-futures:0.3.2'
testCompile "org.junit.jupiter:junit-jupiter-api:${junitVersion}"
testCompile "org.junit.jupiter:junit-jupiter-engine:${junitVersion}"

Datei anzeigen

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

Datei anzeigen

@ -30,7 +30,6 @@ import com.velocitypowered.proxy.config.VelocityConfiguration;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.console.VelocityConsole;
import com.velocitypowered.proxy.network.ConnectionManager;
import com.velocitypowered.proxy.network.http.NettyHttpClient;
import com.velocitypowered.proxy.plugin.VelocityEventManager;
import com.velocitypowered.proxy.plugin.VelocityPluginManager;
import com.velocitypowered.proxy.protocol.packet.Chat;
@ -59,18 +58,26 @@ import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.IntFunction;
import java.util.stream.Collectors;
import net.kyori.text.Component;
import net.kyori.text.TextComponent;
import net.kyori.text.TranslatableComponent;
import net.kyori.text.serializer.gson.GsonComponentSerializer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.asynchttpclient.AsyncHttpClient;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
public class VelocityServer implements ProxyServer {
@ -84,7 +91,6 @@ public class VelocityServer implements ProxyServer {
private final ConnectionManager cm;
private final ProxyOptions options;
private @MonotonicNonNull VelocityConfiguration configuration;
private @MonotonicNonNull NettyHttpClient httpClient;
private @MonotonicNonNull KeyPair serverKeyPair;
private final ServerMap servers;
private final VelocityCommandManager commandManager = new VelocityCommandManager();
@ -167,10 +173,10 @@ public class VelocityServer implements ProxyServer {
cm.logChannelInformation();
// Initialize commands first
commandManager.register(new VelocityCommand(this), "velocity");
commandManager.register(new ServerCommand(this), "server");
commandManager.register(new ShutdownCommand(this), "shutdown", "end");
commandManager.register(new GlistCommand(this), "glist");
commandManager.register("velocity", new VelocityCommand(this));
commandManager.register("server", new ServerCommand(this));
commandManager.register("shutdown", new ShutdownCommand(this),"end");
commandManager.register("glist", new GlistCommand(this));
try {
Path configPath = Paths.get("velocity.toml");
@ -196,7 +202,6 @@ public class VelocityServer implements ProxyServer {
}
ipAttemptLimiter = Ratelimiters.createWithMilliseconds(configuration.getLoginRatelimit());
httpClient = new NettyHttpClient(this);
loadPlugins();
// Go ahead and fire the proxy initialization event. We block since plugins should have a chance
@ -259,15 +264,7 @@ public class VelocityServer implements ProxyServer {
logger.info("Loaded {} plugins", pluginManager.getPlugins().size());
}
public EventLoopGroup getWorkerGroup() {
return this.cm.getWorkerGroup();
}
public Bootstrap initializeGenericBootstrap() {
return this.cm.createWorker();
}
public Bootstrap initializeGenericBootstrap(EventLoopGroup group) {
public Bootstrap createBootstrap(@Nullable EventLoopGroup group) {
return this.cm.createWorker(group);
}
@ -379,14 +376,35 @@ public class VelocityServer implements ProxyServer {
Runnable shutdownProcess = () -> {
logger.info("Shutting down the proxy...");
for (ConnectedPlayer player : ImmutableList.copyOf(connectionsByUuid.values())) {
// Shutdown the connection manager, this should be
// done first to refuse new connections
cm.shutdown();
ImmutableList<ConnectedPlayer> players = ImmutableList.copyOf(connectionsByUuid.values());
for (ConnectedPlayer player : players) {
player.disconnect(TextComponent.of("Proxy shutting down."));
}
this.cm.shutdown();
try {
if (!eventManager.shutdown() || !scheduler.shutdown()) {
boolean timedOut = false;
try {
// Wait for the connections finish tearing down, this
// makes sure that all the disconnect events are being fired
CompletableFuture<Void> playersTeardownFuture = CompletableFuture.allOf(players.stream()
.map(ConnectedPlayer::getTeardownFuture)
.toArray((IntFunction<CompletableFuture<Void>[]>) CompletableFuture[]::new));
playersTeardownFuture.get(10, TimeUnit.SECONDS);
} catch (TimeoutException | ExecutionException e) {
timedOut = true;
}
timedOut = !eventManager.shutdown() || timedOut;
timedOut = !scheduler.shutdown() || timedOut;
if (timedOut) {
logger.error("Your plugins took over 10 seconds to shut down.");
}
} catch (InterruptedException e) {
@ -407,8 +425,8 @@ public class VelocityServer implements ProxyServer {
thread.start();
}
public NettyHttpClient getHttpClient() {
return ensureInitialized(httpClient);
public AsyncHttpClient getAsyncHttpClient() {
return ensureInitialized(cm).getHttpClient();
}
public Ratelimiter getIpAttemptLimiter() {
@ -428,6 +446,9 @@ public class VelocityServer implements ProxyServer {
* @return {@code true} if we can register the connection, {@code false} if not
*/
public boolean canRegisterConnection(ConnectedPlayer connection) {
if (configuration.isOnlineMode() && configuration.isOnlineModeKickExistingPlayers()) {
return true;
}
String lowerName = connection.getUsername().toLowerCase(Locale.US);
return !(connectionsByName.containsKey(lowerName)
|| connectionsByUuid.containsKey(connection.getUniqueId()));
@ -440,12 +461,24 @@ public class VelocityServer implements ProxyServer {
*/
public boolean registerConnection(ConnectedPlayer connection) {
String lowerName = connection.getUsername().toLowerCase(Locale.US);
if (connectionsByName.putIfAbsent(lowerName, connection) != null) {
return false;
}
if (connectionsByUuid.putIfAbsent(connection.getUniqueId(), connection) != null) {
connectionsByName.remove(lowerName, connection);
return false;
if (!this.configuration.isOnlineModeKickExistingPlayers()) {
if (connectionsByName.putIfAbsent(lowerName, connection) != null) {
return false;
}
if (connectionsByUuid.putIfAbsent(connection.getUniqueId(), connection) != null) {
connectionsByName.remove(lowerName, connection);
return false;
}
} else {
ConnectedPlayer existing = connectionsByUuid.get(connection.getUniqueId());
if (existing != null) {
existing.disconnect(TranslatableComponent.of("multiplayer.disconnect.duplicate_login"));
}
// We can now replace the entries as needed.
connectionsByName.put(lowerName, connection);
connectionsByUuid.put(connection.getUniqueId(), connection);
}
return true;
}
@ -472,7 +505,7 @@ public class VelocityServer implements ProxyServer {
Preconditions.checkNotNull(component, "component");
Chat chat = Chat.createClientbound(component);
for (ConnectedPlayer player : connectionsByUuid.values()) {
player.getMinecraftConnection().write(chat);
player.getConnection().write(chat);
}
}

Datei anzeigen

@ -6,27 +6,38 @@ import com.google.common.collect.ImmutableSet;
import com.velocitypowered.api.command.Command;
import com.velocitypowered.api.command.CommandManager;
import com.velocitypowered.api.command.CommandSource;
import java.util.ArrayList;
import com.velocitypowered.api.command.RawCommand;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.checkerframework.checker.nullness.qual.NonNull;
public class VelocityCommandManager implements CommandManager {
private final Map<String, Command> commands = new HashMap<>();
private final Map<String, RawCommand> commands = new HashMap<>();
@Override
@Deprecated
public void register(final Command command, final String... aliases) {
Preconditions.checkNotNull(aliases, "aliases");
Preconditions.checkArgument(aliases.length > 0, "no aliases provided");
register(aliases[0], command, Arrays.copyOfRange(aliases, 1, aliases.length));
}
@Override
public void register(String alias, Command command, String... otherAliases) {
Preconditions.checkNotNull(alias, "alias");
Preconditions.checkNotNull(otherAliases, "otherAliases");
Preconditions.checkNotNull(command, "executor");
for (int i = 0, length = aliases.length; i < length; i++) {
final String alias = aliases[i];
Preconditions.checkNotNull(alias, "alias at index %s", i);
this.commands.put(alias.toLowerCase(Locale.ENGLISH), command);
RawCommand rawCmd = RegularCommandWrapper.wrap(command);
this.commands.put(alias.toLowerCase(Locale.ENGLISH), rawCmd);
for (int i = 0, length = otherAliases.length; i < length; i++) {
final String alias1 = otherAliases[i];
Preconditions.checkNotNull(alias1, "alias at index %s", i + 1);
this.commands.put(alias1.toLowerCase(Locale.ENGLISH), rawCmd);
}
}
@ -41,25 +52,23 @@ public class VelocityCommandManager implements CommandManager {
Preconditions.checkNotNull(source, "invoker");
Preconditions.checkNotNull(cmdLine, "cmdLine");
String[] split = cmdLine.split(" ", -1);
if (split.length == 0) {
return false;
String alias = cmdLine;
String args = "";
int firstSpace = cmdLine.indexOf(' ');
if (firstSpace != -1) {
alias = cmdLine.substring(0, firstSpace);
args = cmdLine.substring(firstSpace).trim();
}
String alias = split[0];
Command command = commands.get(alias.toLowerCase(Locale.ENGLISH));
RawCommand command = commands.get(alias.toLowerCase(Locale.ENGLISH));
if (command == null) {
return false;
}
@SuppressWarnings("nullness")
String[] actualArgs = Arrays.copyOfRange(split, 1, split.length);
try {
if (!command.hasPermission(source, actualArgs)) {
if (!command.hasPermission(source, args)) {
return false;
}
command.execute(source, actualArgs);
command.execute(source, args);
return true;
} catch (Exception e) {
throw new RuntimeException("Unable to invoke command " + cmdLine + " for " + source, e);
@ -84,18 +93,12 @@ public class VelocityCommandManager implements CommandManager {
Preconditions.checkNotNull(source, "source");
Preconditions.checkNotNull(cmdLine, "cmdLine");
String[] split = cmdLine.split(" ", -1);
if (split.length == 0) {
// No command available.
return ImmutableList.of();
}
String alias = split[0];
if (split.length == 1) {
int firstSpace = cmdLine.indexOf(' ');
if (firstSpace == -1) {
// Offer to fill in commands.
ImmutableList.Builder<String> availableCommands = ImmutableList.builder();
for (Map.Entry<String, Command> entry : commands.entrySet()) {
if (entry.getKey().regionMatches(true, 0, alias, 0, alias.length())
for (Map.Entry<String, RawCommand> entry : commands.entrySet()) {
if (entry.getKey().regionMatches(true, 0, cmdLine, 0, cmdLine.length())
&& entry.getValue().hasPermission(source, new String[0])) {
availableCommands.add("/" + entry.getKey());
}
@ -103,23 +106,22 @@ public class VelocityCommandManager implements CommandManager {
return availableCommands.build();
}
Command command = commands.get(alias.toLowerCase(Locale.ENGLISH));
String alias = cmdLine.substring(0, firstSpace);
String args = cmdLine.substring(firstSpace).trim();
RawCommand command = commands.get(alias.toLowerCase(Locale.ENGLISH));
if (command == null) {
// No such command, so we can't offer any tab complete suggestions.
return ImmutableList.of();
}
@SuppressWarnings("nullness")
String[] actualArgs = Arrays.copyOfRange(split, 1, split.length);
try {
if (!command.hasPermission(source, actualArgs)) {
if (!command.hasPermission(source, args)) {
return ImmutableList.of();
}
return ImmutableList.copyOf(command.suggest(source, actualArgs));
return ImmutableList.copyOf(command.suggest(source, args));
} catch (Exception e) {
throw new RuntimeException(
"Unable to invoke suggestions for command " + alias + " for " + source, e);
"Unable to invoke suggestions for command " + cmdLine + " for " + source, e);
}
}
@ -133,26 +135,61 @@ public class VelocityCommandManager implements CommandManager {
Preconditions.checkNotNull(source, "source");
Preconditions.checkNotNull(cmdLine, "cmdLine");
String[] split = cmdLine.split(" ", -1);
if (split.length == 0) {
// No command available.
return false;
String alias = cmdLine;
String args = "";
int firstSpace = cmdLine.indexOf(' ');
if (firstSpace != -1) {
alias = cmdLine.substring(0, firstSpace);
args = cmdLine.substring(firstSpace).trim();
}
String alias = split[0];
Command command = commands.get(alias.toLowerCase(Locale.ENGLISH));
RawCommand command = commands.get(alias.toLowerCase(Locale.ENGLISH));
if (command == null) {
// No such command.
return false;
}
@SuppressWarnings("nullness")
String[] actualArgs = Arrays.copyOfRange(split, 1, split.length);
try {
return command.hasPermission(source, actualArgs);
return command.hasPermission(source, args);
} catch (Exception e) {
throw new RuntimeException(
"Unable to invoke suggestions for command " + alias + " for " + source, e);
}
}
private static class RegularCommandWrapper implements RawCommand {
private final Command delegate;
private RegularCommandWrapper(Command delegate) {
this.delegate = delegate;
}
private static String[] split(String line) {
if (line.isEmpty()) {
return new String[0];
}
return line.split(" ", -1);
}
@Override
public void execute(CommandSource source, String commandLine) {
delegate.execute(source, split(commandLine));
}
@Override
public List<String> suggest(CommandSource source, String currentLine) {
return delegate.suggest(source, split(currentLine));
}
@Override
public boolean hasPermission(CommandSource source, String commandLine) {
return delegate.hasPermission(source, split(commandLine));
}
static RawCommand wrap(Command command) {
if (command instanceof RawCommand) {
return (RawCommand) command;
}
return new RegularCommandWrapper(command);
}
}
}

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

@ -5,26 +5,25 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import com.mojang.brigadier.tree.LiteralCommandNode;
import com.velocitypowered.api.event.connection.PluginMessageEvent;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler;
import com.velocitypowered.proxy.connection.forge.legacy.LegacyForgeConstants;
import com.velocitypowered.proxy.connection.util.ConnectionMessages;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.packet.AvailableCommands;
import com.velocitypowered.proxy.protocol.packet.AvailableCommands.ProtocolSuggestionProvider;
import com.velocitypowered.proxy.protocol.packet.BossBar;
import com.velocitypowered.proxy.protocol.packet.Disconnect;
import com.velocitypowered.proxy.protocol.packet.JoinGame;
import com.velocitypowered.proxy.protocol.packet.KeepAlive;
import com.velocitypowered.proxy.protocol.packet.PlayerListItem;
import com.velocitypowered.proxy.protocol.packet.PluginMessage;
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse;
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
public class BackendPlaySessionHandler implements MinecraftSessionHandler {
@ -37,7 +36,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
BackendPlaySessionHandler(VelocityServer server, VelocityServerConnection serverConn) {
this.server = server;
this.serverConn = serverConn;
this.playerConnection = serverConn.getPlayer().getMinecraftConnection();
this.playerConnection = serverConn.getPlayer().getConnection();
MinecraftSessionHandler psh = playerConnection.getSessionHandler();
if (!(psh instanceof ClientPlaySessionHandler)) {
@ -119,12 +118,15 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
return false;
}
byte[] copy = ByteBufUtil.getBytes(packet.content());
PluginMessageEvent event = new PluginMessageEvent(serverConn, serverConn.getPlayer(), id,
packet.getData());
copy);
server.getEventManager().fire(event)
.thenAcceptAsync(pme -> {
if (pme.getResult().isAllowed() && !playerConnection.isClosed()) {
playerConnection.write(packet);
PluginMessage copied = new PluginMessage(packet.getChannel(),
Unpooled.wrappedBuffer(copy));
playerConnection.write(copied);
}
}, playerConnection.eventLoop());
return true;
@ -163,6 +165,9 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
@Override
public void handleGeneric(MinecraftPacket packet) {
if (packet instanceof PluginMessage) {
((PluginMessage) packet).retain();
}
playerConnection.write(packet);
}
@ -185,7 +190,8 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
public void disconnected() {
serverConn.getServer().removePlayer(serverConn.getPlayer());
if (!serverConn.isGracefulDisconnect() && !exceptionTriggered) {
serverConn.getPlayer().disconnect(ConnectionMessages.UNEXPECTED_DISCONNECT);
serverConn.getPlayer().handleConnectionException(serverConn.getServer(),
Disconnect.create(ConnectionMessages.UNEXPECTED_DISCONNECT), true);
}
}
}

Datei anzeigen

@ -1,6 +1,5 @@
package com.velocitypowered.proxy.connection.backend;
import com.velocitypowered.api.proxy.ConnectionRequestBuilder;
import com.velocitypowered.api.util.GameProfile;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.config.PlayerInfoForwarding;
@ -56,23 +55,16 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
MinecraftConnection mc = serverConn.ensureConnected();
VelocityConfiguration configuration = server.getConfiguration();
if (configuration.getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN && packet
.getChannel()
.equals(VelocityConstants.VELOCITY_IP_FORWARDING_CHANNEL)) {
LoginPluginResponse response = new LoginPluginResponse();
response.setSuccess(true);
response.setId(packet.getId());
response.setData(createForwardingData(configuration.getForwardingSecret(),
.getChannel().equals(VelocityConstants.VELOCITY_IP_FORWARDING_CHANNEL)) {
ByteBuf forwardingData = createForwardingData(configuration.getForwardingSecret(),
serverConn.getPlayer().getRemoteAddress().getHostString(),
serverConn.getPlayer().getGameProfile()));
serverConn.getPlayer().getGameProfile());
LoginPluginResponse response = new LoginPluginResponse(packet.getId(), true, forwardingData);
mc.write(response);
informationForwarded = true;
} else {
// Don't understand
LoginPluginResponse response = new LoginPluginResponse();
response.setSuccess(false);
response.setId(packet.getId());
response.setData(Unpooled.EMPTY_BUFFER);
mc.write(response);
mc.write(new LoginPluginResponse(packet.getId(), false, Unpooled.EMPTY_BUFFER));
}
return true;
}
@ -125,32 +117,28 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
private static ByteBuf createForwardingData(byte[] hmacSecret, String address,
GameProfile profile) {
ByteBuf dataToForward = Unpooled.buffer();
ByteBuf finalData = Unpooled.buffer();
ByteBuf forwarded = Unpooled.buffer(2048);
try {
ProtocolUtils.writeVarInt(dataToForward, VelocityConstants.FORWARDING_VERSION);
ProtocolUtils.writeString(dataToForward, address);
ProtocolUtils.writeUuid(dataToForward, profile.getId());
ProtocolUtils.writeString(dataToForward, profile.getName());
ProtocolUtils.writeProperties(dataToForward, profile.getProperties());
ProtocolUtils.writeVarInt(forwarded, VelocityConstants.FORWARDING_VERSION);
ProtocolUtils.writeString(forwarded, address);
ProtocolUtils.writeUuid(forwarded, profile.getId());
ProtocolUtils.writeString(forwarded, profile.getName());
ProtocolUtils.writeProperties(forwarded, profile.getProperties());
SecretKey key = new SecretKeySpec(hmacSecret, "HmacSHA256");
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(key);
mac.update(dataToForward.array(), dataToForward.arrayOffset(), dataToForward.readableBytes());
mac.update(forwarded.array(), forwarded.arrayOffset(), forwarded.readableBytes());
byte[] sig = mac.doFinal();
finalData.writeBytes(sig);
finalData.writeBytes(dataToForward);
return finalData;
return Unpooled.wrappedBuffer(Unpooled.wrappedBuffer(sig), forwarded);
} catch (InvalidKeyException e) {
finalData.release();
forwarded.release();
throw new RuntimeException("Unable to authenticate data", e);
} catch (NoSuchAlgorithmException e) {
// Should never happen
finalData.release();
forwarded.release();
throw new AssertionError(e);
} finally {
dataToForward.release();
}
}
}

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

@ -6,6 +6,7 @@ import static com.velocitypowered.proxy.protocol.util.PluginMessageUtil.construc
import com.velocitypowered.api.event.connection.PluginMessageEvent;
import com.velocitypowered.api.event.player.PlayerChatEvent;
import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent;
import com.velocitypowered.api.event.player.TabCompleteEvent;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
import com.velocitypowered.proxy.VelocityServer;
@ -29,14 +30,15 @@ import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse.Offer;
import com.velocitypowered.proxy.protocol.packet.TitlePacket;
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import io.netty.util.ReferenceCountUtil;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Queue;
import java.util.Set;
import java.util.UUID;
import net.kyori.text.TextComponent;
import net.kyori.text.format.TextColor;
@ -57,7 +59,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
private final List<UUID> serverBossBars = new ArrayList<>();
private final Queue<PluginMessage> loginPluginMessages = new ArrayDeque<>();
private final VelocityServer server;
private @Nullable TabCompleteRequest legacyCommandTabComplete;
private @Nullable TabCompleteRequest outstandingTabComplete;
/**
* Constructs a client play session handler.
@ -75,11 +77,18 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
.getProtocolVersion());
if (!channels.isEmpty()) {
PluginMessage register = constructChannelsPacket(player.getProtocolVersion(), channels);
player.getMinecraftConnection().write(register);
player.getConnection().write(register);
player.getKnownChannels().addAll(channels);
}
}
@Override
public void deactivated() {
for (PluginMessage message : loginPluginMessages) {
ReferenceCountUtil.release(message);
}
}
@Override
public boolean handle(KeepAlive packet) {
VelocityServerConnection serverConnection = player.getConnectedServer();
@ -146,25 +155,226 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
public boolean handle(TabCompleteRequest packet) {
boolean isCommand = !packet.isAssumeCommand() && packet.getCommand().startsWith("/");
if (!isCommand) {
// We can't deal with anything else.
return false;
if (isCommand) {
return this.handleCommandTabComplete(packet);
} else {
return this.handleRegularTabComplete(packet);
}
}
@Override
public boolean handle(PluginMessage packet) {
VelocityServerConnection serverConn = player.getConnectedServer();
MinecraftConnection backendConn = serverConn != null ? serverConn.getConnection() : null;
if (serverConn != null && backendConn != null) {
if (backendConn.getState() != StateRegistry.PLAY) {
logger.warn("A plugin message was received while the backend server was not "
+ "ready. Channel: {}. Packet discarded.", packet.getChannel());
} else if (PluginMessageUtil.isRegister(packet)) {
player.getKnownChannels().addAll(PluginMessageUtil.getChannels(packet));
backendConn.write(packet.retain());
} else if (PluginMessageUtil.isUnregister(packet)) {
player.getKnownChannels().removeAll(PluginMessageUtil.getChannels(packet));
backendConn.write(packet.retain());
} else if (PluginMessageUtil.isMcBrand(packet)) {
backendConn.write(PluginMessageUtil.rewriteMinecraftBrand(packet, server.getVersion()));
} else {
if (serverConn.getPhase() == BackendConnectionPhases.IN_TRANSITION) {
// We must bypass the currently-connected server when forwarding Forge packets.
VelocityServerConnection inFlight = player.getConnectionInFlight();
if (inFlight != null) {
player.getPhase().handle(player, packet, inFlight);
}
return true;
}
if (!player.getPhase().handle(player, packet, serverConn)) {
if (!player.getPhase().consideredComplete() || !serverConn.getPhase()
.consideredComplete()) {
// The client is trying to send messages too early. This is primarily caused by mods,
// but further aggravated by Velocity. To work around these issues, we will queue any
// non-FML handshake messages to be sent once the FML handshake has completed or the
// JoinGame packet has been received by the proxy, whichever comes first.
//
// We also need to make sure to retain these packets so they can be flushed
// appropriately.
loginPluginMessages.add(packet.retain());
} else {
ChannelIdentifier id = server.getChannelRegistrar().getFromId(packet.getChannel());
if (id == null) {
backendConn.write(packet.retain());
} else {
byte[] copy = ByteBufUtil.getBytes(packet.content());
PluginMessageEvent event = new PluginMessageEvent(player, serverConn, id,
ByteBufUtil.getBytes(packet.content()));
server.getEventManager().fire(event).thenAcceptAsync(pme -> {
PluginMessage message = new PluginMessage(packet.getChannel(),
Unpooled.wrappedBuffer(copy));
backendConn.write(message);
}, backendConn.eventLoop());
}
}
}
}
}
return true;
}
@Override
public boolean handle(ResourcePackResponse packet) {
server.getEventManager().fireAndForget(new PlayerResourcePackStatusEvent(player,
packet.getStatus()));
return false;
}
@Override
public void handleGeneric(MinecraftPacket packet) {
VelocityServerConnection serverConnection = player.getConnectedServer();
if (serverConnection == null) {
// No server connection yet, probably transitioning.
return;
}
MinecraftConnection smc = serverConnection.getConnection();
if (smc != null && serverConnection.getPhase().consideredComplete()) {
if (packet instanceof PluginMessage) {
((PluginMessage) packet).retain();
}
smc.write(packet);
}
}
@Override
public void handleUnknown(ByteBuf buf) {
VelocityServerConnection serverConnection = player.getConnectedServer();
if (serverConnection == null) {
// No server connection yet, probably transitioning.
return;
}
MinecraftConnection smc = serverConnection.getConnection();
if (smc != null && serverConnection.getPhase().consideredComplete()) {
smc.write(buf.retain());
}
}
@Override
public void disconnected() {
player.teardown();
}
@Override
public void exception(Throwable throwable) {
player.disconnect(TextComponent.of("Your connection has encountered an error. Try again later.",
TextColor.RED));
}
@Override
public void writabilityChanged() {
VelocityServerConnection serverConn = player.getConnectedServer();
if (serverConn != null) {
boolean writable = player.getConnection().getChannel().isWritable();
MinecraftConnection smc = serverConn.getConnection();
if (smc != null) {
smc.setAutoReading(writable);
}
}
}
/**
* Handles the {@code JoinGame} packet. This function is responsible for handling the client-side
* switching servers in Velocity.
* @param joinGame the join game packet
* @param destination the new server we are connecting to
*/
public void handleBackendJoinGame(JoinGame joinGame, VelocityServerConnection destination) {
final MinecraftConnection serverMc = destination.ensureConnected();
if (!spawned) {
// Nothing special to do with regards to spawning the player
spawned = true;
player.getConnection().delayedWrite(joinGame);
// Required for Legacy Forge
player.getPhase().onFirstJoin(player);
} else {
// Clear tab list to avoid duplicate entries
player.getTabList().clearAll();
// In order to handle switching to another server, you will need to send three packets:
//
// - The join game packet from the backend server
// - A respawn packet with a different dimension
// - Another respawn with the correct dimension
//
// The two respawns with different dimensions are required, otherwise the client gets
// confused.
//
// Most notably, by having the client accept the join game packet, we can work around the need
// to perform entity ID rewrites, eliminating potential issues from rewriting packets and
// improving compatibility with mods.
player.getConnection().delayedWrite(joinGame);
int tempDim = joinGame.getDimension() == 0 ? -1 : 0;
player.getConnection().delayedWrite(
new Respawn(tempDim, joinGame.getDifficulty(), joinGame.getGamemode(),
joinGame.getLevelType()));
player.getConnection().delayedWrite(
new Respawn(joinGame.getDimension(), joinGame.getDifficulty(), joinGame.getGamemode(),
joinGame.getLevelType()));
}
// Remove previous boss bars. These don't get cleared when sending JoinGame, thus the need to
// track them.
for (UUID serverBossBar : serverBossBars) {
BossBar deletePacket = new BossBar();
deletePacket.setUuid(serverBossBar);
deletePacket.setAction(BossBar.REMOVE);
player.getConnection().delayedWrite(deletePacket);
}
serverBossBars.clear();
// Tell the server about this client's plugin message channels.
ProtocolVersion serverVersion = serverMc.getProtocolVersion();
if (!player.getKnownChannels().isEmpty()) {
serverMc.delayedWrite(constructChannelsPacket(serverVersion, player.getKnownChannels()));
}
// If we had plugin messages queued during login/FML handshake, send them now.
PluginMessage pm;
while ((pm = loginPluginMessages.poll()) != null) {
serverMc.delayedWrite(pm);
}
// Clear any title from the previous server.
player.getConnection()
.delayedWrite(TitlePacket.resetForProtocolVersion(player.getProtocolVersion()));
// Flush everything
player.getConnection().flush();
serverMc.flush();
destination.completeJoin();
}
public List<UUID> getServerBossBars() {
return serverBossBars;
}
private boolean handleCommandTabComplete(TabCompleteRequest packet) {
// In 1.13+, we need to do additional work for the richer suggestions available.
String command = packet.getCommand().substring(1);
int spacePos = command.indexOf(' ');
if (spacePos == -1) {
return false;
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
// tab list completion support for command names. In 1.13, Brigadier handles everything for
// us.
legacyCommandTabComplete = packet;
// additional tab completion support.
outstandingTabComplete = packet;
}
return false;
}
@ -199,221 +409,67 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
resp.setLength(length);
resp.getOffers().addAll(offers);
player.getMinecraftConnection().write(resp);
player.getConnection().write(resp);
return true;
}
@Override
public boolean handle(PluginMessage packet) {
VelocityServerConnection serverConn = player.getConnectedServer();
MinecraftConnection backendConn = serverConn != null ? serverConn.getConnection() : null;
if (serverConn != null && backendConn != null) {
if (backendConn.getState() != StateRegistry.PLAY) {
logger.warn("A plugin message was received while the backend server was not "
+ "ready. Channel: {}. Packet discarded.", packet.getChannel());
} else if (PluginMessageUtil.isRegister(packet)) {
player.getKnownChannels().addAll(PluginMessageUtil.getChannels(packet));
backendConn.write(packet);
} else if (PluginMessageUtil.isUnregister(packet)) {
player.getKnownChannels().removeAll(PluginMessageUtil.getChannels(packet));
backendConn.write(packet);
} else if (PluginMessageUtil.isMcBrand(packet)) {
backendConn.write(PluginMessageUtil.rewriteMinecraftBrand(packet, server.getVersion()));
} else {
if (serverConn.getPhase() == BackendConnectionPhases.IN_TRANSITION) {
// We must bypass the currently-connected server when forwarding Forge packets.
VelocityServerConnection inFlight = player.getConnectionInFlight();
if (inFlight != null) {
player.getPhase().handle(player, packet, inFlight);
}
return true;
}
if (!player.getPhase().handle(player, packet, serverConn)) {
if (!player.getPhase().consideredComplete() || !serverConn.getPhase()
.consideredComplete()) {
// The client is trying to send messages too early. This is primarily caused by mods,
// but further aggravated by Velocity. To work around these issues, we will queue any
// non-FML handshake messages to be sent once the FML handshake has completed or the
// JoinGame packet has been received by the proxy, whichever comes first.
loginPluginMessages.add(packet);
} else {
ChannelIdentifier id = server.getChannelRegistrar().getFromId(packet.getChannel());
if (id == null) {
backendConn.write(packet);
} else {
PluginMessageEvent event = new PluginMessageEvent(player, serverConn, id,
packet.getData());
server.getEventManager().fire(event).thenAcceptAsync(pme -> backendConn.write(packet),
backendConn.eventLoop());
}
}
}
}
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 true;
}
@Override
public boolean handle(ResourcePackResponse packet) {
server.getEventManager().fireAndForget(new PlayerResourcePackStatusEvent(player,
packet.getStatus()));
return false;
}
@Override
public void handleGeneric(MinecraftPacket packet) {
VelocityServerConnection serverConnection = player.getConnectedServer();
if (serverConnection == null) {
// No server connection yet, probably transitioning.
return;
}
MinecraftConnection smc = serverConnection.getConnection();
if (smc != null && serverConnection.getPhase().consideredComplete()) {
smc.write(packet);
}
}
@Override
public void handleUnknown(ByteBuf buf) {
VelocityServerConnection serverConnection = player.getConnectedServer();
if (serverConnection == null) {
// No server connection yet, probably transitioning.
return;
}
MinecraftConnection smc = serverConnection.getConnection();
if (smc != null && serverConnection.getPhase().consideredComplete()) {
smc.write(buf.retain());
}
}
@Override
public void disconnected() {
player.teardown();
}
@Override
public void exception(Throwable throwable) {
player.disconnect(TextComponent.of("Your connection has encountered an error. Try again later.",
TextColor.RED));
}
@Override
public void writabilityChanged() {
VelocityServerConnection serverConn = player.getConnectedServer();
if (serverConn != null) {
boolean writable = player.getMinecraftConnection().getChannel().isWritable();
MinecraftConnection smc = serverConn.getConnection();
if (smc != null) {
smc.setAutoReading(writable);
}
}
}
/**
* Handles the {@code JoinGame} packet. This function is responsible for handling the client-side
* switching servers in Velocity.
* @param joinGame the join game packet
* @param destination the new server we are connecting to
*/
public void handleBackendJoinGame(JoinGame joinGame, VelocityServerConnection destination) {
final MinecraftConnection serverMc = destination.ensureConnected();
if (!spawned) {
// Nothing special to do with regards to spawning the player
spawned = true;
player.getMinecraftConnection().delayedWrite(joinGame);
// Required for Legacy Forge
player.getPhase().onFirstJoin(player);
} else {
// Clear tab list to avoid duplicate entries
player.getTabList().clearAll();
// In order to handle switching to another server, you will need to send three packets:
//
// - The join game packet from the backend server
// - A respawn packet with a different dimension
// - Another respawn with the correct dimension
//
// The two respawns with different dimensions are required, otherwise the client gets
// confused.
//
// Most notably, by having the client accept the join game packet, we can work around the need
// to perform entity ID rewrites, eliminating potential issues from rewriting packets and
// improving compatibility with mods.
player.getMinecraftConnection().delayedWrite(joinGame);
int tempDim = joinGame.getDimension() == 0 ? -1 : 0;
player.getMinecraftConnection().delayedWrite(
new Respawn(tempDim, joinGame.getDifficulty(), joinGame.getGamemode(),
joinGame.getLevelType()));
player.getMinecraftConnection().delayedWrite(
new Respawn(joinGame.getDimension(), joinGame.getDifficulty(), joinGame.getGamemode(),
joinGame.getLevelType()));
}
// Remove previous boss bars. These don't get cleared when sending JoinGame, thus the need to
// track them.
for (UUID serverBossBar : serverBossBars) {
BossBar deletePacket = new BossBar();
deletePacket.setUuid(serverBossBar);
deletePacket.setAction(BossBar.REMOVE);
player.getMinecraftConnection().delayedWrite(deletePacket);
}
serverBossBars.clear();
// Tell the server about this client's plugin message channels.
ProtocolVersion serverVersion = serverMc.getProtocolVersion();
if (!player.getKnownChannels().isEmpty()) {
serverMc.delayedWrite(constructChannelsPacket(serverVersion, player.getKnownChannels()));
}
// If we had plugin messages queued during login/FML handshake, send them now.
PluginMessage pm;
while ((pm = loginPluginMessages.poll()) != null) {
serverMc.delayedWrite(pm);
}
// Clear any title from the previous server.
player.getMinecraftConnection()
.delayedWrite(TitlePacket.resetForProtocolVersion(player.getProtocolVersion()));
// Flush everything
player.getMinecraftConnection().flush();
serverMc.flush();
destination.completeJoin();
}
public List<UUID> getServerBossBars() {
return serverBossBars;
}
/**
* Handles additional tab complete for 1.12 and lower clients.
* Handles additional tab complete.
*
* @param response the tab complete response from the backend
*/
public void handleTabCompleteResponse(TabCompleteResponse response) {
if (legacyCommandTabComplete != null) {
String command = legacyCommandTabComplete.getCommand().substring(1);
try {
List<String> offers = server.getCommandManager().offerSuggestions(player, command);
for (String offer : offers) {
response.getOffers().add(new Offer(offer, null));
}
response.getOffers().sort(null);
} catch (Exception e) {
logger.error("Unable to provide tab list completions for {} for command '{}'",
player.getUsername(),
command, e);
if (outstandingTabComplete != null) {
if (outstandingTabComplete.isAssumeCommand()) {
return; // used for command blocks which can't run Velocity commands anyway
}
legacyCommandTabComplete = null;
if (outstandingTabComplete.getCommand().startsWith("/")) {
this.finishCommandTabComplete(outstandingTabComplete, response);
} else {
this.finishRegularTabComplete(outstandingTabComplete, response);
}
outstandingTabComplete = null;
}
}
player.getMinecraftConnection().write(response);
private void finishCommandTabComplete(TabCompleteRequest request, TabCompleteResponse response) {
String command = request.getCommand().substring(1);
try {
List<String> offers = server.getCommandManager().offerSuggestions(player, command);
for (String offer : offers) {
response.getOffers().add(new Offer(offer, null));
}
response.getOffers().sort(null);
player.getConnection().write(response);
} catch (Exception e) {
logger.error("Unable to provide tab list completions for {} for command '{}'",
player.getUsername(),
command, e);
}
}
private void finishRegularTabComplete(TabCompleteRequest request, TabCompleteResponse response) {
List<String> offers = new ArrayList<>();
for (Offer offer : response.getOffers()) {
offers.add(offer.getText());
}
server.getEventManager().fire(new TabCompleteEvent(player, request.getCommand(), offers))
.thenAcceptAsync(e -> {
response.getOffers().clear();
for (String s : e.getSuggestions()) {
response.getOffers().add(new Offer(s));
}
player.getConnection().write(response);
}, player.getConnection().eventLoop());
}
/**

Datei anzeigen

@ -49,6 +49,7 @@ import com.velocitypowered.proxy.tablist.VelocityTabList;
import com.velocitypowered.proxy.util.VelocityMessages;
import com.velocitypowered.proxy.util.collect.CappedSet;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import java.net.InetSocketAddress;
import java.util.Collection;
import java.util.Collections;
@ -82,7 +83,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
/**
* The actual Minecraft connection. This is actually a wrapper object around the Netty channel.
*/
private final MinecraftConnection minecraftConnection;
private final MinecraftConnection connection;
private final @Nullable InetSocketAddress virtualHost;
private GameProfile profile;
private PermissionFunction permissionFunction;
@ -96,18 +97,19 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
private final VelocityServer server;
private ClientConnectionPhase connectionPhase;
private final Collection<String> knownChannels;
private final CompletableFuture<Void> teardownFuture = new CompletableFuture<>();
private @MonotonicNonNull List<String> serversToTry = null;
ConnectedPlayer(VelocityServer server, GameProfile profile,
MinecraftConnection minecraftConnection, @Nullable InetSocketAddress virtualHost) {
MinecraftConnection connection, @Nullable InetSocketAddress virtualHost) {
this.server = server;
this.tabList = new VelocityTabList(minecraftConnection);
this.tabList = new VelocityTabList(connection);
this.profile = profile;
this.minecraftConnection = minecraftConnection;
this.connection = connection;
this.virtualHost = virtualHost;
this.permissionFunction = PermissionFunction.ALWAYS_UNDEFINED;
this.connectionPhase = minecraftConnection.getType().getInitialClientPhase();
this.connectionPhase = connection.getType().getInitialClientPhase();
this.knownChannels = CappedSet.create(MAX_PLUGIN_CHANNELS);
}
@ -131,8 +133,8 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
return profile;
}
public MinecraftConnection getMinecraftConnection() {
return minecraftConnection;
public MinecraftConnection getConnection() {
return connection;
}
@Override
@ -167,7 +169,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
@Override
public InetSocketAddress getRemoteAddress() {
return (InetSocketAddress) minecraftConnection.getRemoteAddress();
return (InetSocketAddress) connection.getRemoteAddress();
}
@Override
@ -181,12 +183,12 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
@Override
public boolean isActive() {
return minecraftConnection.getChannel().isActive();
return connection.getChannel().isActive();
}
@Override
public ProtocolVersion getProtocolVersion() {
return minecraftConnection.getProtocolVersion();
return connection.getProtocolVersion();
}
@Override
@ -202,7 +204,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
TitlePacket pkt = new TitlePacket();
pkt.setAction(TitlePacket.SET_ACTION_BAR);
pkt.setComponent(GsonComponentSerializer.INSTANCE.serialize(component));
minecraftConnection.write(pkt);
connection.write(pkt);
return;
} else {
// Due to issues with action bar packets, we'll need to convert the text message into a
@ -218,7 +220,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
Chat chat = new Chat();
chat.setType(pos);
chat.setMessage(json);
minecraftConnection.write(chat);
connection.write(chat);
}
@Override
@ -255,23 +257,23 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
public void disconnect(Component reason) {
logger.info("{} has disconnected: {}", this,
LegacyComponentSerializer.legacy().serialize(reason));
minecraftConnection.closeWith(Disconnect.create(reason));
connection.closeWith(Disconnect.create(reason));
}
@Override
public void sendTitle(Title title) {
Preconditions.checkNotNull(title, "title");
ProtocolVersion protocolVersion = minecraftConnection.getProtocolVersion();
ProtocolVersion protocolVersion = connection.getProtocolVersion();
if (title.equals(Titles.reset())) {
minecraftConnection.write(TitlePacket.resetForProtocolVersion(protocolVersion));
connection.write(TitlePacket.resetForProtocolVersion(protocolVersion));
} else if (title.equals(Titles.hide())) {
minecraftConnection.write(TitlePacket.hideForProtocolVersion(protocolVersion));
connection.write(TitlePacket.hideForProtocolVersion(protocolVersion));
} else if (title instanceof TextTitle) {
TextTitle tt = (TextTitle) title;
if (tt.isResetBeforeSend()) {
minecraftConnection.delayedWrite(TitlePacket.resetForProtocolVersion(protocolVersion));
connection.delayedWrite(TitlePacket.resetForProtocolVersion(protocolVersion));
}
Optional<Component> titleText = tt.getTitle();
@ -279,7 +281,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
TitlePacket titlePkt = new TitlePacket();
titlePkt.setAction(TitlePacket.SET_TITLE);
titlePkt.setComponent(GsonComponentSerializer.INSTANCE.serialize(titleText.get()));
minecraftConnection.delayedWrite(titlePkt);
connection.delayedWrite(titlePkt);
}
Optional<Component> subtitleText = tt.getSubtitle();
@ -287,7 +289,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
TitlePacket titlePkt = new TitlePacket();
titlePkt.setAction(TitlePacket.SET_SUBTITLE);
titlePkt.setComponent(GsonComponentSerializer.INSTANCE.serialize(subtitleText.get()));
minecraftConnection.delayedWrite(titlePkt);
connection.delayedWrite(titlePkt);
}
if (tt.areTimesSet()) {
@ -295,9 +297,9 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
timesPkt.setFadeIn(tt.getFadeIn());
timesPkt.setStay(tt.getStay());
timesPkt.setFadeOut(tt.getFadeOut());
minecraftConnection.delayedWrite(timesPkt);
connection.delayedWrite(timesPkt);
}
minecraftConnection.flush();
connection.flush();
} else {
throw new IllegalArgumentException("Unknown title class " + title.getClass().getName());
}
@ -404,18 +406,14 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
}
if (connectedServer == null) {
// The player isn't yet connected to a server. Note that we need to do this in a future run
// of the event loop due to an issue with the Netty kqueue transport.
minecraftConnection.eventLoop().execute(() -> {
Optional<RegisteredServer> nextServer = getNextServerToTry(rs);
if (nextServer.isPresent()) {
// There can't be any connection in flight now.
resetInFlightConnection();
createConnectionRequest(nextServer.get()).fireAndForget();
} else {
disconnect(friendlyReason);
}
});
Optional<RegisteredServer> nextServer = getNextServerToTry(rs);
if (nextServer.isPresent()) {
// There can't be any connection in flight now.
resetInFlightConnection();
createConnectionRequest(nextServer.get()).fireAndForget();
} else {
disconnect(friendlyReason);
}
} else {
boolean kickedFromCurrent = connectedServer.getServer().equals(rs);
ServerKickResult result;
@ -453,9 +451,9 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
if (newResult == null || !newResult) {
disconnect(friendlyReason);
} else {
sendMessage(VelocityMessages.MOVED_TO_NEW_SERVER);
sendMessage(VelocityMessages.MOVED_TO_NEW_SERVER.append(friendlyReason));
}
}, minecraftConnection.eventLoop());
}, connection.eventLoop());
} else if (event.getResult() instanceof Notify) {
Notify res = (Notify) event.getResult();
if (event.kickedDuringServerConnect()) {
@ -467,7 +465,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
// In case someone gets creative, assume we want to disconnect the player.
disconnect(friendlyReason);
}
}, minecraftConnection.eventLoop());
}, connection.eventLoop());
}
/**
@ -557,7 +555,12 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
connectedServer.disconnect();
}
server.unregisterConnection(this);
server.getEventManager().fireAndForget(new DisconnectEvent(this));
server.getEventManager().fire(new DisconnectEvent(this))
.thenRun(() -> this.teardownFuture.complete(null));
}
public CompletableFuture<Void> getTeardownFuture() {
return teardownFuture;
}
@Override
@ -574,10 +577,8 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
public boolean sendPluginMessage(ChannelIdentifier identifier, byte[] data) {
Preconditions.checkNotNull(identifier, "identifier");
Preconditions.checkNotNull(data, "data");
PluginMessage message = new PluginMessage();
message.setChannel(identifier.getId());
message.setData(data);
minecraftConnection.write(message);
PluginMessage message = new PluginMessage(identifier.getId(), Unpooled.wrappedBuffer(data));
connection.write(message);
return true;
}
@ -596,7 +597,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
ResourcePackRequest request = new ResourcePackRequest();
request.setUrl(url);
request.setHash("");
minecraftConnection.write(request);
connection.write(request);
}
@Override
@ -608,7 +609,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
ResourcePackRequest request = new ResourcePackRequest();
request.setUrl(url);
request.setHash(ByteBufUtil.hexDump(hash));
minecraftConnection.write(request);
connection.write(request);
}
/**
@ -617,10 +618,10 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
* ID last sent by the server.
*/
public void sendKeepAlive() {
if (minecraftConnection.getState() == StateRegistry.PLAY) {
if (connection.getState() == StateRegistry.PLAY) {
KeepAlive keepAlive = new KeepAlive();
keepAlive.setRandomId(ThreadLocalRandom.current().nextLong());
minecraftConnection.write(keepAlive);
connection.write(keepAlive);
}
}
@ -744,8 +745,10 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
if (status != null && !status.isSafe()) {
// If it's not safe to continue the connection we need to shut it down.
handleConnectionException(status.getAttemptedConnection(), throwable, true);
} else if ((status != null && !status.isSuccessful())) {
resetInFlightConnection();
}
})
}, connection.eventLoop())
.thenApply(x -> x);
}
@ -778,7 +781,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
// The only remaining value is successful (no need to do anything!)
break;
}
}, minecraftConnection.eventLoop())
}, connection.eventLoop())
.thenApply(Result::isSuccessful);
}

Datei anzeigen

@ -26,7 +26,7 @@ public class InitialConnectSessionHandler implements MinecraftSessionHandler {
} else if (PluginMessageUtil.isUnregister(packet)) {
player.getKnownChannels().removeAll(PluginMessageUtil.getChannels(packet));
}
serverConn.ensureConnected().write(packet);
serverConn.ensureConnected().write(packet.retain());
}
return true;
}

Datei anzeigen

@ -7,6 +7,8 @@ import static com.velocitypowered.proxy.connection.VelocityConstants.EMPTY_BYTE_
import static com.velocitypowered.proxy.connection.VelocityConstants.VELOCITY_IP_FORWARDING_CHANNEL;
import static com.velocitypowered.proxy.util.EncryptionUtils.decryptRsa;
import static com.velocitypowered.proxy.util.EncryptionUtils.generateServerId;
import static org.asynchttpclient.Dsl.asyncHttpClient;
import static org.asynchttpclient.Dsl.config;
import com.google.common.base.Preconditions;
import com.google.common.net.UrlEscapers;
@ -42,10 +44,14 @@ import java.security.KeyPair;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ThreadLocalRandom;
import net.kyori.text.Component;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.asynchttpclient.Dsl;
import org.asynchttpclient.ListenableFuture;
import org.asynchttpclient.Response;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public class LoginSessionHandler implements MinecraftSessionHandler {
@ -123,46 +129,50 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
String url = String.format(MOJANG_HASJOINED_URL,
urlFormParameterEscaper().escape(login.getUsername()), serverId,
urlFormParameterEscaper().escape(playerIp));
server.getHttpClient()
.get(new URL(url), mcConnection.eventLoop())
.thenAcceptAsync(profileResponse -> {
if (mcConnection.isClosed()) {
// The player disconnected after we authenticated them.
return;
}
// Go ahead and enable encryption. Once the client sends EncryptionResponse, encryption
// is enabled.
try {
mcConnection.enableEncryption(decryptedSharedSecret);
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
ListenableFuture<Response> hasJoinedResponse = server.getAsyncHttpClient().prepareGet(url)
.execute();
hasJoinedResponse.addListener(() -> {
if (mcConnection.isClosed()) {
// The player disconnected after we authenticated them.
return;
}
if (profileResponse.getCode() == 200) {
// All went well, initialize the session.
initializePlayer(GSON.fromJson(profileResponse.getBody(), GameProfile.class), true);
} else if (profileResponse.getCode() == 204) {
// Apparently an offline-mode user logged onto this online-mode proxy.
inbound.disconnect(VelocityMessages.ONLINE_MODE_ONLY);
} else {
// Something else went wrong
logger.error(
"Got an unexpected error code {} whilst contacting Mojang to log in {} ({})",
profileResponse.getCode(), login.getUsername(), playerIp);
mcConnection.close();
}
}, mcConnection.eventLoop())
.exceptionally(exception -> {
logger.error("Unable to enable encryption", exception);
// Go ahead and enable encryption. Once the client sends EncryptionResponse, encryption
// is enabled.
try {
mcConnection.enableEncryption(decryptedSharedSecret);
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
try {
Response profileResponse = hasJoinedResponse.get();
if (profileResponse.getStatusCode() == 200) {
// All went well, initialize the session.
initializePlayer(GSON.fromJson(profileResponse.getResponseBody(), GameProfile.class),
true);
} else if (profileResponse.getStatusCode() == 204) {
// Apparently an offline-mode user logged onto this online-mode proxy.
inbound.disconnect(VelocityMessages.ONLINE_MODE_ONLY);
} else {
// Something else went wrong
logger.error(
"Got an unexpected error code {} whilst contacting Mojang to log in {} ({})",
profileResponse.getStatusCode(), login.getUsername(), playerIp);
mcConnection.close();
return null;
});
}
} catch (ExecutionException e) {
logger.error("Unable to authenticate with Mojang", e);
mcConnection.close();
} catch (InterruptedException e) {
// not much we can do usefully
Thread.currentThread().interrupt();
}
}, mcConnection.eventLoop());
} catch (GeneralSecurityException e) {
logger.error("Unable to enable encryption", e);
mcConnection.close();
} catch (MalformedURLException e) {
throw new AssertionError(e);
}
return true;
}
@ -179,6 +189,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
// The player was disconnected
return;
}
PreLoginComponentResult result = event.getResult();
Optional<Component> disconnectReason = result.getReason();
if (disconnectReason.isPresent()) {
@ -277,7 +288,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
player.disconnect(VelocityMessages.ALREADY_CONNECTED);
return;
}
mcConnection.setSessionHandler(new InitialConnectSessionHandler(player));
server.getEventManager().fire(new PostLoginEvent(player))
.thenRun(() -> player.createConnectionRequest(toTry.get()).fireAndForget());

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

@ -135,7 +135,7 @@ public enum LegacyForgeHandshakeClientPhase implements ClientConnectionPhase {
COMPLETE(null) {
@Override
public void resetConnectionPhase(ConnectedPlayer player) {
player.getMinecraftConnection().write(LegacyForgeUtil.resetPacket());
player.getConnection().write(LegacyForgeUtil.resetPacket());
player.setPhase(LegacyForgeHandshakeClientPhase.NOT_STARTED);
}
@ -212,7 +212,7 @@ public enum LegacyForgeHandshakeClientPhase implements ClientConnectionPhase {
PluginMessage message,
MinecraftConnection backendConn) {
// Send the packet on to the server.
backendConn.write(message);
backendConn.write(message.retain());
// We handled the packet. No need to continue processing.
return true;

Datei anzeigen

@ -27,9 +27,8 @@ class LegacyForgeUtil {
*/
static byte getHandshakePacketDiscriminator(PluginMessage message) {
Preconditions.checkArgument(message.getChannel().equals(FORGE_LEGACY_HANDSHAKE_CHANNEL));
byte[] data = message.getData();
Preconditions.checkArgument(data.length >= 1);
return data[0];
Preconditions.checkArgument(message.content().isReadable());
return message.content().getByte(0);
}
/**
@ -44,7 +43,7 @@ class LegacyForgeUtil {
.checkArgument(message.getChannel().equals(FORGE_LEGACY_HANDSHAKE_CHANNEL),
"message is not a FML HS plugin message");
ByteBuf byteBuf = Unpooled.wrappedBuffer(message.getData());
ByteBuf byteBuf = message.content().retainedSlice();
try {
byte discriminator = byteBuf.readByte();
@ -75,7 +74,7 @@ class LegacyForgeUtil {
static PluginMessage resetPacket() {
PluginMessage msg = new PluginMessage();
msg.setChannel(FORGE_LEGACY_HANDSHAKE_CHANNEL);
msg.setData(FORGE_LEGACY_HANDSHAKE_RESET_DATA.clone());
msg.replace(Unpooled.wrappedBuffer(FORGE_LEGACY_HANDSHAKE_RESET_DATA.clone()));
return msg;
}
}

Datei anzeigen

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

Datei anzeigen

@ -1,17 +1,12 @@
package com.velocitypowered.proxy.network;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.velocitypowered.proxy.util.concurrent.VelocityNettyThreadFactory;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.epoll.Epoll;
import io.netty.channel.epoll.EpollDatagramChannel;
import io.netty.channel.epoll.EpollEventLoopGroup;
import io.netty.channel.epoll.EpollServerSocketChannel;
import io.netty.channel.epoll.EpollSocketChannel;
import io.netty.channel.kqueue.KQueue;
import io.netty.channel.kqueue.KQueueDatagramChannel;
import io.netty.channel.kqueue.KQueueEventLoopGroup;
import io.netty.channel.kqueue.KQueueServerSocketChannel;
import io.netty.channel.kqueue.KQueueSocketChannel;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.DatagramChannel;
import io.netty.channel.socket.ServerSocketChannel;
@ -27,10 +22,7 @@ enum TransportType {
(name, type) -> new NioEventLoopGroup(0, createThreadFactory(name, type))),
EPOLL("epoll", EpollServerSocketChannel.class, EpollSocketChannel.class,
EpollDatagramChannel.class,
(name, type) -> new EpollEventLoopGroup(0, createThreadFactory(name, type))),
KQUEUE("Kqueue", KQueueServerSocketChannel.class, KQueueSocketChannel.class,
KQueueDatagramChannel.class,
(name, type) -> new KQueueEventLoopGroup(0, createThreadFactory(name, type)));
(name, type) -> new EpollEventLoopGroup(0, createThreadFactory(name, type)));
final String name;
final Class<? extends ServerSocketChannel> serverSocketChannelClass;
@ -60,17 +52,16 @@ enum TransportType {
}
private static ThreadFactory createThreadFactory(final String name, final Type type) {
return new ThreadFactoryBuilder()
.setNameFormat("Netty " + name + ' ' + type.toString() + " #%d")
.setDaemon(true)
.build();
return new VelocityNettyThreadFactory("Netty " + name + ' ' + type.toString() + " #%d");
}
public static TransportType bestType() {
if (Boolean.getBoolean("velocity.disable-native-transport")) {
return NIO;
}
if (Epoll.isAvailable()) {
return EPOLL;
} else if (KQueue.isAvailable()) {
return KQUEUE;
} else {
return NIO;
}

Datei anzeigen

@ -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;
}
}

Datei anzeigen

@ -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 + '\''
+ '}';
}
}

Datei anzeigen

@ -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);
}
}

Datei anzeigen

@ -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();
}
}

Datei anzeigen

@ -2,6 +2,7 @@ package com.velocitypowered.proxy.plugin.loader.java;
import com.google.inject.Binder;
import com.google.inject.Module;
import com.google.inject.Scopes;
import com.velocitypowered.api.command.CommandManager;
import com.velocitypowered.api.event.EventManager;
import com.velocitypowered.api.plugin.PluginDescription;
@ -27,6 +28,8 @@ class VelocityPluginModule implements Module {
@Override
public void configure(Binder binder) {
binder.bind(description.getMainClass()).in(Scopes.SINGLETON);
binder.bind(Logger.class).toInstance(LoggerFactory.getLogger(description.getId()));
binder.bind(ProxyServer.class).toInstance(server);
binder.bind(Path.class).annotatedWith(DataDirectory.class)

Datei anzeigen

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

Datei anzeigen

@ -4,24 +4,24 @@ import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.util.DeferredByteBufHolder;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import org.checkerframework.checker.nullness.qual.Nullable;
public class LoginPluginMessage implements MinecraftPacket {
public class LoginPluginMessage extends DeferredByteBufHolder implements MinecraftPacket {
private int id;
private @Nullable String channel;
private ByteBuf data = Unpooled.EMPTY_BUFFER;
public LoginPluginMessage() {
super(null);
}
public LoginPluginMessage(int id, @Nullable String channel, ByteBuf data) {
super(data);
this.id = id;
this.channel = channel;
this.data = data;
}
public int getId() {
@ -35,16 +35,12 @@ public class LoginPluginMessage implements MinecraftPacket {
return channel;
}
public ByteBuf getData() {
return data;
}
@Override
public String toString() {
return "LoginPluginMessage{"
+ "id=" + id
+ ", channel='" + channel + '\''
+ ", data=" + data
+ ", data=" + super.toString()
+ '}';
}
@ -53,9 +49,9 @@ public class LoginPluginMessage implements MinecraftPacket {
this.id = ProtocolUtils.readVarInt(buf);
this.channel = ProtocolUtils.readString(buf);
if (buf.isReadable()) {
this.data = buf.readSlice(buf.readableBytes());
this.replace(buf.readSlice(buf.readableBytes()));
} else {
this.data = Unpooled.EMPTY_BUFFER;
this.replace(Unpooled.EMPTY_BUFFER);
}
}
@ -66,7 +62,7 @@ public class LoginPluginMessage implements MinecraftPacket {
throw new IllegalStateException("Channel is not specified!");
}
ProtocolUtils.writeString(buf, channel);
buf.writeBytes(data);
buf.writeBytes(content());
}
@Override

Datei anzeigen

@ -4,14 +4,25 @@ import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.util.DeferredByteBufHolder;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public class LoginPluginResponse implements MinecraftPacket {
public class LoginPluginResponse extends DeferredByteBufHolder implements MinecraftPacket {
private int id;
private boolean success;
private ByteBuf data = Unpooled.EMPTY_BUFFER;
public LoginPluginResponse() {
super(Unpooled.EMPTY_BUFFER);
}
public LoginPluginResponse(int id, boolean success, @MonotonicNonNull ByteBuf buf) {
super(buf);
this.id = id;
this.success = success;
}
public int getId() {
return id;
@ -29,20 +40,12 @@ public class LoginPluginResponse implements MinecraftPacket {
this.success = success;
}
public ByteBuf getData() {
return data;
}
public void setData(ByteBuf data) {
this.data = data;
}
@Override
public String toString() {
return "LoginPluginResponse{"
+ "id=" + id
+ ", success=" + success
+ ", data=" + data
+ ", data=" + super.toString()
+ '}';
}
@ -51,9 +54,9 @@ public class LoginPluginResponse implements MinecraftPacket {
this.id = ProtocolUtils.readVarInt(buf);
this.success = buf.readBoolean();
if (buf.isReadable()) {
this.data = buf.readSlice(buf.readableBytes());
this.replace(buf.readSlice(buf.readableBytes()));
} else {
this.data = Unpooled.EMPTY_BUFFER;
this.replace(Unpooled.EMPTY_BUFFER);
}
}
@ -61,7 +64,7 @@ public class LoginPluginResponse implements MinecraftPacket {
public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
ProtocolUtils.writeVarInt(buf, id);
buf.writeBoolean(success);
buf.writeBytes(data);
buf.writeBytes(content());
}
@Override

Datei anzeigen

@ -1,21 +1,29 @@
package com.velocitypowered.proxy.protocol.packet;
import static com.velocitypowered.proxy.connection.VelocityConstants.EMPTY_BYTE_ARRAY;
import static com.velocitypowered.proxy.protocol.util.PluginMessageUtil.transformLegacyToModernChannel;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
import com.velocitypowered.proxy.protocol.util.DeferredByteBufHolder;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
public class PluginMessage implements MinecraftPacket {
public class PluginMessage extends DeferredByteBufHolder implements MinecraftPacket {
private @Nullable String channel;
private byte[] data = EMPTY_BYTE_ARRAY;
public PluginMessage() {
super(null);
}
public PluginMessage(String channel,
@MonotonicNonNull ByteBuf backing) {
super(backing);
this.channel = channel;
}
public String getChannel() {
if (channel == null) {
@ -28,19 +36,11 @@ public class PluginMessage implements MinecraftPacket {
this.channel = channel;
}
public byte[] getData() {
return data;
}
public void setData(byte[] data) {
this.data = data;
}
@Override
public String toString() {
return "PluginMessage{"
+ "channel='" + channel + '\''
+ ", data=<removed>"
+ ", data=" + super.toString()
+ '}';
}
@ -50,8 +50,7 @@ public class PluginMessage implements MinecraftPacket {
if (version.compareTo(ProtocolVersion.MINECRAFT_1_13) >= 0) {
this.channel = transformLegacyToModernChannel(this.channel);
}
this.data = new byte[buf.readableBytes()];
buf.readBytes(data);
this.replace(buf.readRetainedSlice(buf.readableBytes()));
}
@Override
@ -64,11 +63,51 @@ public class PluginMessage implements MinecraftPacket {
} else {
ProtocolUtils.writeString(buf, this.channel);
}
buf.writeBytes(data);
buf.writeBytes(content());
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
@Override
public PluginMessage copy() {
return (PluginMessage) super.copy();
}
@Override
public PluginMessage duplicate() {
return (PluginMessage) super.duplicate();
}
@Override
public PluginMessage retainedDuplicate() {
return (PluginMessage) super.retainedDuplicate();
}
@Override
public PluginMessage replace(ByteBuf content) {
return (PluginMessage) super.replace(content);
}
@Override
public PluginMessage retain() {
return (PluginMessage) super.retain();
}
@Override
public PluginMessage retain(int increment) {
return (PluginMessage) super.retain(increment);
}
@Override
public PluginMessage touch() {
return (PluginMessage) super.touch();
}
@Override
public PluginMessage touch(Object hint) {
return (PluginMessage) super.touch(hint);
}
}

Datei anzeigen

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

Datei anzeigen

@ -0,0 +1,136 @@
package com.velocitypowered.proxy.protocol.util;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufHolder;
import io.netty.util.IllegalReferenceCountException;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* A special-purpose implementation of {@code ByteBufHolder} that can defer accepting its buffer.
* This is required because Velocity packets are, for better or worse, mutable.
*/
public class DeferredByteBufHolder implements ByteBufHolder {
@MonotonicNonNull
private ByteBuf backing;
public DeferredByteBufHolder(
@MonotonicNonNull ByteBuf backing) {
this.backing = backing;
}
@Override
public ByteBuf content() {
if (backing == null) {
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
}
if (backing.refCnt() <= 0) {
throw new IllegalReferenceCountException(backing.refCnt());
}
return backing;
}
@Override
public ByteBufHolder copy() {
if (backing == null) {
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
}
return new DeferredByteBufHolder(backing.copy());
}
@Override
public ByteBufHolder duplicate() {
if (backing == null) {
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
}
return new DeferredByteBufHolder(backing.duplicate());
}
@Override
public ByteBufHolder retainedDuplicate() {
if (backing == null) {
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
}
return new DeferredByteBufHolder(backing.retainedDuplicate());
}
@Override
public ByteBufHolder replace(ByteBuf content) {
if (content == null) {
throw new NullPointerException("content");
}
this.backing = content;
return this;
}
@Override
public int refCnt() {
if (backing == null) {
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
}
return backing.refCnt();
}
@Override
public ByteBufHolder retain() {
if (backing == null) {
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
}
backing.retain();
return this;
}
@Override
public ByteBufHolder retain(int increment) {
if (backing == null) {
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
}
backing.retain(increment);
return this;
}
@Override
public ByteBufHolder touch() {
if (backing == null) {
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
}
backing.touch();
return this;
}
@Override
public ByteBufHolder touch(Object hint) {
if (backing == null) {
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
}
backing.touch(hint);
return this;
}
@Override
public boolean release() {
if (backing == null) {
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
}
return backing.release();
}
@Override
public boolean release(int decrement) {
if (backing == null) {
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
}
return backing.release(decrement);
}
@Override
public String toString() {
String str = "DeferredByteBufHolder[";
if (backing == null) {
str += "null";
} else {
str += backing.toString();
}
return str + "]";
}
}

Datei anzeigen

@ -11,7 +11,9 @@ import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.packet.PluginMessage;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.util.ByteProcessor;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
@ -93,12 +95,12 @@ public class PluginMessageUtil {
checkNotNull(message, "message");
checkArgument(isRegister(message) || isUnregister(message), "Unknown channel type %s",
message.getChannel());
if (message.getData().length == 0) {
if (!message.content().isReadable()) {
// If we try to split this, we will get an one-element array with the empty string, which
// has caused issues with 1.13+ compatibility. Just return an empty list.
return ImmutableList.of();
}
String channels = new String(message.getData(), StandardCharsets.UTF_8);
String channels = message.content().toString(StandardCharsets.UTF_8);
return ImmutableList.copyOf(channels.split("\0"));
}
@ -114,10 +116,9 @@ public class PluginMessageUtil {
Preconditions.checkArgument(channels.size() > 0, "no channels specified");
String channelName = protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_13) >= 0
? REGISTER_CHANNEL : REGISTER_CHANNEL_LEGACY;
PluginMessage message = new PluginMessage();
message.setChannel(channelName);
message.setData(String.join("\0", channels).getBytes(StandardCharsets.UTF_8));
return message;
ByteBuf contents = Unpooled.buffer();
contents.writeCharSequence(String.join("\0", channels), StandardCharsets.UTF_8);
return new PluginMessage(channelName, contents);
}
/**
@ -133,21 +134,11 @@ public class PluginMessageUtil {
String toAppend = " (" + version.getName() + ")";
byte[] rewrittenData;
ByteBuf rewrittenBuf = Unpooled.buffer();
try {
String currentBrand = ProtocolUtils.readString(Unpooled.wrappedBuffer(message.getData()));
ProtocolUtils.writeString(rewrittenBuf, currentBrand + toAppend);
rewrittenData = new byte[rewrittenBuf.readableBytes()];
rewrittenBuf.readBytes(rewrittenData);
} finally {
rewrittenBuf.release();
}
String currentBrand = ProtocolUtils.readString(message.content().slice());
ProtocolUtils.writeString(rewrittenBuf, currentBrand + toAppend);
PluginMessage newMsg = new PluginMessage();
newMsg.setChannel(message.getChannel());
newMsg.setData(rewrittenData);
return newMsg;
return new PluginMessage(message.getChannel(), rewrittenBuf);
}
private static final Pattern INVALID_IDENTIFIER_REGEX = Pattern.compile("[^a-z0-9\\-_]*");
@ -185,5 +176,4 @@ public class PluginMessageUtil {
return "legacy:" + INVALID_IDENTIFIER_REGEX.matcher(lower).replaceAll("");
}
}
}

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

@ -21,7 +21,7 @@ import org.checkerframework.checker.nullness.qual.Nullable;
public class VelocityTabList implements TabList {
private final MinecraftConnection connection;
private final Map<UUID, TabListEntry> entries = new ConcurrentHashMap<>();
private final Map<UUID, VelocityTabListEntry> entries = new ConcurrentHashMap<>();
public VelocityTabList(MinecraftConnection connection) {
this.connection = connection;
@ -46,15 +46,19 @@ public class VelocityTabList implements TabList {
"The provided entry was not created by this tab list");
Preconditions.checkArgument(!entries.containsKey(entry.getProfile().getId()),
"this TabList already contains an entry with the same uuid");
Preconditions.checkArgument(entry instanceof VelocityTabListEntry,
"Not a Velocity tab list entry");
PlayerListItem.Item packetItem = PlayerListItem.Item.from(entry);
connection.write(
new PlayerListItem(PlayerListItem.ADD_PLAYER, Collections.singletonList(packetItem)));
entries.put(entry.getProfile().getId(), entry);
entries.put(entry.getProfile().getId(), (VelocityTabListEntry) entry);
}
@Override
public Optional<TabListEntry> removeEntry(UUID uuid) {
Preconditions.checkNotNull(uuid, "uuid");
TabListEntry entry = entries.remove(uuid);
if (entry != null) {
PlayerListItem.Item packetItem = PlayerListItem.Item.from(entry);
@ -65,6 +69,12 @@ public class VelocityTabList implements TabList {
return Optional.ofNullable(entry);
}
@Override
public boolean containsEntry(UUID uuid) {
Preconditions.checkNotNull(uuid, "uuid");
return entries.containsKey(uuid);
}
/**
* Clears all entries from the tab list. Note that the entries are written with
* {@link MinecraftConnection#delayedWrite(Object)}, so make sure to do an explicit
@ -111,7 +121,7 @@ public class VelocityTabList implements TabList {
if (name == null || properties == null) {
throw new IllegalStateException("Got null game profile for ADD_PLAYER");
}
entries.put(item.getUuid(), TabListEntry.builder()
entries.put(item.getUuid(), (VelocityTabListEntry) TabListEntry.builder()
.tabList(this)
.profile(new GameProfile(uuid, name, properties))
.displayName(item.getDisplayName())
@ -124,23 +134,23 @@ public class VelocityTabList implements TabList {
entries.remove(uuid);
break;
case PlayerListItem.UPDATE_DISPLAY_NAME: {
TabListEntry entry = entries.get(uuid);
VelocityTabListEntry entry = entries.get(uuid);
if (entry != null) {
entry.setDisplayName(item.getDisplayName());
entry.setDisplayNameInternal(item.getDisplayName());
}
break;
}
case PlayerListItem.UPDATE_LATENCY: {
TabListEntry entry = entries.get(uuid);
VelocityTabListEntry entry = entries.get(uuid);
if (entry != null) {
entry.setLatency(item.getLatency());
entry.setLatencyInternal(item.getLatency());
}
break;
}
case PlayerListItem.UPDATE_GAMEMODE: {
TabListEntry entry = entries.get(uuid);
VelocityTabListEntry entry = entries.get(uuid);
if (entry != null) {
entry.setLatency(item.getGameMode());
entry.setGameModeInternal(item.getGameMode());
}
break;
}

Datei anzeigen

@ -47,6 +47,10 @@ public class VelocityTabListEntry implements TabListEntry {
return this;
}
void setDisplayNameInternal(@Nullable Component displayName) {
this.displayName = displayName;
}
@Override
public int getLatency() {
return latency;
@ -59,6 +63,10 @@ public class VelocityTabListEntry implements TabListEntry {
return this;
}
void setLatencyInternal(int latency) {
this.latency = latency;
}
@Override
public int getGameMode() {
return gameMode;
@ -70,4 +78,8 @@ public class VelocityTabListEntry implements TabListEntry {
tabList.updateEntry(PlayerListItem.UPDATE_GAMEMODE, this);
return this;
}
void setGameModeInternal(int gameMode) {
this.gameMode = gameMode;
}
}

Datei anzeigen

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

Datei anzeigen

@ -7,7 +7,13 @@ import net.kyori.text.format.TextColor;
public class VelocityMessages {
public static final Component ONLINE_MODE_ONLY = TextComponent
.of("This server only accepts connections from online-mode clients.", TextColor.RED);
.builder("This server only accepts connections from online-mode clients.")
.color(TextColor.RED)
.append(
TextComponent.of("\n\nDid you change your username? Sign out of Minecraft, sign back in, "
+ "and try again.", TextColor.GRAY)
)
.build();
public static final Component NO_PROXY_BEHIND_PROXY = TextComponent
.of("Running Velocity behind Velocity isn't supported.", TextColor.RED);
public static final Component NO_AVAILABLE_SERVERS = TextComponent
@ -15,7 +21,7 @@ public class VelocityMessages {
public static final Component ALREADY_CONNECTED = TextComponent
.of("You are already connected to this proxy!", TextColor.RED);
public static final Component MOVED_TO_NEW_SERVER = TextComponent
.of("You were moved from the server you were on because you were kicked", TextColor.RED);
.of("The server you were on kicked you: ", TextColor.RED);
private VelocityMessages() {
throw new AssertionError();

Datei anzeigen

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

Datei anzeigen

@ -0,0 +1,23 @@
package com.velocitypowered.proxy.util.concurrent;
import static com.google.common.base.Preconditions.checkNotNull;
import io.netty.util.concurrent.FastThreadLocalThread;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
public class VelocityNettyThreadFactory implements ThreadFactory {
private final AtomicInteger threadNumber = new AtomicInteger();
private final String nameFormat;
public VelocityNettyThreadFactory(String nameFormat) {
this.nameFormat = checkNotNull(nameFormat, "nameFormat");
}
@Override
public Thread newThread(Runnable r) {
String name = String.format(nameFormat, threadNumber.incrementAndGet());
return new FastThreadLocalThread(r, name);
}
}

Datei anzeigen

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