Mirror von
https://github.com/PaperMC/Velocity.git
synchronisiert 2024-11-16 21:10:30 +01:00
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:
Commit
35856fec04
@ -7,4 +7,4 @@ cache:
|
|||||||
- $HOME/.gradle/caches/
|
- $HOME/.gradle/caches/
|
||||||
- $HOME/.gradle/wrapper/
|
- $HOME/.gradle/wrapper/
|
||||||
jdk:
|
jdk:
|
||||||
- oraclejdk8
|
- openjdk8
|
13
README.md
13
README.md
@ -34,15 +34,4 @@ Once you've built Velocity, you can copy and run the `-all` JAR from
|
|||||||
and you can configure it from there.
|
and you can configure it from there.
|
||||||
|
|
||||||
Alternatively, you can get the proxy JAR from the [downloads](https://www.velocitypowered.com/downloads)
|
Alternatively, you can get the proxy JAR from the [downloads](https://www.velocitypowered.com/downloads)
|
||||||
page.
|
page.
|
||||||
|
|
||||||
## Status
|
|
||||||
|
|
||||||
Velocity is currently in beta. Production networks are successfully running
|
|
||||||
Velocity with many hundreds of concurrent players online, but your mileage
|
|
||||||
may vary.
|
|
||||||
|
|
||||||
Velocity supports Minecraft 1.8-1.14.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.
|
|
@ -24,9 +24,13 @@ dependencies {
|
|||||||
compile "net.kyori:text-serializer-plain:${textVersion}"
|
compile "net.kyori:text-serializer-plain:${textVersion}"
|
||||||
compile 'com.moandjiezana.toml:toml4j:0.7.2'
|
compile 'com.moandjiezana.toml:toml4j:0.7.2'
|
||||||
compile "org.slf4j:slf4j-api:${slf4jVersion}"
|
compile "org.slf4j:slf4j-api:${slf4jVersion}"
|
||||||
compile 'com.google.inject:guice:4.2.0'
|
compile 'com.google.inject:guice:4.2.2'
|
||||||
compile "org.checkerframework:checker-qual:${checkerFrameworkVersion}"
|
compile "org.checkerframework:checker-qual:${checkerFrameworkVersion}"
|
||||||
|
|
||||||
|
compile "org.spongepowered:configurate-hocon:${configurateVersion}"
|
||||||
|
compile "org.spongepowered:configurate-yaml:${configurateVersion}"
|
||||||
|
compile "org.spongepowered:configurate-gson:${configurateVersion}"
|
||||||
|
|
||||||
testCompile "org.junit.jupiter:junit-jupiter-api:${junitVersion}"
|
testCompile "org.junit.jupiter:junit-jupiter-api:${junitVersion}"
|
||||||
testCompile "org.junit.jupiter:junit-jupiter-engine:${junitVersion}"
|
testCompile "org.junit.jupiter:junit-jupiter-engine:${junitVersion}"
|
||||||
}
|
}
|
||||||
|
@ -10,9 +10,22 @@ public interface CommandManager {
|
|||||||
*
|
*
|
||||||
* @param command the command to register
|
* @param command the command to register
|
||||||
* @param aliases the alias to use
|
* @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);
|
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.
|
* Unregisters a command.
|
||||||
*
|
*
|
||||||
|
60
api/src/main/java/com/velocitypowered/api/command/RawCommand.java
Normale Datei
60
api/src/main/java/com/velocitypowered/api/command/RawCommand.java
Normale Datei
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
+ '}';
|
||||||
|
}
|
||||||
|
}
|
@ -29,7 +29,9 @@ public enum ProtocolVersion {
|
|||||||
MINECRAFT_1_13_2(404, "1.13.2"),
|
MINECRAFT_1_13_2(404, "1.13.2"),
|
||||||
MINECRAFT_1_14(477, "1.14"),
|
MINECRAFT_1_14(477, "1.14"),
|
||||||
MINECRAFT_1_14_1(480, "1.14.1"),
|
MINECRAFT_1_14_1(480, "1.14.1"),
|
||||||
MINECRAFT_1_14_2(485, "1.14.2");
|
MINECRAFT_1_14_2(485, "1.14.2"),
|
||||||
|
MINECRAFT_1_14_3(490, "1.14.3"),
|
||||||
|
MINECRAFT_1_14_4(498, "1.14.4");
|
||||||
|
|
||||||
private final int protocol;
|
private final int protocol;
|
||||||
private final String name;
|
private final String name;
|
||||||
|
@ -37,12 +37,20 @@ public interface TabList {
|
|||||||
* Removes the {@link TabListEntry} from the tab list with the {@link GameProfile} identified with
|
* Removes the {@link TabListEntry} from the tab list with the {@link GameProfile} identified with
|
||||||
* the specified {@link UUID}.
|
* the specified {@link UUID}.
|
||||||
*
|
*
|
||||||
* @param uuid of the
|
* @param uuid of the entry
|
||||||
* @return {@link Optional} containing the removed {@link TabListEntry} if present, otherwise
|
* @return {@link Optional} containing the removed {@link TabListEntry} if present, otherwise
|
||||||
* {@link Optional#empty()}
|
* {@link Optional#empty()}
|
||||||
*/
|
*/
|
||||||
Optional<TabListEntry> removeEntry(UUID uuid);
|
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.
|
* Returns an immutable {@link Collection} of the {@link TabListEntry}s in the tab list.
|
||||||
*
|
*
|
||||||
|
@ -182,6 +182,19 @@ public final class ServerPing {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uses the modified {@code mods} list in the response.
|
||||||
|
* @param mods the mods list to use
|
||||||
|
* @return this build, for chaining
|
||||||
|
*/
|
||||||
|
public Builder mods(ModInfo mods) {
|
||||||
|
Preconditions.checkNotNull(mods, "mods");
|
||||||
|
this.modType = mods.getType();
|
||||||
|
this.mods.clear();
|
||||||
|
this.mods.addAll(mods.getMods());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public Builder clearMods() {
|
public Builder clearMods() {
|
||||||
this.mods.clear();
|
this.mods.clear();
|
||||||
return this;
|
return this;
|
||||||
|
13
build.gradle
13
build.gradle
@ -13,20 +13,21 @@ plugins {
|
|||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
group 'com.velocitypowered'
|
group 'com.velocitypowered'
|
||||||
version '1.0.0-SNAPSHOT'
|
version '1.1.0-SNAPSHOT'
|
||||||
|
|
||||||
sourceCompatibility = 1.8
|
sourceCompatibility = 1.8
|
||||||
targetCompatibility = 1.8
|
targetCompatibility = 1.8
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
// dependency versions
|
// dependency versions
|
||||||
textVersion = '3.0.1'
|
textVersion = '3.0.2'
|
||||||
junitVersion = '5.3.0-M1'
|
junitVersion = '5.3.0-M1'
|
||||||
slf4jVersion = '1.7.25'
|
slf4jVersion = '1.7.25'
|
||||||
log4jVersion = '2.11.2'
|
log4jVersion = '2.11.2'
|
||||||
nettyVersion = '4.1.35.Final'
|
nettyVersion = '4.1.38.Final'
|
||||||
guavaVersion = '25.1-jre'
|
guavaVersion = '25.1-jre'
|
||||||
checkerFrameworkVersion = '2.7.0'
|
checkerFrameworkVersion = '2.7.0'
|
||||||
|
configurateVersion = '3.6'
|
||||||
|
|
||||||
getCurrentShortRevision = {
|
getCurrentShortRevision = {
|
||||||
new ByteArrayOutputStream().withStream { os ->
|
new ByteArrayOutputStream().withStream { os ->
|
||||||
@ -53,6 +54,12 @@ allprojects {
|
|||||||
maven {
|
maven {
|
||||||
url "https://libraries.minecraft.net"
|
url "https://libraries.minecraft.net"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Configurate
|
||||||
|
maven {
|
||||||
|
name = 'sponge'
|
||||||
|
url = 'https://repo.spongepowered.org/maven'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
|
@ -56,11 +56,16 @@ public class NativeVelocityCipher implements VelocityCipher {
|
|||||||
int len = source.readableBytes();
|
int len = source.readableBytes();
|
||||||
ByteBuf out = ctx.alloc().directBuffer(len);
|
ByteBuf out = ctx.alloc().directBuffer(len);
|
||||||
|
|
||||||
impl.process(this.ctx, source.memoryAddress() + source.readerIndex(), len,
|
try {
|
||||||
out.memoryAddress(), encrypt);
|
impl.process(this.ctx, source.memoryAddress() + source.readerIndex(), len,
|
||||||
source.skipBytes(len);
|
out.memoryAddress(), encrypt);
|
||||||
out.writerIndex(len);
|
source.skipBytes(len);
|
||||||
return out;
|
out.writerIndex(len);
|
||||||
|
return out;
|
||||||
|
} catch (Exception e) {
|
||||||
|
out.release();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -13,7 +13,12 @@ apply plugin: 'com.github.johnrengelman.shadow'
|
|||||||
jar {
|
jar {
|
||||||
manifest {
|
manifest {
|
||||||
def buildNumber = System.getenv("BUILD_NUMBER") ?: "unknown"
|
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 'Main-Class': 'com.velocitypowered.proxy.Velocity'
|
||||||
attributes 'Implementation-Title': "Velocity"
|
attributes 'Implementation-Title': "Velocity"
|
||||||
@ -43,7 +48,6 @@ dependencies {
|
|||||||
compile "io.netty:netty-handler:${nettyVersion}"
|
compile "io.netty:netty-handler:${nettyVersion}"
|
||||||
compile "io.netty:netty-transport-native-epoll:${nettyVersion}"
|
compile "io.netty:netty-transport-native-epoll:${nettyVersion}"
|
||||||
compile "io.netty:netty-transport-native-epoll:${nettyVersion}:linux-x86_64"
|
compile "io.netty:netty-transport-native-epoll:${nettyVersion}:linux-x86_64"
|
||||||
compile "io.netty:netty-transport-native-kqueue:${nettyVersion}:osx-x86_64"
|
|
||||||
compile "io.netty:netty-resolver-dns:${nettyVersion}"
|
compile "io.netty:netty-resolver-dns:${nettyVersion}"
|
||||||
|
|
||||||
compile "org.apache.logging.log4j:log4j-api:${log4jVersion}"
|
compile "org.apache.logging.log4j:log4j-api:${log4jVersion}"
|
||||||
@ -52,14 +56,18 @@ dependencies {
|
|||||||
compile "org.apache.logging.log4j:log4j-iostreams:${log4jVersion}"
|
compile "org.apache.logging.log4j:log4j-iostreams:${log4jVersion}"
|
||||||
|
|
||||||
compile 'net.sf.jopt-simple:jopt-simple:5.0.4' // command-line options
|
compile 'net.sf.jopt-simple:jopt-simple:5.0.4' // command-line options
|
||||||
compile 'net.minecrell:terminalconsoleappender:1.1.1'
|
compile 'net.minecrell:terminalconsoleappender:1.2.0'
|
||||||
runtime 'net.java.dev.jna:jna:4.5.2' // Needed for JLine
|
runtime 'org.jline:jline-terminal-jansi:3.12.1' // Needed for JLine
|
||||||
runtime 'com.lmax:disruptor:3.4.2' // Async loggers
|
runtime 'com.lmax:disruptor:3.4.2' // Async loggers
|
||||||
|
|
||||||
compile 'it.unimi.dsi:fastutil:8.2.2'
|
compile 'it.unimi.dsi:fastutil:8.2.3'
|
||||||
compile 'net.kyori:event-method-asm:3.0.0'
|
compile 'net.kyori:event-method-asm:3.0.0'
|
||||||
|
|
||||||
compile 'com.mojang:brigadier:1.0.15'
|
compile 'com.mojang:brigadier:1.0.15'
|
||||||
|
|
||||||
|
compile 'org.asynchttpclient:async-http-client:2.10.1'
|
||||||
|
|
||||||
|
compile 'com.spotify:completable-futures:0.3.2'
|
||||||
|
|
||||||
testCompile "org.junit.jupiter:junit-jupiter-api:${junitVersion}"
|
testCompile "org.junit.jupiter:junit-jupiter-api:${junitVersion}"
|
||||||
testCompile "org.junit.jupiter:junit-jupiter-engine:${junitVersion}"
|
testCompile "org.junit.jupiter:junit-jupiter-engine:${junitVersion}"
|
||||||
|
@ -3,12 +3,9 @@ package com.velocitypowered.proxy;
|
|||||||
import com.google.gson.JsonArray;
|
import com.google.gson.JsonArray;
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
import com.velocitypowered.proxy.config.VelocityConfiguration;
|
import com.velocitypowered.proxy.config.VelocityConfiguration;
|
||||||
import io.netty.buffer.ByteBuf;
|
|
||||||
import io.netty.buffer.ByteBufOutputStream;
|
|
||||||
import io.netty.buffer.ByteBufUtil;
|
|
||||||
import io.netty.buffer.Unpooled;
|
|
||||||
import io.netty.handler.codec.http.HttpHeaderNames;
|
import io.netty.handler.codec.http.HttpHeaderNames;
|
||||||
import java.io.BufferedWriter;
|
import java.io.BufferedWriter;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStreamWriter;
|
import java.io.OutputStreamWriter;
|
||||||
import java.io.Writer;
|
import java.io.Writer;
|
||||||
@ -21,11 +18,14 @@ import java.util.Map;
|
|||||||
import java.util.Timer;
|
import java.util.Timer;
|
||||||
import java.util.TimerTask;
|
import java.util.TimerTask;
|
||||||
import java.util.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.util.zip.GZIPOutputStream;
|
import java.util.zip.GZIPOutputStream;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.asynchttpclient.ListenableFuture;
|
||||||
|
import org.asynchttpclient.Response;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* bStats collects some data for plugin authors.
|
* bStats collects some data for plugin authors.
|
||||||
@ -185,40 +185,44 @@ public class Metrics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Compress the data to save bandwidth
|
// Compress the data to save bandwidth
|
||||||
ByteBuf reqBody = createResponseBody(data);
|
ListenableFuture<Response> future = server.getAsyncHttpClient()
|
||||||
|
.preparePost(URL)
|
||||||
server.getHttpClient().post(new URL(URL), reqBody, request -> {
|
.addHeader(HttpHeaderNames.CONTENT_ENCODING, "gzip")
|
||||||
request.headers().add(HttpHeaderNames.CONTENT_ENCODING, "gzip");
|
.addHeader(HttpHeaderNames.ACCEPT, "application/json")
|
||||||
request.headers().add(HttpHeaderNames.ACCEPT, "application/json");
|
.addHeader(HttpHeaderNames.CONTENT_TYPE, "application/json")
|
||||||
request.headers().add(HttpHeaderNames.CONTENT_TYPE, "application/json");
|
.setBody(createResponseBody(data))
|
||||||
})
|
.execute();
|
||||||
.whenCompleteAsync((resp, exc) -> {
|
future.addListener(() -> {
|
||||||
if (logFailedRequests) {
|
if (logFailedRequests) {
|
||||||
if (exc != null) {
|
try {
|
||||||
logger.error("Unable to send metrics to bStats", exc);
|
Response r = future.get();
|
||||||
} else if (resp.getCode() != 429) {
|
if (r.getStatusCode() != 429) {
|
||||||
logger.error("Got HTTP status code {} when sending metrics to bStats",
|
logger.error("Got HTTP status code {} when sending metrics to bStats",
|
||||||
resp.getCode());
|
r.getStatusCode());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
} catch (ExecutionException e) {
|
||||||
|
logger.error("Unable to send metrics to bStats", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ByteBuf createResponseBody(JsonObject object) throws IOException {
|
private static byte[] createResponseBody(JsonObject object) throws IOException {
|
||||||
ByteBuf buf = Unpooled.buffer();
|
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||||
try (Writer writer =
|
try (Writer writer =
|
||||||
new BufferedWriter(
|
new BufferedWriter(
|
||||||
new OutputStreamWriter(
|
new OutputStreamWriter(
|
||||||
new GZIPOutputStream(new ByteBufOutputStream(buf)), StandardCharsets.UTF_8
|
new GZIPOutputStream(os), StandardCharsets.UTF_8
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
VelocityServer.GSON.toJson(object, writer);
|
VelocityServer.GSON.toJson(object, writer);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
buf.release();
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
return buf;
|
return os.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -30,7 +30,6 @@ import com.velocitypowered.proxy.config.VelocityConfiguration;
|
|||||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||||
import com.velocitypowered.proxy.console.VelocityConsole;
|
import com.velocitypowered.proxy.console.VelocityConsole;
|
||||||
import com.velocitypowered.proxy.network.ConnectionManager;
|
import com.velocitypowered.proxy.network.ConnectionManager;
|
||||||
import com.velocitypowered.proxy.network.http.NettyHttpClient;
|
|
||||||
import com.velocitypowered.proxy.plugin.VelocityEventManager;
|
import com.velocitypowered.proxy.plugin.VelocityEventManager;
|
||||||
import com.velocitypowered.proxy.plugin.VelocityPluginManager;
|
import com.velocitypowered.proxy.plugin.VelocityPluginManager;
|
||||||
import com.velocitypowered.proxy.protocol.packet.Chat;
|
import com.velocitypowered.proxy.protocol.packet.Chat;
|
||||||
@ -59,18 +58,26 @@ import java.util.Map;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.CountDownLatch;
|
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.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.function.IntFunction;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import net.kyori.text.Component;
|
import net.kyori.text.Component;
|
||||||
import net.kyori.text.TextComponent;
|
import net.kyori.text.TextComponent;
|
||||||
|
import net.kyori.text.TranslatableComponent;
|
||||||
import net.kyori.text.serializer.gson.GsonComponentSerializer;
|
import net.kyori.text.serializer.gson.GsonComponentSerializer;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.asynchttpclient.AsyncHttpClient;
|
||||||
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
|
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
|
||||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||||
|
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||||
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||||
|
|
||||||
public class VelocityServer implements ProxyServer {
|
public class VelocityServer implements ProxyServer {
|
||||||
@ -84,7 +91,6 @@ public class VelocityServer implements ProxyServer {
|
|||||||
private final ConnectionManager cm;
|
private final ConnectionManager cm;
|
||||||
private final ProxyOptions options;
|
private final ProxyOptions options;
|
||||||
private @MonotonicNonNull VelocityConfiguration configuration;
|
private @MonotonicNonNull VelocityConfiguration configuration;
|
||||||
private @MonotonicNonNull NettyHttpClient httpClient;
|
|
||||||
private @MonotonicNonNull KeyPair serverKeyPair;
|
private @MonotonicNonNull KeyPair serverKeyPair;
|
||||||
private final ServerMap servers;
|
private final ServerMap servers;
|
||||||
private final VelocityCommandManager commandManager = new VelocityCommandManager();
|
private final VelocityCommandManager commandManager = new VelocityCommandManager();
|
||||||
@ -167,10 +173,10 @@ public class VelocityServer implements ProxyServer {
|
|||||||
cm.logChannelInformation();
|
cm.logChannelInformation();
|
||||||
|
|
||||||
// Initialize commands first
|
// Initialize commands first
|
||||||
commandManager.register(new VelocityCommand(this), "velocity");
|
commandManager.register("velocity", new VelocityCommand(this));
|
||||||
commandManager.register(new ServerCommand(this), "server");
|
commandManager.register("server", new ServerCommand(this));
|
||||||
commandManager.register(new ShutdownCommand(this), "shutdown", "end");
|
commandManager.register("shutdown", new ShutdownCommand(this),"end");
|
||||||
commandManager.register(new GlistCommand(this), "glist");
|
commandManager.register("glist", new GlistCommand(this));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Path configPath = Paths.get("velocity.toml");
|
Path configPath = Paths.get("velocity.toml");
|
||||||
@ -196,7 +202,6 @@ public class VelocityServer implements ProxyServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ipAttemptLimiter = Ratelimiters.createWithMilliseconds(configuration.getLoginRatelimit());
|
ipAttemptLimiter = Ratelimiters.createWithMilliseconds(configuration.getLoginRatelimit());
|
||||||
httpClient = new NettyHttpClient(this);
|
|
||||||
loadPlugins();
|
loadPlugins();
|
||||||
|
|
||||||
// Go ahead and fire the proxy initialization event. We block since plugins should have a chance
|
// Go ahead and fire the proxy initialization event. We block since plugins should have a chance
|
||||||
@ -259,15 +264,7 @@ public class VelocityServer implements ProxyServer {
|
|||||||
logger.info("Loaded {} plugins", pluginManager.getPlugins().size());
|
logger.info("Loaded {} plugins", pluginManager.getPlugins().size());
|
||||||
}
|
}
|
||||||
|
|
||||||
public EventLoopGroup getWorkerGroup() {
|
public Bootstrap createBootstrap(@Nullable EventLoopGroup group) {
|
||||||
return this.cm.getWorkerGroup();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Bootstrap initializeGenericBootstrap() {
|
|
||||||
return this.cm.createWorker();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Bootstrap initializeGenericBootstrap(EventLoopGroup group) {
|
|
||||||
return this.cm.createWorker(group);
|
return this.cm.createWorker(group);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -379,14 +376,35 @@ public class VelocityServer implements ProxyServer {
|
|||||||
Runnable shutdownProcess = () -> {
|
Runnable shutdownProcess = () -> {
|
||||||
logger.info("Shutting down the proxy...");
|
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."));
|
player.disconnect(TextComponent.of("Proxy shutting down."));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.cm.shutdown();
|
|
||||||
|
|
||||||
try {
|
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.");
|
logger.error("Your plugins took over 10 seconds to shut down.");
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
@ -407,8 +425,8 @@ public class VelocityServer implements ProxyServer {
|
|||||||
thread.start();
|
thread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
public NettyHttpClient getHttpClient() {
|
public AsyncHttpClient getAsyncHttpClient() {
|
||||||
return ensureInitialized(httpClient);
|
return ensureInitialized(cm).getHttpClient();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Ratelimiter getIpAttemptLimiter() {
|
public Ratelimiter getIpAttemptLimiter() {
|
||||||
@ -428,6 +446,9 @@ public class VelocityServer implements ProxyServer {
|
|||||||
* @return {@code true} if we can register the connection, {@code false} if not
|
* @return {@code true} if we can register the connection, {@code false} if not
|
||||||
*/
|
*/
|
||||||
public boolean canRegisterConnection(ConnectedPlayer connection) {
|
public boolean canRegisterConnection(ConnectedPlayer connection) {
|
||||||
|
if (configuration.isOnlineMode() && configuration.isOnlineModeKickExistingPlayers()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
String lowerName = connection.getUsername().toLowerCase(Locale.US);
|
String lowerName = connection.getUsername().toLowerCase(Locale.US);
|
||||||
return !(connectionsByName.containsKey(lowerName)
|
return !(connectionsByName.containsKey(lowerName)
|
||||||
|| connectionsByUuid.containsKey(connection.getUniqueId()));
|
|| connectionsByUuid.containsKey(connection.getUniqueId()));
|
||||||
@ -440,12 +461,24 @@ public class VelocityServer implements ProxyServer {
|
|||||||
*/
|
*/
|
||||||
public boolean registerConnection(ConnectedPlayer connection) {
|
public boolean registerConnection(ConnectedPlayer connection) {
|
||||||
String lowerName = connection.getUsername().toLowerCase(Locale.US);
|
String lowerName = connection.getUsername().toLowerCase(Locale.US);
|
||||||
if (connectionsByName.putIfAbsent(lowerName, connection) != null) {
|
|
||||||
return false;
|
if (!this.configuration.isOnlineModeKickExistingPlayers()) {
|
||||||
}
|
if (connectionsByName.putIfAbsent(lowerName, connection) != null) {
|
||||||
if (connectionsByUuid.putIfAbsent(connection.getUniqueId(), connection) != null) {
|
return false;
|
||||||
connectionsByName.remove(lowerName, connection);
|
}
|
||||||
return false;
|
if (connectionsByUuid.putIfAbsent(connection.getUniqueId(), connection) != null) {
|
||||||
|
connectionsByName.remove(lowerName, connection);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ConnectedPlayer existing = connectionsByUuid.get(connection.getUniqueId());
|
||||||
|
if (existing != null) {
|
||||||
|
existing.disconnect(TranslatableComponent.of("multiplayer.disconnect.duplicate_login"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can now replace the entries as needed.
|
||||||
|
connectionsByName.put(lowerName, connection);
|
||||||
|
connectionsByUuid.put(connection.getUniqueId(), connection);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -472,7 +505,7 @@ public class VelocityServer implements ProxyServer {
|
|||||||
Preconditions.checkNotNull(component, "component");
|
Preconditions.checkNotNull(component, "component");
|
||||||
Chat chat = Chat.createClientbound(component);
|
Chat chat = Chat.createClientbound(component);
|
||||||
for (ConnectedPlayer player : connectionsByUuid.values()) {
|
for (ConnectedPlayer player : connectionsByUuid.values()) {
|
||||||
player.getMinecraftConnection().write(chat);
|
player.getConnection().write(chat);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,27 +6,38 @@ import com.google.common.collect.ImmutableSet;
|
|||||||
import com.velocitypowered.api.command.Command;
|
import com.velocitypowered.api.command.Command;
|
||||||
import com.velocitypowered.api.command.CommandManager;
|
import com.velocitypowered.api.command.CommandManager;
|
||||||
import com.velocitypowered.api.command.CommandSource;
|
import com.velocitypowered.api.command.CommandSource;
|
||||||
import java.util.ArrayList;
|
import com.velocitypowered.api.command.RawCommand;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
|
||||||
|
|
||||||
public class VelocityCommandManager implements CommandManager {
|
public class VelocityCommandManager implements CommandManager {
|
||||||
|
|
||||||
private final Map<String, Command> commands = new HashMap<>();
|
private final Map<String, RawCommand> commands = new HashMap<>();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Deprecated
|
||||||
public void register(final Command command, final String... aliases) {
|
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");
|
Preconditions.checkNotNull(command, "executor");
|
||||||
for (int i = 0, length = aliases.length; i < length; i++) {
|
|
||||||
final String alias = aliases[i];
|
RawCommand rawCmd = RegularCommandWrapper.wrap(command);
|
||||||
Preconditions.checkNotNull(alias, "alias at index %s", i);
|
this.commands.put(alias.toLowerCase(Locale.ENGLISH), rawCmd);
|
||||||
this.commands.put(alias.toLowerCase(Locale.ENGLISH), command);
|
|
||||||
|
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(source, "invoker");
|
||||||
Preconditions.checkNotNull(cmdLine, "cmdLine");
|
Preconditions.checkNotNull(cmdLine, "cmdLine");
|
||||||
|
|
||||||
String[] split = cmdLine.split(" ", -1);
|
String alias = cmdLine;
|
||||||
if (split.length == 0) {
|
String args = "";
|
||||||
return false;
|
int firstSpace = cmdLine.indexOf(' ');
|
||||||
|
if (firstSpace != -1) {
|
||||||
|
alias = cmdLine.substring(0, firstSpace);
|
||||||
|
args = cmdLine.substring(firstSpace).trim();
|
||||||
}
|
}
|
||||||
|
RawCommand command = commands.get(alias.toLowerCase(Locale.ENGLISH));
|
||||||
String alias = split[0];
|
|
||||||
Command command = commands.get(alias.toLowerCase(Locale.ENGLISH));
|
|
||||||
if (command == null) {
|
if (command == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("nullness")
|
|
||||||
String[] actualArgs = Arrays.copyOfRange(split, 1, split.length);
|
|
||||||
try {
|
try {
|
||||||
if (!command.hasPermission(source, actualArgs)) {
|
if (!command.hasPermission(source, args)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
command.execute(source, args);
|
||||||
command.execute(source, actualArgs);
|
|
||||||
return true;
|
return true;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("Unable to invoke command " + cmdLine + " for " + source, 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(source, "source");
|
||||||
Preconditions.checkNotNull(cmdLine, "cmdLine");
|
Preconditions.checkNotNull(cmdLine, "cmdLine");
|
||||||
|
|
||||||
String[] split = cmdLine.split(" ", -1);
|
int firstSpace = cmdLine.indexOf(' ');
|
||||||
if (split.length == 0) {
|
if (firstSpace == -1) {
|
||||||
// No command available.
|
|
||||||
return ImmutableList.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
String alias = split[0];
|
|
||||||
if (split.length == 1) {
|
|
||||||
// Offer to fill in commands.
|
// Offer to fill in commands.
|
||||||
ImmutableList.Builder<String> availableCommands = ImmutableList.builder();
|
ImmutableList.Builder<String> availableCommands = ImmutableList.builder();
|
||||||
for (Map.Entry<String, Command> entry : commands.entrySet()) {
|
for (Map.Entry<String, RawCommand> entry : commands.entrySet()) {
|
||||||
if (entry.getKey().regionMatches(true, 0, alias, 0, alias.length())
|
if (entry.getKey().regionMatches(true, 0, cmdLine, 0, cmdLine.length())
|
||||||
&& entry.getValue().hasPermission(source, new String[0])) {
|
&& entry.getValue().hasPermission(source, new String[0])) {
|
||||||
availableCommands.add("/" + entry.getKey());
|
availableCommands.add("/" + entry.getKey());
|
||||||
}
|
}
|
||||||
@ -103,23 +106,22 @@ public class VelocityCommandManager implements CommandManager {
|
|||||||
return availableCommands.build();
|
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) {
|
if (command == null) {
|
||||||
// No such command, so we can't offer any tab complete suggestions.
|
// No such command, so we can't offer any tab complete suggestions.
|
||||||
return ImmutableList.of();
|
return ImmutableList.of();
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("nullness")
|
|
||||||
String[] actualArgs = Arrays.copyOfRange(split, 1, split.length);
|
|
||||||
try {
|
try {
|
||||||
if (!command.hasPermission(source, actualArgs)) {
|
if (!command.hasPermission(source, args)) {
|
||||||
return ImmutableList.of();
|
return ImmutableList.of();
|
||||||
}
|
}
|
||||||
|
return ImmutableList.copyOf(command.suggest(source, args));
|
||||||
return ImmutableList.copyOf(command.suggest(source, actualArgs));
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException(
|
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(source, "source");
|
||||||
Preconditions.checkNotNull(cmdLine, "cmdLine");
|
Preconditions.checkNotNull(cmdLine, "cmdLine");
|
||||||
|
|
||||||
String[] split = cmdLine.split(" ", -1);
|
String alias = cmdLine;
|
||||||
if (split.length == 0) {
|
String args = "";
|
||||||
// No command available.
|
int firstSpace = cmdLine.indexOf(' ');
|
||||||
return false;
|
if (firstSpace != -1) {
|
||||||
|
alias = cmdLine.substring(0, firstSpace);
|
||||||
|
args = cmdLine.substring(firstSpace).trim();
|
||||||
}
|
}
|
||||||
|
RawCommand command = commands.get(alias.toLowerCase(Locale.ENGLISH));
|
||||||
String alias = split[0];
|
|
||||||
Command command = commands.get(alias.toLowerCase(Locale.ENGLISH));
|
|
||||||
if (command == null) {
|
if (command == null) {
|
||||||
// No such command.
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("nullness")
|
|
||||||
String[] actualArgs = Arrays.copyOfRange(split, 1, split.length);
|
|
||||||
try {
|
try {
|
||||||
return command.hasPermission(source, actualArgs);
|
return command.hasPermission(source, args);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException(
|
throw new RuntimeException(
|
||||||
"Unable to invoke suggestions for command " + alias + " for " + source, e);
|
"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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
package com.velocitypowered.proxy.config;
|
||||||
|
|
||||||
|
public enum PingPassthroughMode {
|
||||||
|
DISABLED,
|
||||||
|
MODS,
|
||||||
|
ALL
|
||||||
|
}
|
@ -72,11 +72,36 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
|
|||||||
@ConfigKey("forwarding-secret")
|
@ConfigKey("forwarding-secret")
|
||||||
private byte[] forwardingSecret = generateRandomString(12).getBytes(StandardCharsets.UTF_8);
|
private byte[] forwardingSecret = generateRandomString(12).getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
@Comment({"Announce whether or not your server supports Forge. If you run a modded server, we",
|
@Comment({
|
||||||
"suggest turning this on."})
|
"Announce whether or not your server supports Forge. If you run a modded server, we",
|
||||||
|
"suggest turning this on.",
|
||||||
|
"",
|
||||||
|
"If your network runs one modpack consistently, consider using ping-passthrough = \"mods\"",
|
||||||
|
"instead for a nicer display in the server list."
|
||||||
|
})
|
||||||
@ConfigKey("announce-forge")
|
@ConfigKey("announce-forge")
|
||||||
private boolean announceForge = false;
|
private boolean announceForge = false;
|
||||||
|
|
||||||
|
@Comment({"If enabled (default is false) and the proxy is in online mode, Velocity will kick",
|
||||||
|
"any existing player who is online if a duplicate connection attempt is made."})
|
||||||
|
@ConfigKey("kick-existing-players")
|
||||||
|
private boolean onlineModeKickExistingPlayers = false;
|
||||||
|
|
||||||
|
@Comment({
|
||||||
|
"Should Velocity pass server list ping requests to a backend server?",
|
||||||
|
"Available options:",
|
||||||
|
"- \"disabled\": No pass-through will be done. The velocity.toml and server-icon.png",
|
||||||
|
" will determine the initial server list ping response.",
|
||||||
|
"- \"mods\": Passes only the mod list from your backend server into the response.",
|
||||||
|
" The first server in your try list (or forced host) with a mod list will be",
|
||||||
|
" used. If no backend servers can be contacted, Velocity will not display any",
|
||||||
|
" mod information.",
|
||||||
|
"- \"all\": Passes everything from the backend server into the response. The Velocity",
|
||||||
|
" configuration is used if no servers could be contacted."
|
||||||
|
})
|
||||||
|
@ConfigKey("ping-passthrough")
|
||||||
|
private PingPassthroughMode pingPassthrough;
|
||||||
|
|
||||||
@Table("[servers]")
|
@Table("[servers]")
|
||||||
private final Servers servers;
|
private final Servers servers;
|
||||||
|
|
||||||
@ -109,7 +134,8 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
|
|||||||
|
|
||||||
private VelocityConfiguration(String bind, String motd, int showMaxPlayers, boolean onlineMode,
|
private VelocityConfiguration(String bind, String motd, int showMaxPlayers, boolean onlineMode,
|
||||||
boolean announceForge, PlayerInfoForwarding playerInfoForwardingMode, byte[] forwardingSecret,
|
boolean announceForge, PlayerInfoForwarding playerInfoForwardingMode, byte[] forwardingSecret,
|
||||||
Servers servers, ForcedHosts forcedHosts, Advanced advanced, Query query, Metrics metrics) {
|
boolean onlineModeKickExistingPlayers, PingPassthroughMode pingPassthrough, Servers servers,
|
||||||
|
ForcedHosts forcedHosts, Advanced advanced, Query query, Metrics metrics) {
|
||||||
this.bind = bind;
|
this.bind = bind;
|
||||||
this.motd = motd;
|
this.motd = motd;
|
||||||
this.showMaxPlayers = showMaxPlayers;
|
this.showMaxPlayers = showMaxPlayers;
|
||||||
@ -117,6 +143,8 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
|
|||||||
this.announceForge = announceForge;
|
this.announceForge = announceForge;
|
||||||
this.playerInfoForwardingMode = playerInfoForwardingMode;
|
this.playerInfoForwardingMode = playerInfoForwardingMode;
|
||||||
this.forwardingSecret = forwardingSecret;
|
this.forwardingSecret = forwardingSecret;
|
||||||
|
this.onlineModeKickExistingPlayers = onlineModeKickExistingPlayers;
|
||||||
|
this.pingPassthrough = pingPassthrough;
|
||||||
this.servers = servers;
|
this.servers = servers;
|
||||||
this.forcedHosts = forcedHosts;
|
this.forcedHosts = forcedHosts;
|
||||||
this.advanced = advanced;
|
this.advanced = advanced;
|
||||||
@ -365,10 +393,18 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
|
|||||||
return advanced.isProxyProtocol();
|
return advanced.isProxyProtocol();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean useTcpFastOpen() {
|
||||||
|
return advanced.tcpFastOpen;
|
||||||
|
}
|
||||||
|
|
||||||
public Metrics getMetrics() {
|
public Metrics getMetrics() {
|
||||||
return metrics;
|
return metrics;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PingPassthroughMode getPingPassthrough() {
|
||||||
|
return pingPassthrough;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return MoreObjects.toStringHelper(this)
|
return MoreObjects.toStringHelper(this)
|
||||||
@ -416,6 +452,8 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
|
|||||||
|
|
||||||
String forwardingModeName = toml.getString("player-info-forwarding-mode", "MODERN")
|
String forwardingModeName = toml.getString("player-info-forwarding-mode", "MODERN")
|
||||||
.toUpperCase(Locale.US);
|
.toUpperCase(Locale.US);
|
||||||
|
String passThroughName = toml.getString("ping-passthrough", "DISABLED")
|
||||||
|
.toUpperCase(Locale.US);
|
||||||
|
|
||||||
return new VelocityConfiguration(
|
return new VelocityConfiguration(
|
||||||
toml.getString("bind", "0.0.0.0:25577"),
|
toml.getString("bind", "0.0.0.0:25577"),
|
||||||
@ -425,6 +463,8 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
|
|||||||
toml.getBoolean("announce-forge", false),
|
toml.getBoolean("announce-forge", false),
|
||||||
PlayerInfoForwarding.valueOf(forwardingModeName),
|
PlayerInfoForwarding.valueOf(forwardingModeName),
|
||||||
forwardingSecret,
|
forwardingSecret,
|
||||||
|
toml.getBoolean("kick-existing-players", false),
|
||||||
|
PingPassthroughMode.valueOf(passThroughName),
|
||||||
servers,
|
servers,
|
||||||
forcedHosts,
|
forcedHosts,
|
||||||
advanced,
|
advanced,
|
||||||
@ -443,6 +483,10 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
|
|||||||
return builder.toString();
|
return builder.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isOnlineModeKickExistingPlayers() {
|
||||||
|
return onlineModeKickExistingPlayers;
|
||||||
|
}
|
||||||
|
|
||||||
private static class Servers {
|
private static class Servers {
|
||||||
|
|
||||||
@IsMap
|
@IsMap
|
||||||
@ -596,6 +640,10 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
|
|||||||
@ConfigKey("proxy-protocol")
|
@ConfigKey("proxy-protocol")
|
||||||
private boolean proxyProtocol = false;
|
private boolean proxyProtocol = false;
|
||||||
|
|
||||||
|
@Comment("Enables TCP fast open support on the proxy. Requires the proxy to run on Linux.")
|
||||||
|
@ConfigKey("tcp-fast-open")
|
||||||
|
private boolean tcpFastOpen = false;
|
||||||
|
|
||||||
private Advanced() {
|
private Advanced() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -607,6 +655,7 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
|
|||||||
this.connectionTimeout = toml.getLong("connection-timeout", 5000L).intValue();
|
this.connectionTimeout = toml.getLong("connection-timeout", 5000L).intValue();
|
||||||
this.readTimeout = toml.getLong("read-timeout", 30000L).intValue();
|
this.readTimeout = toml.getLong("read-timeout", 30000L).intValue();
|
||||||
this.proxyProtocol = toml.getBoolean("proxy-protocol", false);
|
this.proxyProtocol = toml.getBoolean("proxy-protocol", false);
|
||||||
|
this.tcpFastOpen = toml.getBoolean("tcp-fast-open", false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -634,6 +683,10 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
|
|||||||
return proxyProtocol;
|
return proxyProtocol;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isTcpFastOpen() {
|
||||||
|
return tcpFastOpen;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "Advanced{"
|
return "Advanced{"
|
||||||
@ -643,6 +696,7 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
|
|||||||
+ ", connectionTimeout=" + connectionTimeout
|
+ ", connectionTimeout=" + connectionTimeout
|
||||||
+ ", readTimeout=" + readTimeout
|
+ ", readTimeout=" + readTimeout
|
||||||
+ ", proxyProtocol=" + proxyProtocol
|
+ ", proxyProtocol=" + proxyProtocol
|
||||||
|
+ ", tcpFastOpen=" + tcpFastOpen
|
||||||
+ '}';
|
+ '}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -131,8 +131,8 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
|||||||
try {
|
try {
|
||||||
sessionHandler.exception(cause);
|
sessionHandler.exception(cause);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
logger.error("{}: exception handling exception", (association != null ? association :
|
logger.error("{}: exception handling exception in {}",
|
||||||
channel.remoteAddress()), cause);
|
(association != null ? association : channel.remoteAddress()), sessionHandler, cause);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,7 +140,7 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
|||||||
if (cause instanceof ReadTimeoutException) {
|
if (cause instanceof ReadTimeoutException) {
|
||||||
logger.error("{}: read timed out", association);
|
logger.error("{}: read timed out", association);
|
||||||
} else {
|
} 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() {
|
public EventLoop eventLoop() {
|
||||||
return channel.eventLoop();
|
return channel.eventLoop();
|
||||||
}
|
}
|
||||||
@ -233,6 +237,8 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
|||||||
* @param autoReading whether or not we should read data automatically
|
* @param autoReading whether or not we should read data automatically
|
||||||
*/
|
*/
|
||||||
public void setAutoReading(boolean autoReading) {
|
public void setAutoReading(boolean autoReading) {
|
||||||
|
ensureInEventLoop();
|
||||||
|
|
||||||
channel.config().setAutoRead(autoReading);
|
channel.config().setAutoRead(autoReading);
|
||||||
if (autoReading) {
|
if (autoReading) {
|
||||||
// For some reason, the channel may not completely read its queued contents once autoread
|
// For some reason, the channel may not completely read its queued contents once autoread
|
||||||
@ -249,6 +255,8 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
|||||||
* @param state the new state
|
* @param state the new state
|
||||||
*/
|
*/
|
||||||
public void setState(StateRegistry state) {
|
public void setState(StateRegistry state) {
|
||||||
|
ensureInEventLoop();
|
||||||
|
|
||||||
this.state = state;
|
this.state = state;
|
||||||
this.channel.pipeline().get(MinecraftEncoder.class).setState(state);
|
this.channel.pipeline().get(MinecraftEncoder.class).setState(state);
|
||||||
this.channel.pipeline().get(MinecraftDecoder.class).setState(state);
|
this.channel.pipeline().get(MinecraftDecoder.class).setState(state);
|
||||||
@ -263,6 +271,8 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
|||||||
* @param protocolVersion the protocol version to use
|
* @param protocolVersion the protocol version to use
|
||||||
*/
|
*/
|
||||||
public void setProtocolVersion(ProtocolVersion protocolVersion) {
|
public void setProtocolVersion(ProtocolVersion protocolVersion) {
|
||||||
|
ensureInEventLoop();
|
||||||
|
|
||||||
this.protocolVersion = protocolVersion;
|
this.protocolVersion = protocolVersion;
|
||||||
this.nextProtocolVersion = protocolVersion;
|
this.nextProtocolVersion = protocolVersion;
|
||||||
if (protocolVersion != ProtocolVersion.LEGACY) {
|
if (protocolVersion != ProtocolVersion.LEGACY) {
|
||||||
@ -284,6 +294,8 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
|||||||
* @param sessionHandler the handler to use
|
* @param sessionHandler the handler to use
|
||||||
*/
|
*/
|
||||||
public void setSessionHandler(MinecraftSessionHandler sessionHandler) {
|
public void setSessionHandler(MinecraftSessionHandler sessionHandler) {
|
||||||
|
ensureInEventLoop();
|
||||||
|
|
||||||
if (this.sessionHandler != null) {
|
if (this.sessionHandler != null) {
|
||||||
this.sessionHandler.deactivated();
|
this.sessionHandler.deactivated();
|
||||||
}
|
}
|
||||||
@ -302,6 +314,7 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
|||||||
*/
|
*/
|
||||||
public void setCompressionThreshold(int threshold) {
|
public void setCompressionThreshold(int threshold) {
|
||||||
ensureOpen();
|
ensureOpen();
|
||||||
|
ensureInEventLoop();
|
||||||
|
|
||||||
if (threshold == -1) {
|
if (threshold == -1) {
|
||||||
channel.pipeline().remove(COMPRESSION_DECODER);
|
channel.pipeline().remove(COMPRESSION_DECODER);
|
||||||
@ -325,6 +338,7 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
|||||||
*/
|
*/
|
||||||
public void enableEncryption(byte[] secret) throws GeneralSecurityException {
|
public void enableEncryption(byte[] secret) throws GeneralSecurityException {
|
||||||
ensureOpen();
|
ensureOpen();
|
||||||
|
ensureInEventLoop();
|
||||||
|
|
||||||
SecretKey key = new SecretKeySpec(secret, "AES");
|
SecretKey key = new SecretKeySpec(secret, "AES");
|
||||||
|
|
||||||
@ -342,6 +356,7 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void setAssociation(MinecraftConnectionAssociation association) {
|
public void setAssociation(MinecraftConnectionAssociation association) {
|
||||||
|
ensureInEventLoop();
|
||||||
this.association = association;
|
this.association = association;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,26 +5,25 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
|||||||
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
|
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
|
||||||
import com.mojang.brigadier.tree.LiteralCommandNode;
|
import com.mojang.brigadier.tree.LiteralCommandNode;
|
||||||
import com.velocitypowered.api.event.connection.PluginMessageEvent;
|
import com.velocitypowered.api.event.connection.PluginMessageEvent;
|
||||||
import com.velocitypowered.api.network.ProtocolVersion;
|
|
||||||
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
import com.velocitypowered.proxy.VelocityServer;
|
||||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||||
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
||||||
import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler;
|
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.connection.util.ConnectionMessages;
|
||||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.AvailableCommands;
|
import com.velocitypowered.proxy.protocol.packet.AvailableCommands;
|
||||||
import com.velocitypowered.proxy.protocol.packet.AvailableCommands.ProtocolSuggestionProvider;
|
import com.velocitypowered.proxy.protocol.packet.AvailableCommands.ProtocolSuggestionProvider;
|
||||||
import com.velocitypowered.proxy.protocol.packet.BossBar;
|
import com.velocitypowered.proxy.protocol.packet.BossBar;
|
||||||
import com.velocitypowered.proxy.protocol.packet.Disconnect;
|
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.KeepAlive;
|
||||||
import com.velocitypowered.proxy.protocol.packet.PlayerListItem;
|
import com.velocitypowered.proxy.protocol.packet.PlayerListItem;
|
||||||
import com.velocitypowered.proxy.protocol.packet.PluginMessage;
|
import com.velocitypowered.proxy.protocol.packet.PluginMessage;
|
||||||
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse;
|
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse;
|
||||||
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
|
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
|
||||||
import io.netty.buffer.ByteBuf;
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import io.netty.buffer.ByteBufUtil;
|
||||||
|
import io.netty.buffer.Unpooled;
|
||||||
|
|
||||||
public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
||||||
|
|
||||||
@ -37,7 +36,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
BackendPlaySessionHandler(VelocityServer server, VelocityServerConnection serverConn) {
|
BackendPlaySessionHandler(VelocityServer server, VelocityServerConnection serverConn) {
|
||||||
this.server = server;
|
this.server = server;
|
||||||
this.serverConn = serverConn;
|
this.serverConn = serverConn;
|
||||||
this.playerConnection = serverConn.getPlayer().getMinecraftConnection();
|
this.playerConnection = serverConn.getPlayer().getConnection();
|
||||||
|
|
||||||
MinecraftSessionHandler psh = playerConnection.getSessionHandler();
|
MinecraftSessionHandler psh = playerConnection.getSessionHandler();
|
||||||
if (!(psh instanceof ClientPlaySessionHandler)) {
|
if (!(psh instanceof ClientPlaySessionHandler)) {
|
||||||
@ -119,12 +118,15 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
byte[] copy = ByteBufUtil.getBytes(packet.content());
|
||||||
PluginMessageEvent event = new PluginMessageEvent(serverConn, serverConn.getPlayer(), id,
|
PluginMessageEvent event = new PluginMessageEvent(serverConn, serverConn.getPlayer(), id,
|
||||||
packet.getData());
|
copy);
|
||||||
server.getEventManager().fire(event)
|
server.getEventManager().fire(event)
|
||||||
.thenAcceptAsync(pme -> {
|
.thenAcceptAsync(pme -> {
|
||||||
if (pme.getResult().isAllowed() && !playerConnection.isClosed()) {
|
if (pme.getResult().isAllowed() && !playerConnection.isClosed()) {
|
||||||
playerConnection.write(packet);
|
PluginMessage copied = new PluginMessage(packet.getChannel(),
|
||||||
|
Unpooled.wrappedBuffer(copy));
|
||||||
|
playerConnection.write(copied);
|
||||||
}
|
}
|
||||||
}, playerConnection.eventLoop());
|
}, playerConnection.eventLoop());
|
||||||
return true;
|
return true;
|
||||||
@ -163,6 +165,9 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleGeneric(MinecraftPacket packet) {
|
public void handleGeneric(MinecraftPacket packet) {
|
||||||
|
if (packet instanceof PluginMessage) {
|
||||||
|
((PluginMessage) packet).retain();
|
||||||
|
}
|
||||||
playerConnection.write(packet);
|
playerConnection.write(packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -185,7 +190,8 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
public void disconnected() {
|
public void disconnected() {
|
||||||
serverConn.getServer().removePlayer(serverConn.getPlayer());
|
serverConn.getServer().removePlayer(serverConn.getPlayer());
|
||||||
if (!serverConn.isGracefulDisconnect() && !exceptionTriggered) {
|
if (!serverConn.isGracefulDisconnect() && !exceptionTriggered) {
|
||||||
serverConn.getPlayer().disconnect(ConnectionMessages.UNEXPECTED_DISCONNECT);
|
serverConn.getPlayer().handleConnectionException(serverConn.getServer(),
|
||||||
|
Disconnect.create(ConnectionMessages.UNEXPECTED_DISCONNECT), true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package com.velocitypowered.proxy.connection.backend;
|
package com.velocitypowered.proxy.connection.backend;
|
||||||
|
|
||||||
import com.velocitypowered.api.proxy.ConnectionRequestBuilder;
|
|
||||||
import com.velocitypowered.api.util.GameProfile;
|
import com.velocitypowered.api.util.GameProfile;
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
import com.velocitypowered.proxy.VelocityServer;
|
||||||
import com.velocitypowered.proxy.config.PlayerInfoForwarding;
|
import com.velocitypowered.proxy.config.PlayerInfoForwarding;
|
||||||
@ -56,23 +55,16 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
|||||||
MinecraftConnection mc = serverConn.ensureConnected();
|
MinecraftConnection mc = serverConn.ensureConnected();
|
||||||
VelocityConfiguration configuration = server.getConfiguration();
|
VelocityConfiguration configuration = server.getConfiguration();
|
||||||
if (configuration.getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN && packet
|
if (configuration.getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN && packet
|
||||||
.getChannel()
|
.getChannel().equals(VelocityConstants.VELOCITY_IP_FORWARDING_CHANNEL)) {
|
||||||
.equals(VelocityConstants.VELOCITY_IP_FORWARDING_CHANNEL)) {
|
ByteBuf forwardingData = createForwardingData(configuration.getForwardingSecret(),
|
||||||
LoginPluginResponse response = new LoginPluginResponse();
|
|
||||||
response.setSuccess(true);
|
|
||||||
response.setId(packet.getId());
|
|
||||||
response.setData(createForwardingData(configuration.getForwardingSecret(),
|
|
||||||
serverConn.getPlayer().getRemoteAddress().getHostString(),
|
serverConn.getPlayer().getRemoteAddress().getHostString(),
|
||||||
serverConn.getPlayer().getGameProfile()));
|
serverConn.getPlayer().getGameProfile());
|
||||||
|
LoginPluginResponse response = new LoginPluginResponse(packet.getId(), true, forwardingData);
|
||||||
mc.write(response);
|
mc.write(response);
|
||||||
informationForwarded = true;
|
informationForwarded = true;
|
||||||
} else {
|
} else {
|
||||||
// Don't understand
|
// Don't understand
|
||||||
LoginPluginResponse response = new LoginPluginResponse();
|
mc.write(new LoginPluginResponse(packet.getId(), false, Unpooled.EMPTY_BUFFER));
|
||||||
response.setSuccess(false);
|
|
||||||
response.setId(packet.getId());
|
|
||||||
response.setData(Unpooled.EMPTY_BUFFER);
|
|
||||||
mc.write(response);
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -125,32 +117,28 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
|||||||
|
|
||||||
private static ByteBuf createForwardingData(byte[] hmacSecret, String address,
|
private static ByteBuf createForwardingData(byte[] hmacSecret, String address,
|
||||||
GameProfile profile) {
|
GameProfile profile) {
|
||||||
ByteBuf dataToForward = Unpooled.buffer();
|
ByteBuf forwarded = Unpooled.buffer(2048);
|
||||||
ByteBuf finalData = Unpooled.buffer();
|
|
||||||
try {
|
try {
|
||||||
ProtocolUtils.writeVarInt(dataToForward, VelocityConstants.FORWARDING_VERSION);
|
ProtocolUtils.writeVarInt(forwarded, VelocityConstants.FORWARDING_VERSION);
|
||||||
ProtocolUtils.writeString(dataToForward, address);
|
ProtocolUtils.writeString(forwarded, address);
|
||||||
ProtocolUtils.writeUuid(dataToForward, profile.getId());
|
ProtocolUtils.writeUuid(forwarded, profile.getId());
|
||||||
ProtocolUtils.writeString(dataToForward, profile.getName());
|
ProtocolUtils.writeString(forwarded, profile.getName());
|
||||||
ProtocolUtils.writeProperties(dataToForward, profile.getProperties());
|
ProtocolUtils.writeProperties(forwarded, profile.getProperties());
|
||||||
|
|
||||||
SecretKey key = new SecretKeySpec(hmacSecret, "HmacSHA256");
|
SecretKey key = new SecretKeySpec(hmacSecret, "HmacSHA256");
|
||||||
Mac mac = Mac.getInstance("HmacSHA256");
|
Mac mac = Mac.getInstance("HmacSHA256");
|
||||||
mac.init(key);
|
mac.init(key);
|
||||||
mac.update(dataToForward.array(), dataToForward.arrayOffset(), dataToForward.readableBytes());
|
mac.update(forwarded.array(), forwarded.arrayOffset(), forwarded.readableBytes());
|
||||||
byte[] sig = mac.doFinal();
|
byte[] sig = mac.doFinal();
|
||||||
finalData.writeBytes(sig);
|
|
||||||
finalData.writeBytes(dataToForward);
|
return Unpooled.wrappedBuffer(Unpooled.wrappedBuffer(sig), forwarded);
|
||||||
return finalData;
|
|
||||||
} catch (InvalidKeyException e) {
|
} catch (InvalidKeyException e) {
|
||||||
finalData.release();
|
forwarded.release();
|
||||||
throw new RuntimeException("Unable to authenticate data", e);
|
throw new RuntimeException("Unable to authenticate data", e);
|
||||||
} catch (NoSuchAlgorithmException e) {
|
} catch (NoSuchAlgorithmException e) {
|
||||||
// Should never happen
|
// Should never happen
|
||||||
finalData.release();
|
forwarded.release();
|
||||||
throw new AssertionError(e);
|
throw new AssertionError(e);
|
||||||
} finally {
|
|
||||||
dataToForward.release();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -84,13 +84,13 @@ public class TransitionSessionHandler implements MinecraftSessionHandler {
|
|||||||
.whenCompleteAsync((x, error) -> {
|
.whenCompleteAsync((x, error) -> {
|
||||||
// Strap on the ClientPlaySessionHandler if required.
|
// Strap on the ClientPlaySessionHandler if required.
|
||||||
ClientPlaySessionHandler playHandler;
|
ClientPlaySessionHandler playHandler;
|
||||||
if (serverConn.getPlayer().getMinecraftConnection().getSessionHandler()
|
if (serverConn.getPlayer().getConnection().getSessionHandler()
|
||||||
instanceof ClientPlaySessionHandler) {
|
instanceof ClientPlaySessionHandler) {
|
||||||
playHandler = (ClientPlaySessionHandler) serverConn.getPlayer().getMinecraftConnection()
|
playHandler = (ClientPlaySessionHandler) serverConn.getPlayer().getConnection()
|
||||||
.getSessionHandler();
|
.getSessionHandler();
|
||||||
} else {
|
} else {
|
||||||
playHandler = new ClientPlaySessionHandler(server, serverConn.getPlayer());
|
playHandler = new ClientPlaySessionHandler(server, serverConn.getPlayer());
|
||||||
serverConn.getPlayer().getMinecraftConnection().setSessionHandler(playHandler);
|
serverConn.getPlayer().getConnection().setSessionHandler(playHandler);
|
||||||
}
|
}
|
||||||
playHandler.handleBackendJoinGame(packet, serverConn);
|
playHandler.handleBackendJoinGame(packet, serverConn);
|
||||||
|
|
||||||
@ -167,7 +167,7 @@ public class TransitionSessionHandler implements MinecraftSessionHandler {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
serverConn.getPlayer().getMinecraftConnection().write(packet);
|
serverConn.getPlayer().getConnection().write(packet.retain());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,7 +12,6 @@ import static com.velocitypowered.proxy.network.Connections.READ_TIMEOUT;
|
|||||||
|
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
import com.velocitypowered.api.network.ProtocolVersion;
|
import com.velocitypowered.api.network.ProtocolVersion;
|
||||||
import com.velocitypowered.api.proxy.ConnectionRequestBuilder;
|
|
||||||
import com.velocitypowered.api.proxy.ServerConnection;
|
import com.velocitypowered.api.proxy.ServerConnection;
|
||||||
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
||||||
import com.velocitypowered.api.proxy.server.ServerInfo;
|
import com.velocitypowered.api.proxy.server.ServerInfo;
|
||||||
@ -33,6 +32,7 @@ import com.velocitypowered.proxy.protocol.packet.Handshake;
|
|||||||
import com.velocitypowered.proxy.protocol.packet.PluginMessage;
|
import com.velocitypowered.proxy.protocol.packet.PluginMessage;
|
||||||
import com.velocitypowered.proxy.protocol.packet.ServerLogin;
|
import com.velocitypowered.proxy.protocol.packet.ServerLogin;
|
||||||
import com.velocitypowered.proxy.server.VelocityRegisteredServer;
|
import com.velocitypowered.proxy.server.VelocityRegisteredServer;
|
||||||
|
import io.netty.buffer.Unpooled;
|
||||||
import io.netty.channel.Channel;
|
import io.netty.channel.Channel;
|
||||||
import io.netty.channel.ChannelFutureListener;
|
import io.netty.channel.ChannelFutureListener;
|
||||||
import io.netty.channel.ChannelInitializer;
|
import io.netty.channel.ChannelInitializer;
|
||||||
@ -76,7 +76,7 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
|
|||||||
CompletableFuture<Impl> result = new CompletableFuture<>();
|
CompletableFuture<Impl> result = new CompletableFuture<>();
|
||||||
// Note: we use the event loop for the connection the player is on. This reduces context
|
// Note: we use the event loop for the connection the player is on. This reduces context
|
||||||
// switches.
|
// switches.
|
||||||
server.initializeGenericBootstrap(proxyPlayer.getMinecraftConnection().eventLoop())
|
server.createBootstrap(proxyPlayer.getConnection().eventLoop())
|
||||||
.handler(new ChannelInitializer<Channel>() {
|
.handler(new ChannelInitializer<Channel>() {
|
||||||
@Override
|
@Override
|
||||||
protected void initChannel(Channel ch) throws Exception {
|
protected void initChannel(Channel ch) throws Exception {
|
||||||
@ -138,23 +138,24 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
|
|||||||
PlayerInfoForwarding forwardingMode = server.getConfiguration().getPlayerInfoForwardingMode();
|
PlayerInfoForwarding forwardingMode = server.getConfiguration().getPlayerInfoForwardingMode();
|
||||||
|
|
||||||
// Initiate the handshake.
|
// Initiate the handshake.
|
||||||
ProtocolVersion protocolVersion = proxyPlayer.getMinecraftConnection().getNextProtocolVersion();
|
ProtocolVersion protocolVersion = proxyPlayer.getConnection().getNextProtocolVersion();
|
||||||
Handshake handshake = new Handshake();
|
Handshake handshake = new Handshake();
|
||||||
handshake.setNextStatus(StateRegistry.LOGIN_ID);
|
handshake.setNextStatus(StateRegistry.LOGIN_ID);
|
||||||
handshake.setProtocolVersion(protocolVersion);
|
handshake.setProtocolVersion(protocolVersion);
|
||||||
if (forwardingMode == PlayerInfoForwarding.LEGACY) {
|
if (forwardingMode == PlayerInfoForwarding.LEGACY) {
|
||||||
handshake.setServerAddress(createLegacyForwardingAddress());
|
handshake.setServerAddress(createLegacyForwardingAddress());
|
||||||
} else if (proxyPlayer.getMinecraftConnection().getType() == ConnectionTypes.LEGACY_FORGE) {
|
} else if (proxyPlayer.getConnection().getType() == ConnectionTypes.LEGACY_FORGE) {
|
||||||
handshake.setServerAddress(handshake.getServerAddress() + HANDSHAKE_HOSTNAME_TOKEN);
|
handshake.setServerAddress(handshake.getServerAddress() + HANDSHAKE_HOSTNAME_TOKEN);
|
||||||
} else {
|
} else {
|
||||||
handshake.setServerAddress(registeredServer.getServerInfo().getAddress().getHostString());
|
handshake.setServerAddress(registeredServer.getServerInfo().getAddress().getHostString());
|
||||||
}
|
}
|
||||||
handshake.setPort(registeredServer.getServerInfo().getAddress().getPort());
|
handshake.setPort(registeredServer.getServerInfo().getAddress().getPort());
|
||||||
mc.write(handshake);
|
mc.delayedWrite(handshake);
|
||||||
|
|
||||||
mc.setProtocolVersion(protocolVersion);
|
mc.setProtocolVersion(protocolVersion);
|
||||||
mc.setState(StateRegistry.LOGIN);
|
mc.setState(StateRegistry.LOGIN);
|
||||||
mc.write(new ServerLogin(proxyPlayer.getUsername()));
|
mc.delayedWrite(new ServerLogin(proxyPlayer.getUsername()));
|
||||||
|
mc.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
public @Nullable MinecraftConnection getConnection() {
|
public @Nullable MinecraftConnection getConnection() {
|
||||||
@ -212,9 +213,7 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
|
|||||||
|
|
||||||
MinecraftConnection mc = ensureConnected();
|
MinecraftConnection mc = ensureConnected();
|
||||||
|
|
||||||
PluginMessage message = new PluginMessage();
|
PluginMessage message = new PluginMessage(identifier.getId(), Unpooled.wrappedBuffer(data));
|
||||||
message.setChannel(identifier.getId());
|
|
||||||
message.setData(data);
|
|
||||||
mc.write(message);
|
mc.write(message);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -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.connection.PluginMessageEvent;
|
||||||
import com.velocitypowered.api.event.player.PlayerChatEvent;
|
import com.velocitypowered.api.event.player.PlayerChatEvent;
|
||||||
import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent;
|
import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent;
|
||||||
|
import com.velocitypowered.api.event.player.TabCompleteEvent;
|
||||||
import com.velocitypowered.api.network.ProtocolVersion;
|
import com.velocitypowered.api.network.ProtocolVersion;
|
||||||
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
import com.velocitypowered.proxy.VelocityServer;
|
||||||
@ -29,14 +30,15 @@ import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse.Offer;
|
|||||||
import com.velocitypowered.proxy.protocol.packet.TitlePacket;
|
import com.velocitypowered.proxy.protocol.packet.TitlePacket;
|
||||||
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
|
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
|
||||||
import io.netty.buffer.ByteBuf;
|
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.ArrayDeque;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import net.kyori.text.TextComponent;
|
import net.kyori.text.TextComponent;
|
||||||
import net.kyori.text.format.TextColor;
|
import net.kyori.text.format.TextColor;
|
||||||
@ -57,7 +59,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
private final List<UUID> serverBossBars = new ArrayList<>();
|
private final List<UUID> serverBossBars = new ArrayList<>();
|
||||||
private final Queue<PluginMessage> loginPluginMessages = new ArrayDeque<>();
|
private final Queue<PluginMessage> loginPluginMessages = new ArrayDeque<>();
|
||||||
private final VelocityServer server;
|
private final VelocityServer server;
|
||||||
private @Nullable TabCompleteRequest legacyCommandTabComplete;
|
private @Nullable TabCompleteRequest outstandingTabComplete;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a client play session handler.
|
* Constructs a client play session handler.
|
||||||
@ -75,11 +77,18 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
.getProtocolVersion());
|
.getProtocolVersion());
|
||||||
if (!channels.isEmpty()) {
|
if (!channels.isEmpty()) {
|
||||||
PluginMessage register = constructChannelsPacket(player.getProtocolVersion(), channels);
|
PluginMessage register = constructChannelsPacket(player.getProtocolVersion(), channels);
|
||||||
player.getMinecraftConnection().write(register);
|
player.getConnection().write(register);
|
||||||
player.getKnownChannels().addAll(channels);
|
player.getKnownChannels().addAll(channels);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deactivated() {
|
||||||
|
for (PluginMessage message : loginPluginMessages) {
|
||||||
|
ReferenceCountUtil.release(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean handle(KeepAlive packet) {
|
public boolean handle(KeepAlive packet) {
|
||||||
VelocityServerConnection serverConnection = player.getConnectedServer();
|
VelocityServerConnection serverConnection = player.getConnectedServer();
|
||||||
@ -146,25 +155,226 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
public boolean handle(TabCompleteRequest packet) {
|
public boolean handle(TabCompleteRequest packet) {
|
||||||
boolean isCommand = !packet.isAssumeCommand() && packet.getCommand().startsWith("/");
|
boolean isCommand = !packet.isAssumeCommand() && packet.getCommand().startsWith("/");
|
||||||
|
|
||||||
if (!isCommand) {
|
if (isCommand) {
|
||||||
// We can't deal with anything else.
|
return this.handleCommandTabComplete(packet);
|
||||||
return false;
|
} 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.
|
// In 1.13+, we need to do additional work for the richer suggestions available.
|
||||||
String command = packet.getCommand().substring(1);
|
String command = packet.getCommand().substring(1);
|
||||||
int spacePos = command.indexOf(' ');
|
int spacePos = command.indexOf(' ');
|
||||||
if (spacePos == -1) {
|
if (spacePos == -1) {
|
||||||
return false;
|
spacePos = command.length();
|
||||||
}
|
}
|
||||||
|
|
||||||
String commandLabel = command.substring(0, spacePos);
|
String commandLabel = command.substring(0, spacePos);
|
||||||
if (!server.getCommandManager().hasCommand(commandLabel)) {
|
if (!server.getCommandManager().hasCommand(commandLabel)) {
|
||||||
if (player.getProtocolVersion().compareTo(MINECRAFT_1_13) < 0) {
|
if (player.getProtocolVersion().compareTo(MINECRAFT_1_13) < 0) {
|
||||||
// Outstanding tab completes are recorded for use with 1.12 clients and below to provide
|
// 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
|
// additional tab completion support.
|
||||||
// us.
|
outstandingTabComplete = packet;
|
||||||
legacyCommandTabComplete = packet;
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -199,221 +409,67 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
resp.setLength(length);
|
resp.setLength(length);
|
||||||
resp.getOffers().addAll(offers);
|
resp.getOffers().addAll(offers);
|
||||||
|
|
||||||
player.getMinecraftConnection().write(resp);
|
player.getConnection().write(resp);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private boolean handleRegularTabComplete(TabCompleteRequest packet) {
|
||||||
public boolean handle(PluginMessage packet) {
|
if (player.getProtocolVersion().compareTo(MINECRAFT_1_13) < 0) {
|
||||||
VelocityServerConnection serverConn = player.getConnectedServer();
|
// Outstanding tab completes are recorded for use with 1.12 clients and below to provide
|
||||||
MinecraftConnection backendConn = serverConn != null ? serverConn.getConnection() : null;
|
// additional tab completion support.
|
||||||
if (serverConn != null && backendConn != null) {
|
outstandingTabComplete = packet;
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean handle(ResourcePackResponse packet) {
|
|
||||||
server.getEventManager().fireAndForget(new PlayerResourcePackStatusEvent(player,
|
|
||||||
packet.getStatus()));
|
|
||||||
return false;
|
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
|
* Handles additional tab complete.
|
||||||
* 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.
|
|
||||||
*
|
*
|
||||||
* @param response the tab complete response from the backend
|
* @param response the tab complete response from the backend
|
||||||
*/
|
*/
|
||||||
public void handleTabCompleteResponse(TabCompleteResponse response) {
|
public void handleTabCompleteResponse(TabCompleteResponse response) {
|
||||||
if (legacyCommandTabComplete != null) {
|
if (outstandingTabComplete != null) {
|
||||||
String command = legacyCommandTabComplete.getCommand().substring(1);
|
if (outstandingTabComplete.isAssumeCommand()) {
|
||||||
try {
|
return; // used for command blocks which can't run Velocity commands anyway
|
||||||
List<String> offers = server.getCommandManager().offerSuggestions(player, command);
|
|
||||||
for (String offer : offers) {
|
|
||||||
response.getOffers().add(new Offer(offer, null));
|
|
||||||
}
|
|
||||||
response.getOffers().sort(null);
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("Unable to provide tab list completions for {} for command '{}'",
|
|
||||||
player.getUsername(),
|
|
||||||
command, e);
|
|
||||||
}
|
}
|
||||||
legacyCommandTabComplete = null;
|
if (outstandingTabComplete.getCommand().startsWith("/")) {
|
||||||
|
this.finishCommandTabComplete(outstandingTabComplete, response);
|
||||||
|
} else {
|
||||||
|
this.finishRegularTabComplete(outstandingTabComplete, response);
|
||||||
|
}
|
||||||
|
outstandingTabComplete = null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
player.getMinecraftConnection().write(response);
|
private void finishCommandTabComplete(TabCompleteRequest request, TabCompleteResponse response) {
|
||||||
|
String command = request.getCommand().substring(1);
|
||||||
|
try {
|
||||||
|
List<String> offers = server.getCommandManager().offerSuggestions(player, command);
|
||||||
|
for (String offer : offers) {
|
||||||
|
response.getOffers().add(new Offer(offer, null));
|
||||||
|
}
|
||||||
|
response.getOffers().sort(null);
|
||||||
|
player.getConnection().write(response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Unable to provide tab list completions for {} for command '{}'",
|
||||||
|
player.getUsername(),
|
||||||
|
command, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finishRegularTabComplete(TabCompleteRequest request, TabCompleteResponse response) {
|
||||||
|
List<String> offers = new ArrayList<>();
|
||||||
|
for (Offer offer : response.getOffers()) {
|
||||||
|
offers.add(offer.getText());
|
||||||
|
}
|
||||||
|
server.getEventManager().fire(new TabCompleteEvent(player, request.getCommand(), offers))
|
||||||
|
.thenAcceptAsync(e -> {
|
||||||
|
response.getOffers().clear();
|
||||||
|
for (String s : e.getSuggestions()) {
|
||||||
|
response.getOffers().add(new Offer(s));
|
||||||
|
}
|
||||||
|
player.getConnection().write(response);
|
||||||
|
}, player.getConnection().eventLoop());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -49,6 +49,7 @@ import com.velocitypowered.proxy.tablist.VelocityTabList;
|
|||||||
import com.velocitypowered.proxy.util.VelocityMessages;
|
import com.velocitypowered.proxy.util.VelocityMessages;
|
||||||
import com.velocitypowered.proxy.util.collect.CappedSet;
|
import com.velocitypowered.proxy.util.collect.CappedSet;
|
||||||
import io.netty.buffer.ByteBufUtil;
|
import io.netty.buffer.ByteBufUtil;
|
||||||
|
import io.netty.buffer.Unpooled;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
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.
|
* The actual Minecraft connection. This is actually a wrapper object around the Netty channel.
|
||||||
*/
|
*/
|
||||||
private final MinecraftConnection minecraftConnection;
|
private final MinecraftConnection connection;
|
||||||
private final @Nullable InetSocketAddress virtualHost;
|
private final @Nullable InetSocketAddress virtualHost;
|
||||||
private GameProfile profile;
|
private GameProfile profile;
|
||||||
private PermissionFunction permissionFunction;
|
private PermissionFunction permissionFunction;
|
||||||
@ -96,18 +97,19 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
private final VelocityServer server;
|
private final VelocityServer server;
|
||||||
private ClientConnectionPhase connectionPhase;
|
private ClientConnectionPhase connectionPhase;
|
||||||
private final Collection<String> knownChannels;
|
private final Collection<String> knownChannels;
|
||||||
|
private final CompletableFuture<Void> teardownFuture = new CompletableFuture<>();
|
||||||
|
|
||||||
private @MonotonicNonNull List<String> serversToTry = null;
|
private @MonotonicNonNull List<String> serversToTry = null;
|
||||||
|
|
||||||
ConnectedPlayer(VelocityServer server, GameProfile profile,
|
ConnectedPlayer(VelocityServer server, GameProfile profile,
|
||||||
MinecraftConnection minecraftConnection, @Nullable InetSocketAddress virtualHost) {
|
MinecraftConnection connection, @Nullable InetSocketAddress virtualHost) {
|
||||||
this.server = server;
|
this.server = server;
|
||||||
this.tabList = new VelocityTabList(minecraftConnection);
|
this.tabList = new VelocityTabList(connection);
|
||||||
this.profile = profile;
|
this.profile = profile;
|
||||||
this.minecraftConnection = minecraftConnection;
|
this.connection = connection;
|
||||||
this.virtualHost = virtualHost;
|
this.virtualHost = virtualHost;
|
||||||
this.permissionFunction = PermissionFunction.ALWAYS_UNDEFINED;
|
this.permissionFunction = PermissionFunction.ALWAYS_UNDEFINED;
|
||||||
this.connectionPhase = minecraftConnection.getType().getInitialClientPhase();
|
this.connectionPhase = connection.getType().getInitialClientPhase();
|
||||||
this.knownChannels = CappedSet.create(MAX_PLUGIN_CHANNELS);
|
this.knownChannels = CappedSet.create(MAX_PLUGIN_CHANNELS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,8 +133,8 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
return profile;
|
return profile;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MinecraftConnection getMinecraftConnection() {
|
public MinecraftConnection getConnection() {
|
||||||
return minecraftConnection;
|
return connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -167,7 +169,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public InetSocketAddress getRemoteAddress() {
|
public InetSocketAddress getRemoteAddress() {
|
||||||
return (InetSocketAddress) minecraftConnection.getRemoteAddress();
|
return (InetSocketAddress) connection.getRemoteAddress();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -181,12 +183,12 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isActive() {
|
public boolean isActive() {
|
||||||
return minecraftConnection.getChannel().isActive();
|
return connection.getChannel().isActive();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ProtocolVersion getProtocolVersion() {
|
public ProtocolVersion getProtocolVersion() {
|
||||||
return minecraftConnection.getProtocolVersion();
|
return connection.getProtocolVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -202,7 +204,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
TitlePacket pkt = new TitlePacket();
|
TitlePacket pkt = new TitlePacket();
|
||||||
pkt.setAction(TitlePacket.SET_ACTION_BAR);
|
pkt.setAction(TitlePacket.SET_ACTION_BAR);
|
||||||
pkt.setComponent(GsonComponentSerializer.INSTANCE.serialize(component));
|
pkt.setComponent(GsonComponentSerializer.INSTANCE.serialize(component));
|
||||||
minecraftConnection.write(pkt);
|
connection.write(pkt);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
// Due to issues with action bar packets, we'll need to convert the text message into a
|
// Due to issues with action bar packets, we'll need to convert the text message into a
|
||||||
@ -218,7 +220,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
Chat chat = new Chat();
|
Chat chat = new Chat();
|
||||||
chat.setType(pos);
|
chat.setType(pos);
|
||||||
chat.setMessage(json);
|
chat.setMessage(json);
|
||||||
minecraftConnection.write(chat);
|
connection.write(chat);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -255,23 +257,23 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
public void disconnect(Component reason) {
|
public void disconnect(Component reason) {
|
||||||
logger.info("{} has disconnected: {}", this,
|
logger.info("{} has disconnected: {}", this,
|
||||||
LegacyComponentSerializer.legacy().serialize(reason));
|
LegacyComponentSerializer.legacy().serialize(reason));
|
||||||
minecraftConnection.closeWith(Disconnect.create(reason));
|
connection.closeWith(Disconnect.create(reason));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void sendTitle(Title title) {
|
public void sendTitle(Title title) {
|
||||||
Preconditions.checkNotNull(title, "title");
|
Preconditions.checkNotNull(title, "title");
|
||||||
|
|
||||||
ProtocolVersion protocolVersion = minecraftConnection.getProtocolVersion();
|
ProtocolVersion protocolVersion = connection.getProtocolVersion();
|
||||||
if (title.equals(Titles.reset())) {
|
if (title.equals(Titles.reset())) {
|
||||||
minecraftConnection.write(TitlePacket.resetForProtocolVersion(protocolVersion));
|
connection.write(TitlePacket.resetForProtocolVersion(protocolVersion));
|
||||||
} else if (title.equals(Titles.hide())) {
|
} else if (title.equals(Titles.hide())) {
|
||||||
minecraftConnection.write(TitlePacket.hideForProtocolVersion(protocolVersion));
|
connection.write(TitlePacket.hideForProtocolVersion(protocolVersion));
|
||||||
} else if (title instanceof TextTitle) {
|
} else if (title instanceof TextTitle) {
|
||||||
TextTitle tt = (TextTitle) title;
|
TextTitle tt = (TextTitle) title;
|
||||||
|
|
||||||
if (tt.isResetBeforeSend()) {
|
if (tt.isResetBeforeSend()) {
|
||||||
minecraftConnection.delayedWrite(TitlePacket.resetForProtocolVersion(protocolVersion));
|
connection.delayedWrite(TitlePacket.resetForProtocolVersion(protocolVersion));
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<Component> titleText = tt.getTitle();
|
Optional<Component> titleText = tt.getTitle();
|
||||||
@ -279,7 +281,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
TitlePacket titlePkt = new TitlePacket();
|
TitlePacket titlePkt = new TitlePacket();
|
||||||
titlePkt.setAction(TitlePacket.SET_TITLE);
|
titlePkt.setAction(TitlePacket.SET_TITLE);
|
||||||
titlePkt.setComponent(GsonComponentSerializer.INSTANCE.serialize(titleText.get()));
|
titlePkt.setComponent(GsonComponentSerializer.INSTANCE.serialize(titleText.get()));
|
||||||
minecraftConnection.delayedWrite(titlePkt);
|
connection.delayedWrite(titlePkt);
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<Component> subtitleText = tt.getSubtitle();
|
Optional<Component> subtitleText = tt.getSubtitle();
|
||||||
@ -287,7 +289,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
TitlePacket titlePkt = new TitlePacket();
|
TitlePacket titlePkt = new TitlePacket();
|
||||||
titlePkt.setAction(TitlePacket.SET_SUBTITLE);
|
titlePkt.setAction(TitlePacket.SET_SUBTITLE);
|
||||||
titlePkt.setComponent(GsonComponentSerializer.INSTANCE.serialize(subtitleText.get()));
|
titlePkt.setComponent(GsonComponentSerializer.INSTANCE.serialize(subtitleText.get()));
|
||||||
minecraftConnection.delayedWrite(titlePkt);
|
connection.delayedWrite(titlePkt);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tt.areTimesSet()) {
|
if (tt.areTimesSet()) {
|
||||||
@ -295,9 +297,9 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
timesPkt.setFadeIn(tt.getFadeIn());
|
timesPkt.setFadeIn(tt.getFadeIn());
|
||||||
timesPkt.setStay(tt.getStay());
|
timesPkt.setStay(tt.getStay());
|
||||||
timesPkt.setFadeOut(tt.getFadeOut());
|
timesPkt.setFadeOut(tt.getFadeOut());
|
||||||
minecraftConnection.delayedWrite(timesPkt);
|
connection.delayedWrite(timesPkt);
|
||||||
}
|
}
|
||||||
minecraftConnection.flush();
|
connection.flush();
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalArgumentException("Unknown title class " + title.getClass().getName());
|
throw new IllegalArgumentException("Unknown title class " + title.getClass().getName());
|
||||||
}
|
}
|
||||||
@ -404,18 +406,14 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (connectedServer == null) {
|
if (connectedServer == null) {
|
||||||
// The player isn't yet connected to a server. Note that we need to do this in a future run
|
Optional<RegisteredServer> nextServer = getNextServerToTry(rs);
|
||||||
// of the event loop due to an issue with the Netty kqueue transport.
|
if (nextServer.isPresent()) {
|
||||||
minecraftConnection.eventLoop().execute(() -> {
|
// There can't be any connection in flight now.
|
||||||
Optional<RegisteredServer> nextServer = getNextServerToTry(rs);
|
resetInFlightConnection();
|
||||||
if (nextServer.isPresent()) {
|
createConnectionRequest(nextServer.get()).fireAndForget();
|
||||||
// There can't be any connection in flight now.
|
} else {
|
||||||
resetInFlightConnection();
|
disconnect(friendlyReason);
|
||||||
createConnectionRequest(nextServer.get()).fireAndForget();
|
}
|
||||||
} else {
|
|
||||||
disconnect(friendlyReason);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
boolean kickedFromCurrent = connectedServer.getServer().equals(rs);
|
boolean kickedFromCurrent = connectedServer.getServer().equals(rs);
|
||||||
ServerKickResult result;
|
ServerKickResult result;
|
||||||
@ -453,9 +451,9 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
if (newResult == null || !newResult) {
|
if (newResult == null || !newResult) {
|
||||||
disconnect(friendlyReason);
|
disconnect(friendlyReason);
|
||||||
} else {
|
} else {
|
||||||
sendMessage(VelocityMessages.MOVED_TO_NEW_SERVER);
|
sendMessage(VelocityMessages.MOVED_TO_NEW_SERVER.append(friendlyReason));
|
||||||
}
|
}
|
||||||
}, minecraftConnection.eventLoop());
|
}, connection.eventLoop());
|
||||||
} else if (event.getResult() instanceof Notify) {
|
} else if (event.getResult() instanceof Notify) {
|
||||||
Notify res = (Notify) event.getResult();
|
Notify res = (Notify) event.getResult();
|
||||||
if (event.kickedDuringServerConnect()) {
|
if (event.kickedDuringServerConnect()) {
|
||||||
@ -467,7 +465,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
// In case someone gets creative, assume we want to disconnect the player.
|
// In case someone gets creative, assume we want to disconnect the player.
|
||||||
disconnect(friendlyReason);
|
disconnect(friendlyReason);
|
||||||
}
|
}
|
||||||
}, minecraftConnection.eventLoop());
|
}, connection.eventLoop());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -557,7 +555,12 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
connectedServer.disconnect();
|
connectedServer.disconnect();
|
||||||
}
|
}
|
||||||
server.unregisterConnection(this);
|
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
|
@Override
|
||||||
@ -574,10 +577,8 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
public boolean sendPluginMessage(ChannelIdentifier identifier, byte[] data) {
|
public boolean sendPluginMessage(ChannelIdentifier identifier, byte[] data) {
|
||||||
Preconditions.checkNotNull(identifier, "identifier");
|
Preconditions.checkNotNull(identifier, "identifier");
|
||||||
Preconditions.checkNotNull(data, "data");
|
Preconditions.checkNotNull(data, "data");
|
||||||
PluginMessage message = new PluginMessage();
|
PluginMessage message = new PluginMessage(identifier.getId(), Unpooled.wrappedBuffer(data));
|
||||||
message.setChannel(identifier.getId());
|
connection.write(message);
|
||||||
message.setData(data);
|
|
||||||
minecraftConnection.write(message);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -596,7 +597,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
ResourcePackRequest request = new ResourcePackRequest();
|
ResourcePackRequest request = new ResourcePackRequest();
|
||||||
request.setUrl(url);
|
request.setUrl(url);
|
||||||
request.setHash("");
|
request.setHash("");
|
||||||
minecraftConnection.write(request);
|
connection.write(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -608,7 +609,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
ResourcePackRequest request = new ResourcePackRequest();
|
ResourcePackRequest request = new ResourcePackRequest();
|
||||||
request.setUrl(url);
|
request.setUrl(url);
|
||||||
request.setHash(ByteBufUtil.hexDump(hash));
|
request.setHash(ByteBufUtil.hexDump(hash));
|
||||||
minecraftConnection.write(request);
|
connection.write(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -617,10 +618,10 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
* ID last sent by the server.
|
* ID last sent by the server.
|
||||||
*/
|
*/
|
||||||
public void sendKeepAlive() {
|
public void sendKeepAlive() {
|
||||||
if (minecraftConnection.getState() == StateRegistry.PLAY) {
|
if (connection.getState() == StateRegistry.PLAY) {
|
||||||
KeepAlive keepAlive = new KeepAlive();
|
KeepAlive keepAlive = new KeepAlive();
|
||||||
keepAlive.setRandomId(ThreadLocalRandom.current().nextLong());
|
keepAlive.setRandomId(ThreadLocalRandom.current().nextLong());
|
||||||
minecraftConnection.write(keepAlive);
|
connection.write(keepAlive);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -744,8 +745,10 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
if (status != null && !status.isSafe()) {
|
if (status != null && !status.isSafe()) {
|
||||||
// If it's not safe to continue the connection we need to shut it down.
|
// If it's not safe to continue the connection we need to shut it down.
|
||||||
handleConnectionException(status.getAttemptedConnection(), throwable, true);
|
handleConnectionException(status.getAttemptedConnection(), throwable, true);
|
||||||
|
} else if ((status != null && !status.isSuccessful())) {
|
||||||
|
resetInFlightConnection();
|
||||||
}
|
}
|
||||||
})
|
}, connection.eventLoop())
|
||||||
.thenApply(x -> x);
|
.thenApply(x -> x);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -778,7 +781,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
|||||||
// The only remaining value is successful (no need to do anything!)
|
// The only remaining value is successful (no need to do anything!)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}, minecraftConnection.eventLoop())
|
}, connection.eventLoop())
|
||||||
.thenApply(Result::isSuccessful);
|
.thenApply(Result::isSuccessful);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ public class InitialConnectSessionHandler implements MinecraftSessionHandler {
|
|||||||
} else if (PluginMessageUtil.isUnregister(packet)) {
|
} else if (PluginMessageUtil.isUnregister(packet)) {
|
||||||
player.getKnownChannels().removeAll(PluginMessageUtil.getChannels(packet));
|
player.getKnownChannels().removeAll(PluginMessageUtil.getChannels(packet));
|
||||||
}
|
}
|
||||||
serverConn.ensureConnected().write(packet);
|
serverConn.ensureConnected().write(packet.retain());
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -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.connection.VelocityConstants.VELOCITY_IP_FORWARDING_CHANNEL;
|
||||||
import static com.velocitypowered.proxy.util.EncryptionUtils.decryptRsa;
|
import static com.velocitypowered.proxy.util.EncryptionUtils.decryptRsa;
|
||||||
import static com.velocitypowered.proxy.util.EncryptionUtils.generateServerId;
|
import static com.velocitypowered.proxy.util.EncryptionUtils.generateServerId;
|
||||||
|
import static org.asynchttpclient.Dsl.asyncHttpClient;
|
||||||
|
import static org.asynchttpclient.Dsl.config;
|
||||||
|
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
import com.google.common.net.UrlEscapers;
|
import com.google.common.net.UrlEscapers;
|
||||||
@ -42,10 +44,14 @@ import java.security.KeyPair;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.ThreadLocalRandom;
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
import net.kyori.text.Component;
|
import net.kyori.text.Component;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.asynchttpclient.Dsl;
|
||||||
|
import org.asynchttpclient.ListenableFuture;
|
||||||
|
import org.asynchttpclient.Response;
|
||||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
|
|
||||||
public class LoginSessionHandler implements MinecraftSessionHandler {
|
public class LoginSessionHandler implements MinecraftSessionHandler {
|
||||||
@ -123,46 +129,50 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
|||||||
String url = String.format(MOJANG_HASJOINED_URL,
|
String url = String.format(MOJANG_HASJOINED_URL,
|
||||||
urlFormParameterEscaper().escape(login.getUsername()), serverId,
|
urlFormParameterEscaper().escape(login.getUsername()), serverId,
|
||||||
urlFormParameterEscaper().escape(playerIp));
|
urlFormParameterEscaper().escape(playerIp));
|
||||||
server.getHttpClient()
|
|
||||||
.get(new URL(url), mcConnection.eventLoop())
|
|
||||||
.thenAcceptAsync(profileResponse -> {
|
|
||||||
if (mcConnection.isClosed()) {
|
|
||||||
// The player disconnected after we authenticated them.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Go ahead and enable encryption. Once the client sends EncryptionResponse, encryption
|
ListenableFuture<Response> hasJoinedResponse = server.getAsyncHttpClient().prepareGet(url)
|
||||||
// is enabled.
|
.execute();
|
||||||
try {
|
hasJoinedResponse.addListener(() -> {
|
||||||
mcConnection.enableEncryption(decryptedSharedSecret);
|
if (mcConnection.isClosed()) {
|
||||||
} catch (GeneralSecurityException e) {
|
// The player disconnected after we authenticated them.
|
||||||
throw new RuntimeException(e);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (profileResponse.getCode() == 200) {
|
// Go ahead and enable encryption. Once the client sends EncryptionResponse, encryption
|
||||||
// All went well, initialize the session.
|
// is enabled.
|
||||||
initializePlayer(GSON.fromJson(profileResponse.getBody(), GameProfile.class), true);
|
try {
|
||||||
} else if (profileResponse.getCode() == 204) {
|
mcConnection.enableEncryption(decryptedSharedSecret);
|
||||||
// Apparently an offline-mode user logged onto this online-mode proxy.
|
} catch (GeneralSecurityException e) {
|
||||||
inbound.disconnect(VelocityMessages.ONLINE_MODE_ONLY);
|
throw new RuntimeException(e);
|
||||||
} else {
|
}
|
||||||
// Something else went wrong
|
|
||||||
logger.error(
|
try {
|
||||||
"Got an unexpected error code {} whilst contacting Mojang to log in {} ({})",
|
Response profileResponse = hasJoinedResponse.get();
|
||||||
profileResponse.getCode(), login.getUsername(), playerIp);
|
if (profileResponse.getStatusCode() == 200) {
|
||||||
mcConnection.close();
|
// All went well, initialize the session.
|
||||||
}
|
initializePlayer(GSON.fromJson(profileResponse.getResponseBody(), GameProfile.class),
|
||||||
}, mcConnection.eventLoop())
|
true);
|
||||||
.exceptionally(exception -> {
|
} else if (profileResponse.getStatusCode() == 204) {
|
||||||
logger.error("Unable to enable encryption", exception);
|
// Apparently an offline-mode user logged onto this online-mode proxy.
|
||||||
|
inbound.disconnect(VelocityMessages.ONLINE_MODE_ONLY);
|
||||||
|
} else {
|
||||||
|
// Something else went wrong
|
||||||
|
logger.error(
|
||||||
|
"Got an unexpected error code {} whilst contacting Mojang to log in {} ({})",
|
||||||
|
profileResponse.getStatusCode(), login.getUsername(), playerIp);
|
||||||
mcConnection.close();
|
mcConnection.close();
|
||||||
return null;
|
}
|
||||||
});
|
} catch (ExecutionException e) {
|
||||||
|
logger.error("Unable to authenticate with Mojang", e);
|
||||||
|
mcConnection.close();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// not much we can do usefully
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}, mcConnection.eventLoop());
|
||||||
} catch (GeneralSecurityException e) {
|
} catch (GeneralSecurityException e) {
|
||||||
logger.error("Unable to enable encryption", e);
|
logger.error("Unable to enable encryption", e);
|
||||||
mcConnection.close();
|
mcConnection.close();
|
||||||
} catch (MalformedURLException e) {
|
|
||||||
throw new AssertionError(e);
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -179,6 +189,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
|||||||
// The player was disconnected
|
// The player was disconnected
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
PreLoginComponentResult result = event.getResult();
|
PreLoginComponentResult result = event.getResult();
|
||||||
Optional<Component> disconnectReason = result.getReason();
|
Optional<Component> disconnectReason = result.getReason();
|
||||||
if (disconnectReason.isPresent()) {
|
if (disconnectReason.isPresent()) {
|
||||||
@ -277,7 +288,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
|||||||
player.disconnect(VelocityMessages.ALREADY_CONNECTED);
|
player.disconnect(VelocityMessages.ALREADY_CONNECTED);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
mcConnection.setSessionHandler(new InitialConnectSessionHandler(player));
|
mcConnection.setSessionHandler(new InitialConnectSessionHandler(player));
|
||||||
server.getEventManager().fire(new PostLoginEvent(player))
|
server.getEventManager().fire(new PostLoginEvent(player))
|
||||||
.thenRun(() -> player.createConnectionRequest(toTry.get()).fireAndForget());
|
.thenRun(() -> player.createConnectionRequest(toTry.get()).fireAndForget());
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
package com.velocitypowered.proxy.connection.client;
|
package com.velocitypowered.proxy.connection.client;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.spotify.futures.CompletableFutures;
|
||||||
import com.velocitypowered.api.event.proxy.ProxyPingEvent;
|
import com.velocitypowered.api.event.proxy.ProxyPingEvent;
|
||||||
import com.velocitypowered.api.network.ProtocolVersion;
|
import com.velocitypowered.api.network.ProtocolVersion;
|
||||||
import com.velocitypowered.api.proxy.InboundConnection;
|
import com.velocitypowered.api.proxy.InboundConnection;
|
||||||
|
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||||
import com.velocitypowered.api.proxy.server.ServerPing;
|
import com.velocitypowered.api.proxy.server.ServerPing;
|
||||||
import com.velocitypowered.api.util.ModInfo;
|
import com.velocitypowered.api.util.ModInfo;
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
import com.velocitypowered.proxy.VelocityServer;
|
||||||
|
import com.velocitypowered.proxy.config.PingPassthroughMode;
|
||||||
import com.velocitypowered.proxy.config.VelocityConfiguration;
|
import com.velocitypowered.proxy.config.VelocityConfiguration;
|
||||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||||
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
||||||
@ -15,27 +18,32 @@ import com.velocitypowered.proxy.protocol.packet.LegacyPing;
|
|||||||
import com.velocitypowered.proxy.protocol.packet.StatusPing;
|
import com.velocitypowered.proxy.protocol.packet.StatusPing;
|
||||||
import com.velocitypowered.proxy.protocol.packet.StatusRequest;
|
import com.velocitypowered.proxy.protocol.packet.StatusRequest;
|
||||||
import com.velocitypowered.proxy.protocol.packet.StatusResponse;
|
import com.velocitypowered.proxy.protocol.packet.StatusResponse;
|
||||||
|
import com.velocitypowered.proxy.server.VelocityRegisteredServer;
|
||||||
import io.netty.buffer.ByteBuf;
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
public class StatusSessionHandler implements MinecraftSessionHandler {
|
public class StatusSessionHandler implements MinecraftSessionHandler {
|
||||||
|
|
||||||
private final VelocityServer server;
|
private final VelocityServer server;
|
||||||
private final MinecraftConnection connection;
|
private final MinecraftConnection connection;
|
||||||
private final InboundConnection inboundWrapper;
|
private final InboundConnection inbound;
|
||||||
|
|
||||||
StatusSessionHandler(VelocityServer server, MinecraftConnection connection,
|
StatusSessionHandler(VelocityServer server, MinecraftConnection connection,
|
||||||
InboundConnection inboundWrapper) {
|
InboundConnection inbound) {
|
||||||
this.server = server;
|
this.server = server;
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
this.inboundWrapper = inboundWrapper;
|
this.inbound = inbound;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ServerPing createInitialPing() {
|
private ServerPing constructLocalPing(ProtocolVersion version) {
|
||||||
VelocityConfiguration configuration = server.getConfiguration();
|
VelocityConfiguration configuration = server.getConfiguration();
|
||||||
ProtocolVersion shownVersion = ProtocolVersion.isSupported(connection.getProtocolVersion())
|
|
||||||
? connection.getProtocolVersion() : ProtocolVersion.MAXIMUM_VERSION;
|
|
||||||
return new ServerPing(
|
return new ServerPing(
|
||||||
new ServerPing.Version(shownVersion.getProtocol(),
|
new ServerPing.Version(version.getProtocol(),
|
||||||
"Velocity " + ProtocolVersion.SUPPORTED_VERSION_STRING),
|
"Velocity " + ProtocolVersion.SUPPORTED_VERSION_STRING),
|
||||||
new ServerPing.Players(server.getPlayerCount(), configuration.getShowMaxPlayers(),
|
new ServerPing.Players(server.getPlayerCount(), configuration.getShowMaxPlayers(),
|
||||||
ImmutableList.of()),
|
ImmutableList.of()),
|
||||||
@ -45,12 +53,78 @@ public class StatusSessionHandler implements MinecraftSessionHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private CompletableFuture<ServerPing> attemptPingPassthrough(PingPassthroughMode mode,
|
||||||
|
List<String> servers, ProtocolVersion pingingVersion) {
|
||||||
|
ServerPing fallback = constructLocalPing(pingingVersion);
|
||||||
|
List<CompletableFuture<ServerPing>> pings = new ArrayList<>();
|
||||||
|
for (String s : servers) {
|
||||||
|
Optional<RegisteredServer> rs = server.getServer(s);
|
||||||
|
if (!rs.isPresent()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
VelocityRegisteredServer vrs = (VelocityRegisteredServer) rs.get();
|
||||||
|
pings.add(vrs.ping(connection.eventLoop(), pingingVersion));
|
||||||
|
}
|
||||||
|
if (pings.isEmpty()) {
|
||||||
|
return CompletableFuture.completedFuture(fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
CompletableFuture<List<ServerPing>> pingResponses = CompletableFutures.successfulAsList(pings,
|
||||||
|
(ex) -> fallback);
|
||||||
|
switch (mode) {
|
||||||
|
case ALL:
|
||||||
|
return pingResponses.thenApply(responses -> {
|
||||||
|
// Find the first non-fallback
|
||||||
|
for (ServerPing response : responses) {
|
||||||
|
if (response == fallback) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
});
|
||||||
|
case MODS:
|
||||||
|
return pingResponses.thenApply(responses -> {
|
||||||
|
// Find the first non-fallback that contains a mod list
|
||||||
|
for (ServerPing response : responses) {
|
||||||
|
if (response == fallback) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Optional<ModInfo> modInfo = response.getModinfo();
|
||||||
|
if (modInfo.isPresent()) {
|
||||||
|
return fallback.asBuilder().mods(modInfo.get()).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
// Not possible, but covered for completeness.
|
||||||
|
return CompletableFuture.completedFuture(fallback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private CompletableFuture<ServerPing> getInitialPing() {
|
||||||
|
VelocityConfiguration configuration = server.getConfiguration();
|
||||||
|
ProtocolVersion shownVersion = ProtocolVersion.isSupported(connection.getProtocolVersion())
|
||||||
|
? connection.getProtocolVersion() : ProtocolVersion.MAXIMUM_VERSION;
|
||||||
|
PingPassthroughMode passthrough = configuration.getPingPassthrough();
|
||||||
|
|
||||||
|
if (passthrough == PingPassthroughMode.DISABLED) {
|
||||||
|
return CompletableFuture.completedFuture(constructLocalPing(shownVersion));
|
||||||
|
} else {
|
||||||
|
String virtualHostStr = inbound.getVirtualHost().map(InetSocketAddress::getHostString)
|
||||||
|
.orElse("");
|
||||||
|
List<String> serversToTry = server.getConfiguration().getForcedHosts().getOrDefault(
|
||||||
|
virtualHostStr, server.getConfiguration().getAttemptConnectionOrder());
|
||||||
|
return attemptPingPassthrough(configuration.getPingPassthrough(), serversToTry, shownVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean handle(LegacyPing packet) {
|
public boolean handle(LegacyPing packet) {
|
||||||
ServerPing initialPing = createInitialPing();
|
getInitialPing()
|
||||||
ProxyPingEvent event = new ProxyPingEvent(inboundWrapper, initialPing);
|
.thenCompose(ping -> server.getEventManager().fire(new ProxyPingEvent(inbound, ping)))
|
||||||
server.getEventManager().fire(event)
|
.thenAcceptAsync(event -> {
|
||||||
.thenRunAsync(() -> {
|
|
||||||
connection.closeWith(LegacyDisconnect.fromServerPing(event.getPing(),
|
connection.closeWith(LegacyDisconnect.fromServerPing(event.getPing(),
|
||||||
packet.getVersion()));
|
packet.getVersion()));
|
||||||
}, connection.eventLoop());
|
}, connection.eventLoop());
|
||||||
@ -65,11 +139,10 @@ public class StatusSessionHandler implements MinecraftSessionHandler {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean handle(StatusRequest packet) {
|
public boolean handle(StatusRequest packet) {
|
||||||
ServerPing initialPing = createInitialPing();
|
getInitialPing()
|
||||||
ProxyPingEvent event = new ProxyPingEvent(inboundWrapper, initialPing);
|
.thenCompose(ping -> server.getEventManager().fire(new ProxyPingEvent(inbound, ping)))
|
||||||
server.getEventManager().fire(event)
|
.thenAcceptAsync(
|
||||||
.thenRunAsync(
|
(event) -> {
|
||||||
() -> {
|
|
||||||
StringBuilder json = new StringBuilder();
|
StringBuilder json = new StringBuilder();
|
||||||
VelocityServer.GSON.toJson(event.getPing(), json);
|
VelocityServer.GSON.toJson(event.getPing(), json);
|
||||||
connection.write(new StatusResponse(json));
|
connection.write(new StatusResponse(json));
|
||||||
|
@ -114,7 +114,7 @@ public enum LegacyForgeHandshakeBackendPhase implements BackendConnectionPhase {
|
|||||||
serverConnection.setConnectionPhase(newPhase);
|
serverConnection.setConnectionPhase(newPhase);
|
||||||
|
|
||||||
// Write the packet to the player, we don't need it now.
|
// Write the packet to the player, we don't need it now.
|
||||||
player.getMinecraftConnection().write(message);
|
player.getConnection().write(message.retain());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,7 +135,7 @@ public enum LegacyForgeHandshakeClientPhase implements ClientConnectionPhase {
|
|||||||
COMPLETE(null) {
|
COMPLETE(null) {
|
||||||
@Override
|
@Override
|
||||||
public void resetConnectionPhase(ConnectedPlayer player) {
|
public void resetConnectionPhase(ConnectedPlayer player) {
|
||||||
player.getMinecraftConnection().write(LegacyForgeUtil.resetPacket());
|
player.getConnection().write(LegacyForgeUtil.resetPacket());
|
||||||
player.setPhase(LegacyForgeHandshakeClientPhase.NOT_STARTED);
|
player.setPhase(LegacyForgeHandshakeClientPhase.NOT_STARTED);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,7 +212,7 @@ public enum LegacyForgeHandshakeClientPhase implements ClientConnectionPhase {
|
|||||||
PluginMessage message,
|
PluginMessage message,
|
||||||
MinecraftConnection backendConn) {
|
MinecraftConnection backendConn) {
|
||||||
// Send the packet on to the server.
|
// Send the packet on to the server.
|
||||||
backendConn.write(message);
|
backendConn.write(message.retain());
|
||||||
|
|
||||||
// We handled the packet. No need to continue processing.
|
// We handled the packet. No need to continue processing.
|
||||||
return true;
|
return true;
|
||||||
|
@ -27,9 +27,8 @@ class LegacyForgeUtil {
|
|||||||
*/
|
*/
|
||||||
static byte getHandshakePacketDiscriminator(PluginMessage message) {
|
static byte getHandshakePacketDiscriminator(PluginMessage message) {
|
||||||
Preconditions.checkArgument(message.getChannel().equals(FORGE_LEGACY_HANDSHAKE_CHANNEL));
|
Preconditions.checkArgument(message.getChannel().equals(FORGE_LEGACY_HANDSHAKE_CHANNEL));
|
||||||
byte[] data = message.getData();
|
Preconditions.checkArgument(message.content().isReadable());
|
||||||
Preconditions.checkArgument(data.length >= 1);
|
return message.content().getByte(0);
|
||||||
return data[0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -44,7 +43,7 @@ class LegacyForgeUtil {
|
|||||||
.checkArgument(message.getChannel().equals(FORGE_LEGACY_HANDSHAKE_CHANNEL),
|
.checkArgument(message.getChannel().equals(FORGE_LEGACY_HANDSHAKE_CHANNEL),
|
||||||
"message is not a FML HS plugin message");
|
"message is not a FML HS plugin message");
|
||||||
|
|
||||||
ByteBuf byteBuf = Unpooled.wrappedBuffer(message.getData());
|
ByteBuf byteBuf = message.content().retainedSlice();
|
||||||
try {
|
try {
|
||||||
byte discriminator = byteBuf.readByte();
|
byte discriminator = byteBuf.readByte();
|
||||||
|
|
||||||
@ -75,7 +74,7 @@ class LegacyForgeUtil {
|
|||||||
static PluginMessage resetPacket() {
|
static PluginMessage resetPacket() {
|
||||||
PluginMessage msg = new PluginMessage();
|
PluginMessage msg = new PluginMessage();
|
||||||
msg.setChannel(FORGE_LEGACY_HANDSHAKE_CHANNEL);
|
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;
|
return msg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
package com.velocitypowered.proxy.network;
|
package com.velocitypowered.proxy.network;
|
||||||
|
|
||||||
|
import static org.asynchttpclient.Dsl.asyncHttpClient;
|
||||||
|
import static org.asynchttpclient.Dsl.config;
|
||||||
|
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
import com.velocitypowered.natives.util.Natives;
|
import com.velocitypowered.natives.util.Natives;
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
import com.velocitypowered.proxy.VelocityServer;
|
||||||
|
import com.velocitypowered.proxy.network.netty.DnsAddressResolverGroupNameResolverAdapter;
|
||||||
import com.velocitypowered.proxy.protocol.netty.GS4QueryHandler;
|
import com.velocitypowered.proxy.protocol.netty.GS4QueryHandler;
|
||||||
import io.netty.bootstrap.Bootstrap;
|
import io.netty.bootstrap.Bootstrap;
|
||||||
import io.netty.bootstrap.ServerBootstrap;
|
import io.netty.bootstrap.ServerBootstrap;
|
||||||
@ -11,17 +15,26 @@ import io.netty.channel.ChannelFutureListener;
|
|||||||
import io.netty.channel.ChannelOption;
|
import io.netty.channel.ChannelOption;
|
||||||
import io.netty.channel.EventLoopGroup;
|
import io.netty.channel.EventLoopGroup;
|
||||||
import io.netty.channel.WriteBufferWaterMark;
|
import io.netty.channel.WriteBufferWaterMark;
|
||||||
|
import io.netty.channel.epoll.EpollChannelOption;
|
||||||
import io.netty.resolver.dns.DnsAddressResolverGroup;
|
import io.netty.resolver.dns.DnsAddressResolverGroup;
|
||||||
import io.netty.resolver.dns.DnsNameResolverBuilder;
|
import io.netty.resolver.dns.DnsNameResolverBuilder;
|
||||||
|
import io.netty.util.concurrent.EventExecutor;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.asynchttpclient.AsyncHttpClient;
|
||||||
|
import org.asynchttpclient.RequestBuilder;
|
||||||
|
import org.asynchttpclient.filter.FilterContext;
|
||||||
|
import org.asynchttpclient.filter.FilterContext.FilterContextBuilder;
|
||||||
|
import org.asynchttpclient.filter.FilterException;
|
||||||
|
import org.asynchttpclient.filter.RequestFilter;
|
||||||
|
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||||
|
|
||||||
public final class ConnectionManager {
|
public final class ConnectionManager {
|
||||||
|
|
||||||
private static final WriteBufferWaterMark SERVER_WRITE_MARK = new WriteBufferWaterMark(1 << 21,
|
private static final WriteBufferWaterMark SERVER_WRITE_MARK = new WriteBufferWaterMark(1 << 20,
|
||||||
1 << 21);
|
1 << 21);
|
||||||
private static final Logger LOGGER = LogManager.getLogger(ConnectionManager.class);
|
private static final Logger LOGGER = LogManager.getLogger(ConnectionManager.class);
|
||||||
private final Map<InetSocketAddress, Channel> endpoints = new HashMap<>();
|
private final Map<InetSocketAddress, Channel> endpoints = new HashMap<>();
|
||||||
@ -35,6 +48,7 @@ public final class ConnectionManager {
|
|||||||
public final ServerChannelInitializerHolder serverChannelInitializer;
|
public final ServerChannelInitializerHolder serverChannelInitializer;
|
||||||
|
|
||||||
private final DnsAddressResolverGroup resolverGroup;
|
private final DnsAddressResolverGroup resolverGroup;
|
||||||
|
private final AsyncHttpClient httpClient;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initalizes the {@code ConnectionManager}.
|
* Initalizes the {@code ConnectionManager}.
|
||||||
@ -48,12 +62,26 @@ public final class ConnectionManager {
|
|||||||
this.workerGroup = this.transportType.createEventLoopGroup(TransportType.Type.WORKER);
|
this.workerGroup = this.transportType.createEventLoopGroup(TransportType.Type.WORKER);
|
||||||
this.serverChannelInitializer = new ServerChannelInitializerHolder(
|
this.serverChannelInitializer = new ServerChannelInitializerHolder(
|
||||||
new ServerChannelInitializer(this.server));
|
new ServerChannelInitializer(this.server));
|
||||||
this.resolverGroup = new DnsAddressResolverGroup(
|
this.resolverGroup = new DnsAddressResolverGroup(new DnsNameResolverBuilder()
|
||||||
new DnsNameResolverBuilder()
|
.channelType(this.transportType.datagramChannelClass)
|
||||||
.channelType(this.transportType.datagramChannelClass)
|
.negativeTtl(15)
|
||||||
.negativeTtl(15)
|
.ndots(1));
|
||||||
.ndots(1)
|
this.httpClient = asyncHttpClient(config()
|
||||||
);
|
.setEventLoopGroup(this.workerGroup)
|
||||||
|
.setUserAgent(server.getVersion().getName() + "/" + server.getVersion().getVersion())
|
||||||
|
.addRequestFilter(new RequestFilter() {
|
||||||
|
@Override
|
||||||
|
public <T> FilterContext<T> filter(FilterContext<T> ctx) throws FilterException {
|
||||||
|
return new FilterContextBuilder<>(ctx)
|
||||||
|
.request(new RequestBuilder(ctx.getRequest())
|
||||||
|
.setNameResolver(
|
||||||
|
new DnsAddressResolverGroupNameResolverAdapter(resolverGroup, workerGroup)
|
||||||
|
)
|
||||||
|
.build())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void logChannelInformation() {
|
public void logChannelInformation() {
|
||||||
@ -75,6 +103,11 @@ public final class ConnectionManager {
|
|||||||
.childOption(ChannelOption.TCP_NODELAY, true)
|
.childOption(ChannelOption.TCP_NODELAY, true)
|
||||||
.childOption(ChannelOption.IP_TOS, 0x18)
|
.childOption(ChannelOption.IP_TOS, 0x18)
|
||||||
.localAddress(address);
|
.localAddress(address);
|
||||||
|
|
||||||
|
if (transportType == TransportType.EPOLL && server.getConfiguration().useTcpFastOpen()) {
|
||||||
|
bootstrap.option(EpollChannelOption.TCP_FASTOPEN, 3);
|
||||||
|
}
|
||||||
|
|
||||||
bootstrap.bind()
|
bootstrap.bind()
|
||||||
.addListener((ChannelFutureListener) future -> {
|
.addListener((ChannelFutureListener) future -> {
|
||||||
final Channel channel = future.channel();
|
final Channel channel = future.channel();
|
||||||
@ -112,25 +145,25 @@ public final class ConnectionManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public Bootstrap createWorker() {
|
|
||||||
return this.createWorker(this.workerGroup);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a TCP {@link Bootstrap} using Velocity's event loops.
|
* Creates a TCP {@link Bootstrap} using Velocity's event loops.
|
||||||
*
|
*
|
||||||
* @param group the event loop group to use
|
* @param group the event loop group to use. Use {@code null} for the default worker group.
|
||||||
*
|
*
|
||||||
* @return a new {@link Bootstrap}
|
* @return a new {@link Bootstrap}
|
||||||
*/
|
*/
|
||||||
public Bootstrap createWorker(EventLoopGroup group) {
|
public Bootstrap createWorker(@Nullable EventLoopGroup group) {
|
||||||
return new Bootstrap()
|
Bootstrap bootstrap = new Bootstrap()
|
||||||
.channel(this.transportType.socketChannelClass)
|
.channel(this.transportType.socketChannelClass)
|
||||||
.group(group)
|
|
||||||
.option(ChannelOption.TCP_NODELAY, true)
|
.option(ChannelOption.TCP_NODELAY, true)
|
||||||
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS,
|
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS,
|
||||||
this.server.getConfiguration().getConnectTimeout())
|
this.server.getConfiguration().getConnectTimeout())
|
||||||
|
.group(group == null ? this.workerGroup : group)
|
||||||
.resolver(this.resolverGroup);
|
.resolver(this.resolverGroup);
|
||||||
|
if (transportType == TransportType.EPOLL && server.getConfiguration().useTcpFastOpen()) {
|
||||||
|
bootstrap.option(EpollChannelOption.TCP_FASTOPEN_CONNECT, true);
|
||||||
|
}
|
||||||
|
return bootstrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -164,11 +197,11 @@ public final class ConnectionManager {
|
|||||||
return bossGroup;
|
return bossGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
public EventLoopGroup getWorkerGroup() {
|
|
||||||
return workerGroup;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ServerChannelInitializerHolder getServerChannelInitializer() {
|
public ServerChannelInitializerHolder getServerChannelInitializer() {
|
||||||
return this.serverChannelInitializer;
|
return this.serverChannelInitializer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AsyncHttpClient getHttpClient() {
|
||||||
|
return httpClient;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,12 @@
|
|||||||
package com.velocitypowered.proxy.network;
|
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.EventLoopGroup;
|
||||||
import io.netty.channel.epoll.Epoll;
|
import io.netty.channel.epoll.Epoll;
|
||||||
import io.netty.channel.epoll.EpollDatagramChannel;
|
import io.netty.channel.epoll.EpollDatagramChannel;
|
||||||
import io.netty.channel.epoll.EpollEventLoopGroup;
|
import io.netty.channel.epoll.EpollEventLoopGroup;
|
||||||
import io.netty.channel.epoll.EpollServerSocketChannel;
|
import io.netty.channel.epoll.EpollServerSocketChannel;
|
||||||
import io.netty.channel.epoll.EpollSocketChannel;
|
import io.netty.channel.epoll.EpollSocketChannel;
|
||||||
import io.netty.channel.kqueue.KQueue;
|
|
||||||
import io.netty.channel.kqueue.KQueueDatagramChannel;
|
|
||||||
import io.netty.channel.kqueue.KQueueEventLoopGroup;
|
|
||||||
import io.netty.channel.kqueue.KQueueServerSocketChannel;
|
|
||||||
import io.netty.channel.kqueue.KQueueSocketChannel;
|
|
||||||
import io.netty.channel.nio.NioEventLoopGroup;
|
import io.netty.channel.nio.NioEventLoopGroup;
|
||||||
import io.netty.channel.socket.DatagramChannel;
|
import io.netty.channel.socket.DatagramChannel;
|
||||||
import io.netty.channel.socket.ServerSocketChannel;
|
import io.netty.channel.socket.ServerSocketChannel;
|
||||||
@ -27,10 +22,7 @@ enum TransportType {
|
|||||||
(name, type) -> new NioEventLoopGroup(0, createThreadFactory(name, type))),
|
(name, type) -> new NioEventLoopGroup(0, createThreadFactory(name, type))),
|
||||||
EPOLL("epoll", EpollServerSocketChannel.class, EpollSocketChannel.class,
|
EPOLL("epoll", EpollServerSocketChannel.class, EpollSocketChannel.class,
|
||||||
EpollDatagramChannel.class,
|
EpollDatagramChannel.class,
|
||||||
(name, type) -> new EpollEventLoopGroup(0, createThreadFactory(name, type))),
|
(name, type) -> new EpollEventLoopGroup(0, createThreadFactory(name, type)));
|
||||||
KQUEUE("Kqueue", KQueueServerSocketChannel.class, KQueueSocketChannel.class,
|
|
||||||
KQueueDatagramChannel.class,
|
|
||||||
(name, type) -> new KQueueEventLoopGroup(0, createThreadFactory(name, type)));
|
|
||||||
|
|
||||||
final String name;
|
final String name;
|
||||||
final Class<? extends ServerSocketChannel> serverSocketChannelClass;
|
final Class<? extends ServerSocketChannel> serverSocketChannelClass;
|
||||||
@ -60,17 +52,16 @@ enum TransportType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static ThreadFactory createThreadFactory(final String name, final Type type) {
|
private static ThreadFactory createThreadFactory(final String name, final Type type) {
|
||||||
return new ThreadFactoryBuilder()
|
return new VelocityNettyThreadFactory("Netty " + name + ' ' + type.toString() + " #%d");
|
||||||
.setNameFormat("Netty " + name + ' ' + type.toString() + " #%d")
|
|
||||||
.setDaemon(true)
|
|
||||||
.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static TransportType bestType() {
|
public static TransportType bestType() {
|
||||||
|
if (Boolean.getBoolean("velocity.disable-native-transport")) {
|
||||||
|
return NIO;
|
||||||
|
}
|
||||||
|
|
||||||
if (Epoll.isAvailable()) {
|
if (Epoll.isAvailable()) {
|
||||||
return EPOLL;
|
return EPOLL;
|
||||||
} else if (KQueue.isAvailable()) {
|
|
||||||
return KQUEUE;
|
|
||||||
} else {
|
} else {
|
||||||
return NIO;
|
return NIO;
|
||||||
}
|
}
|
||||||
|
@ -1,153 +0,0 @@
|
|||||||
package com.velocitypowered.proxy.network.http;
|
|
||||||
|
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
|
||||||
import io.netty.buffer.ByteBuf;
|
|
||||||
import io.netty.channel.Channel;
|
|
||||||
import io.netty.channel.ChannelFuture;
|
|
||||||
import io.netty.channel.ChannelFutureListener;
|
|
||||||
import io.netty.channel.ChannelInitializer;
|
|
||||||
import io.netty.channel.EventLoop;
|
|
||||||
import io.netty.handler.codec.http.DefaultFullHttpRequest;
|
|
||||||
import io.netty.handler.codec.http.HttpClientCodec;
|
|
||||||
import io.netty.handler.codec.http.HttpContentCompressor;
|
|
||||||
import io.netty.handler.codec.http.HttpHeaderNames;
|
|
||||||
import io.netty.handler.codec.http.HttpMethod;
|
|
||||||
import io.netty.handler.codec.http.HttpRequest;
|
|
||||||
import io.netty.handler.codec.http.HttpVersion;
|
|
||||||
import io.netty.handler.ssl.SslContext;
|
|
||||||
import io.netty.handler.ssl.SslContextBuilder;
|
|
||||||
import io.netty.handler.ssl.SslHandler;
|
|
||||||
import java.net.InetSocketAddress;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
import javax.net.ssl.SSLEngine;
|
|
||||||
|
|
||||||
public class NettyHttpClient {
|
|
||||||
|
|
||||||
private final String userAgent;
|
|
||||||
private final VelocityServer server;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the HTTP client.
|
|
||||||
*
|
|
||||||
* @param server the Velocity server
|
|
||||||
*/
|
|
||||||
public NettyHttpClient(VelocityServer server) {
|
|
||||||
this.userAgent = server.getVersion().getName() + "/" + server.getVersion().getVersion();
|
|
||||||
this.server = server;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ChannelFuture establishConnection(URL url, EventLoop loop) {
|
|
||||||
String host = url.getHost();
|
|
||||||
int port = url.getPort();
|
|
||||||
boolean ssl = url.getProtocol().equals("https");
|
|
||||||
if (port == -1) {
|
|
||||||
port = ssl ? 443 : 80;
|
|
||||||
}
|
|
||||||
|
|
||||||
InetSocketAddress address = InetSocketAddress.createUnresolved(host, port);
|
|
||||||
return server.initializeGenericBootstrap(loop)
|
|
||||||
.handler(new ChannelInitializer<Channel>() {
|
|
||||||
@Override
|
|
||||||
protected void initChannel(Channel ch) throws Exception {
|
|
||||||
if (ssl) {
|
|
||||||
SslContext context = SslContextBuilder.forClient().protocols("TLSv1.2").build();
|
|
||||||
// Unbelievably, Java doesn't automatically check the CN to make sure we're talking
|
|
||||||
// to the right host! Therefore, we provide the intended host name and port, along
|
|
||||||
// with asking Java very nicely if it could check the hostname in the certificate
|
|
||||||
// for us.
|
|
||||||
SSLEngine engine = context.newEngine(ch.alloc(), address.getHostString(),
|
|
||||||
address.getPort());
|
|
||||||
engine.getSSLParameters().setEndpointIdentificationAlgorithm("HTTPS");
|
|
||||||
ch.pipeline().addLast("ssl", new SslHandler(engine));
|
|
||||||
}
|
|
||||||
ch.pipeline().addLast("http", new HttpClientCodec());
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.connect(address);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempts an HTTP GET request to the specified URL.
|
|
||||||
* @param url the URL to fetch
|
|
||||||
* @param loop the event loop to use
|
|
||||||
* @return a future representing the response
|
|
||||||
*/
|
|
||||||
public CompletableFuture<SimpleHttpResponse> get(URL url, EventLoop loop) {
|
|
||||||
CompletableFuture<SimpleHttpResponse> reply = new CompletableFuture<>();
|
|
||||||
establishConnection(url, loop)
|
|
||||||
.addListener((ChannelFutureListener) future -> {
|
|
||||||
if (future.isSuccess()) {
|
|
||||||
Channel channel = future.channel();
|
|
||||||
|
|
||||||
channel.pipeline().addLast("collector", new SimpleHttpResponseCollector(reply));
|
|
||||||
|
|
||||||
String pathAndQuery = url.getPath();
|
|
||||||
if (url.getQuery() != null && url.getQuery().length() > 0) {
|
|
||||||
pathAndQuery += "?" + url.getQuery();
|
|
||||||
}
|
|
||||||
|
|
||||||
DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1,
|
|
||||||
HttpMethod.GET, pathAndQuery);
|
|
||||||
request.headers().add(HttpHeaderNames.HOST, url.getHost());
|
|
||||||
request.headers().add(HttpHeaderNames.USER_AGENT, userAgent);
|
|
||||||
channel.writeAndFlush(request, channel.voidPromise());
|
|
||||||
} else {
|
|
||||||
reply.completeExceptionally(future.cause());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return reply;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempts an HTTP POST request to the specified URL.
|
|
||||||
* @param url the URL to fetch
|
|
||||||
* @param body the body to post
|
|
||||||
* @param decorator a consumer that can modify the request as required
|
|
||||||
* @return a future representing the response
|
|
||||||
*/
|
|
||||||
public CompletableFuture<SimpleHttpResponse> post(URL url, ByteBuf body,
|
|
||||||
Consumer<HttpRequest> decorator) {
|
|
||||||
return post(url, server.getWorkerGroup().next(), body, decorator);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempts an HTTP POST request to the specified URL.
|
|
||||||
* @param url the URL to fetch
|
|
||||||
* @param loop the event loop to use
|
|
||||||
* @param body the body to post
|
|
||||||
* @param decorator a consumer that can modify the request as required
|
|
||||||
* @return a future representing the response
|
|
||||||
*/
|
|
||||||
public CompletableFuture<SimpleHttpResponse> post(URL url, EventLoop loop, ByteBuf body,
|
|
||||||
Consumer<HttpRequest> decorator) {
|
|
||||||
CompletableFuture<SimpleHttpResponse> reply = new CompletableFuture<>();
|
|
||||||
establishConnection(url, loop)
|
|
||||||
.addListener((ChannelFutureListener) future -> {
|
|
||||||
if (future.isSuccess()) {
|
|
||||||
Channel channel = future.channel();
|
|
||||||
|
|
||||||
channel.pipeline().addLast("collector", new SimpleHttpResponseCollector(reply));
|
|
||||||
|
|
||||||
String pathAndQuery = url.getPath();
|
|
||||||
if (url.getQuery() != null && url.getQuery().length() > 0) {
|
|
||||||
pathAndQuery += "?" + url.getQuery();
|
|
||||||
}
|
|
||||||
|
|
||||||
DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1,
|
|
||||||
HttpMethod.POST, pathAndQuery, body);
|
|
||||||
request.headers().add(HttpHeaderNames.HOST, url.getHost());
|
|
||||||
request.headers().add(HttpHeaderNames.USER_AGENT, userAgent);
|
|
||||||
request.headers().add(HttpHeaderNames.CONTENT_LENGTH, body.readableBytes());
|
|
||||||
decorator.accept(request);
|
|
||||||
|
|
||||||
channel.writeAndFlush(request, channel.voidPromise());
|
|
||||||
} else {
|
|
||||||
body.release();
|
|
||||||
reply.completeExceptionally(future.cause());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return reply;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
package com.velocitypowered.proxy.network.http;
|
|
||||||
|
|
||||||
public class SimpleHttpResponse {
|
|
||||||
|
|
||||||
private final int code;
|
|
||||||
private final String body;
|
|
||||||
|
|
||||||
SimpleHttpResponse(int code, String body) {
|
|
||||||
this.code = code;
|
|
||||||
this.body = body;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getCode() {
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getBody() {
|
|
||||||
return body;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "SimpleHttpResponse{"
|
|
||||||
+ "code=" + code
|
|
||||||
+ ", body='" + body + '\''
|
|
||||||
+ '}';
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
package com.velocitypowered.proxy.network.http;
|
|
||||||
|
|
||||||
import io.netty.channel.ChannelHandlerContext;
|
|
||||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
|
||||||
import io.netty.handler.codec.http.HttpContent;
|
|
||||||
import io.netty.handler.codec.http.HttpResponse;
|
|
||||||
import io.netty.handler.codec.http.HttpResponseStatus;
|
|
||||||
import io.netty.handler.codec.http.HttpUtil;
|
|
||||||
import io.netty.handler.codec.http.LastHttpContent;
|
|
||||||
import io.netty.util.ReferenceCountUtil;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
|
|
||||||
class SimpleHttpResponseCollector extends ChannelInboundHandlerAdapter {
|
|
||||||
|
|
||||||
private final StringBuilder buffer = new StringBuilder();
|
|
||||||
private final CompletableFuture<SimpleHttpResponse> reply;
|
|
||||||
private int httpCode;
|
|
||||||
|
|
||||||
SimpleHttpResponseCollector(CompletableFuture<SimpleHttpResponse> reply) {
|
|
||||||
this.reply = reply;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
|
||||||
try {
|
|
||||||
if (msg instanceof HttpResponse) {
|
|
||||||
HttpResponse response = (HttpResponse) msg;
|
|
||||||
HttpResponseStatus status = response.status();
|
|
||||||
this.httpCode = status.code();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg instanceof HttpContent) {
|
|
||||||
buffer.append(((HttpContent) msg).content().toString(StandardCharsets.UTF_8));
|
|
||||||
|
|
||||||
if (msg instanceof LastHttpContent) {
|
|
||||||
ctx.close();
|
|
||||||
reply.complete(new SimpleHttpResponse(httpCode, buffer.toString()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
ReferenceCountUtil.release(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
|
||||||
ctx.close();
|
|
||||||
reply.completeExceptionally(cause);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,73 @@
|
|||||||
|
package com.velocitypowered.proxy.network.netty;
|
||||||
|
|
||||||
|
import io.netty.channel.EventLoopGroup;
|
||||||
|
import io.netty.resolver.InetNameResolver;
|
||||||
|
import io.netty.resolver.dns.DnsAddressResolverGroup;
|
||||||
|
import io.netty.util.concurrent.EventExecutor;
|
||||||
|
import io.netty.util.concurrent.FutureListener;
|
||||||
|
import io.netty.util.concurrent.ImmediateEventExecutor;
|
||||||
|
import io.netty.util.concurrent.Promise;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class DnsAddressResolverGroupNameResolverAdapter extends InetNameResolver {
|
||||||
|
|
||||||
|
private final DnsAddressResolverGroup resolverGroup;
|
||||||
|
private final EventLoopGroup group;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a DnsAddressResolverGroupNameResolverAdapter.
|
||||||
|
* @param resolverGroup the resolver group to use
|
||||||
|
* @param group the event loop group
|
||||||
|
*/
|
||||||
|
public DnsAddressResolverGroupNameResolverAdapter(
|
||||||
|
DnsAddressResolverGroup resolverGroup, EventLoopGroup group) {
|
||||||
|
super(ImmediateEventExecutor.INSTANCE);
|
||||||
|
this.resolverGroup = resolverGroup;
|
||||||
|
this.group = group;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doResolve(String inetHost, Promise<InetAddress> promise) throws Exception {
|
||||||
|
EventExecutor executor = this.findExecutor();
|
||||||
|
resolverGroup.getResolver(executor).resolve(InetSocketAddress.createUnresolved(inetHost, 17))
|
||||||
|
.addListener((FutureListener<InetSocketAddress>) future -> {
|
||||||
|
if (future.isSuccess()) {
|
||||||
|
promise.trySuccess(future.getNow().getAddress());
|
||||||
|
} else {
|
||||||
|
promise.tryFailure(future.cause());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doResolveAll(String inetHost, Promise<List<InetAddress>> promise)
|
||||||
|
throws Exception {
|
||||||
|
EventExecutor executor = this.findExecutor();
|
||||||
|
resolverGroup.getResolver(executor).resolveAll(InetSocketAddress.createUnresolved(inetHost, 17))
|
||||||
|
.addListener((FutureListener<List<InetSocketAddress>>) future -> {
|
||||||
|
if (future.isSuccess()) {
|
||||||
|
List<InetAddress> addresses = new ArrayList<>(future.getNow().size());
|
||||||
|
for (InetSocketAddress address : future.getNow()) {
|
||||||
|
addresses.add(address.getAddress());
|
||||||
|
}
|
||||||
|
promise.trySuccess(addresses);
|
||||||
|
} else {
|
||||||
|
promise.tryFailure(future.cause());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private EventExecutor findExecutor() {
|
||||||
|
for (EventExecutor executor : group) {
|
||||||
|
if (executor.inEventLoop()) {
|
||||||
|
return executor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise, pick one
|
||||||
|
return group.next();
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@ package com.velocitypowered.proxy.plugin.loader.java;
|
|||||||
|
|
||||||
import com.google.inject.Binder;
|
import com.google.inject.Binder;
|
||||||
import com.google.inject.Module;
|
import com.google.inject.Module;
|
||||||
|
import com.google.inject.Scopes;
|
||||||
import com.velocitypowered.api.command.CommandManager;
|
import com.velocitypowered.api.command.CommandManager;
|
||||||
import com.velocitypowered.api.event.EventManager;
|
import com.velocitypowered.api.event.EventManager;
|
||||||
import com.velocitypowered.api.plugin.PluginDescription;
|
import com.velocitypowered.api.plugin.PluginDescription;
|
||||||
@ -27,6 +28,8 @@ class VelocityPluginModule implements Module {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configure(Binder binder) {
|
public void configure(Binder binder) {
|
||||||
|
binder.bind(description.getMainClass()).in(Scopes.SINGLETON);
|
||||||
|
|
||||||
binder.bind(Logger.class).toInstance(LoggerFactory.getLogger(description.getId()));
|
binder.bind(Logger.class).toInstance(LoggerFactory.getLogger(description.getId()));
|
||||||
binder.bind(ProxyServer.class).toInstance(server);
|
binder.bind(ProxyServer.class).toInstance(server);
|
||||||
binder.bind(Path.class).annotatedWith(DataDirectory.class)
|
binder.bind(Path.class).annotatedWith(DataDirectory.class)
|
||||||
|
@ -35,10 +35,9 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
|
|||||||
|
|
||||||
checkFrame(expectedSize >= threshold, "Uncompressed size %s is less than threshold %s",
|
checkFrame(expectedSize >= threshold, "Uncompressed size %s is less than threshold %s",
|
||||||
expectedSize, threshold);
|
expectedSize, threshold);
|
||||||
checkFrame(expectedSize <= MAXIMUM_UNCOMPRESSED_SIZE, "Expected uncompressed size"
|
int initialCapacity = Math.min(expectedSize, MAXIMUM_UNCOMPRESSED_SIZE);
|
||||||
+ "%s is larger than protocol maximum of %s", expectedSize, MAXIMUM_UNCOMPRESSED_SIZE);
|
|
||||||
ByteBuf compatibleIn = ensureCompatible(ctx.alloc(), compressor, in);
|
ByteBuf compatibleIn = ensureCompatible(ctx.alloc(), compressor, in);
|
||||||
ByteBuf uncompressed = preferredBuffer(ctx.alloc(), compressor, expectedSize);
|
ByteBuf uncompressed = preferredBuffer(ctx.alloc(), compressor, initialCapacity);
|
||||||
try {
|
try {
|
||||||
compressor.inflate(compatibleIn, uncompressed, expectedSize);
|
compressor.inflate(compatibleIn, uncompressed, expectedSize);
|
||||||
out.add(uncompressed);
|
out.add(uncompressed);
|
||||||
|
@ -4,24 +4,24 @@ import com.velocitypowered.api.network.ProtocolVersion;
|
|||||||
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
||||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||||
|
import com.velocitypowered.proxy.protocol.util.DeferredByteBufHolder;
|
||||||
import io.netty.buffer.ByteBuf;
|
import io.netty.buffer.ByteBuf;
|
||||||
import io.netty.buffer.Unpooled;
|
import io.netty.buffer.Unpooled;
|
||||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||||
|
|
||||||
public class LoginPluginMessage implements MinecraftPacket {
|
public class LoginPluginMessage extends DeferredByteBufHolder implements MinecraftPacket {
|
||||||
|
|
||||||
private int id;
|
private int id;
|
||||||
private @Nullable String channel;
|
private @Nullable String channel;
|
||||||
private ByteBuf data = Unpooled.EMPTY_BUFFER;
|
|
||||||
|
|
||||||
public LoginPluginMessage() {
|
public LoginPluginMessage() {
|
||||||
|
super(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public LoginPluginMessage(int id, @Nullable String channel, ByteBuf data) {
|
public LoginPluginMessage(int id, @Nullable String channel, ByteBuf data) {
|
||||||
|
super(data);
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.channel = channel;
|
this.channel = channel;
|
||||||
this.data = data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getId() {
|
public int getId() {
|
||||||
@ -35,16 +35,12 @@ public class LoginPluginMessage implements MinecraftPacket {
|
|||||||
return channel;
|
return channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ByteBuf getData() {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "LoginPluginMessage{"
|
return "LoginPluginMessage{"
|
||||||
+ "id=" + id
|
+ "id=" + id
|
||||||
+ ", channel='" + channel + '\''
|
+ ", channel='" + channel + '\''
|
||||||
+ ", data=" + data
|
+ ", data=" + super.toString()
|
||||||
+ '}';
|
+ '}';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,9 +49,9 @@ public class LoginPluginMessage implements MinecraftPacket {
|
|||||||
this.id = ProtocolUtils.readVarInt(buf);
|
this.id = ProtocolUtils.readVarInt(buf);
|
||||||
this.channel = ProtocolUtils.readString(buf);
|
this.channel = ProtocolUtils.readString(buf);
|
||||||
if (buf.isReadable()) {
|
if (buf.isReadable()) {
|
||||||
this.data = buf.readSlice(buf.readableBytes());
|
this.replace(buf.readSlice(buf.readableBytes()));
|
||||||
} else {
|
} 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!");
|
throw new IllegalStateException("Channel is not specified!");
|
||||||
}
|
}
|
||||||
ProtocolUtils.writeString(buf, channel);
|
ProtocolUtils.writeString(buf, channel);
|
||||||
buf.writeBytes(data);
|
buf.writeBytes(content());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -4,14 +4,25 @@ import com.velocitypowered.api.network.ProtocolVersion;
|
|||||||
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
||||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||||
|
import com.velocitypowered.proxy.protocol.util.DeferredByteBufHolder;
|
||||||
import io.netty.buffer.ByteBuf;
|
import io.netty.buffer.ByteBuf;
|
||||||
import io.netty.buffer.Unpooled;
|
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 int id;
|
||||||
private boolean success;
|
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() {
|
public int getId() {
|
||||||
return id;
|
return id;
|
||||||
@ -29,20 +40,12 @@ public class LoginPluginResponse implements MinecraftPacket {
|
|||||||
this.success = success;
|
this.success = success;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ByteBuf getData() {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setData(ByteBuf data) {
|
|
||||||
this.data = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "LoginPluginResponse{"
|
return "LoginPluginResponse{"
|
||||||
+ "id=" + id
|
+ "id=" + id
|
||||||
+ ", success=" + success
|
+ ", success=" + success
|
||||||
+ ", data=" + data
|
+ ", data=" + super.toString()
|
||||||
+ '}';
|
+ '}';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,9 +54,9 @@ public class LoginPluginResponse implements MinecraftPacket {
|
|||||||
this.id = ProtocolUtils.readVarInt(buf);
|
this.id = ProtocolUtils.readVarInt(buf);
|
||||||
this.success = buf.readBoolean();
|
this.success = buf.readBoolean();
|
||||||
if (buf.isReadable()) {
|
if (buf.isReadable()) {
|
||||||
this.data = buf.readSlice(buf.readableBytes());
|
this.replace(buf.readSlice(buf.readableBytes()));
|
||||||
} else {
|
} 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) {
|
public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
|
||||||
ProtocolUtils.writeVarInt(buf, id);
|
ProtocolUtils.writeVarInt(buf, id);
|
||||||
buf.writeBoolean(success);
|
buf.writeBoolean(success);
|
||||||
buf.writeBytes(data);
|
buf.writeBytes(content());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -1,21 +1,29 @@
|
|||||||
package com.velocitypowered.proxy.protocol.packet;
|
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 static com.velocitypowered.proxy.protocol.util.PluginMessageUtil.transformLegacyToModernChannel;
|
||||||
|
|
||||||
import com.velocitypowered.api.network.ProtocolVersion;
|
import com.velocitypowered.api.network.ProtocolVersion;
|
||||||
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
||||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
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.ByteBuf;
|
||||||
import io.netty.buffer.ByteBufUtil;
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||||
|
|
||||||
public class PluginMessage implements MinecraftPacket {
|
public class PluginMessage extends DeferredByteBufHolder implements MinecraftPacket {
|
||||||
|
|
||||||
private @Nullable String channel;
|
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() {
|
public String getChannel() {
|
||||||
if (channel == null) {
|
if (channel == null) {
|
||||||
@ -28,19 +36,11 @@ public class PluginMessage implements MinecraftPacket {
|
|||||||
this.channel = channel;
|
this.channel = channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] getData() {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setData(byte[] data) {
|
|
||||||
this.data = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "PluginMessage{"
|
return "PluginMessage{"
|
||||||
+ "channel='" + channel + '\''
|
+ "channel='" + channel + '\''
|
||||||
+ ", data=<removed>"
|
+ ", data=" + super.toString()
|
||||||
+ '}';
|
+ '}';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,8 +50,7 @@ public class PluginMessage implements MinecraftPacket {
|
|||||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_13) >= 0) {
|
if (version.compareTo(ProtocolVersion.MINECRAFT_1_13) >= 0) {
|
||||||
this.channel = transformLegacyToModernChannel(this.channel);
|
this.channel = transformLegacyToModernChannel(this.channel);
|
||||||
}
|
}
|
||||||
this.data = new byte[buf.readableBytes()];
|
this.replace(buf.readRetainedSlice(buf.readableBytes()));
|
||||||
buf.readBytes(data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -64,11 +63,51 @@ public class PluginMessage implements MinecraftPacket {
|
|||||||
} else {
|
} else {
|
||||||
ProtocolUtils.writeString(buf, this.channel);
|
ProtocolUtils.writeString(buf, this.channel);
|
||||||
}
|
}
|
||||||
buf.writeBytes(data);
|
buf.writeBytes(content());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean handle(MinecraftSessionHandler handler) {
|
public boolean handle(MinecraftSessionHandler handler) {
|
||||||
return handler.handle(this);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -133,5 +133,9 @@ public class TabCompleteResponse implements MinecraftPacket {
|
|||||||
public int compareTo(Offer o) {
|
public int compareTo(Offer o) {
|
||||||
return this.text.compareTo(o.text);
|
return this.text.compareTo(o.text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getText() {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 + "]";
|
||||||
|
}
|
||||||
|
}
|
@ -11,7 +11,9 @@ import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
|||||||
import com.velocitypowered.proxy.protocol.packet.PluginMessage;
|
import com.velocitypowered.proxy.protocol.packet.PluginMessage;
|
||||||
import io.netty.buffer.ByteBuf;
|
import io.netty.buffer.ByteBuf;
|
||||||
import io.netty.buffer.Unpooled;
|
import io.netty.buffer.Unpooled;
|
||||||
|
import io.netty.util.ByteProcessor;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
@ -93,12 +95,12 @@ public class PluginMessageUtil {
|
|||||||
checkNotNull(message, "message");
|
checkNotNull(message, "message");
|
||||||
checkArgument(isRegister(message) || isUnregister(message), "Unknown channel type %s",
|
checkArgument(isRegister(message) || isUnregister(message), "Unknown channel type %s",
|
||||||
message.getChannel());
|
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
|
// 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.
|
// has caused issues with 1.13+ compatibility. Just return an empty list.
|
||||||
return ImmutableList.of();
|
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"));
|
return ImmutableList.copyOf(channels.split("\0"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,10 +116,9 @@ public class PluginMessageUtil {
|
|||||||
Preconditions.checkArgument(channels.size() > 0, "no channels specified");
|
Preconditions.checkArgument(channels.size() > 0, "no channels specified");
|
||||||
String channelName = protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_13) >= 0
|
String channelName = protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_13) >= 0
|
||||||
? REGISTER_CHANNEL : REGISTER_CHANNEL_LEGACY;
|
? REGISTER_CHANNEL : REGISTER_CHANNEL_LEGACY;
|
||||||
PluginMessage message = new PluginMessage();
|
ByteBuf contents = Unpooled.buffer();
|
||||||
message.setChannel(channelName);
|
contents.writeCharSequence(String.join("\0", channels), StandardCharsets.UTF_8);
|
||||||
message.setData(String.join("\0", channels).getBytes(StandardCharsets.UTF_8));
|
return new PluginMessage(channelName, contents);
|
||||||
return message;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -133,21 +134,11 @@ public class PluginMessageUtil {
|
|||||||
|
|
||||||
String toAppend = " (" + version.getName() + ")";
|
String toAppend = " (" + version.getName() + ")";
|
||||||
|
|
||||||
byte[] rewrittenData;
|
|
||||||
ByteBuf rewrittenBuf = Unpooled.buffer();
|
ByteBuf rewrittenBuf = Unpooled.buffer();
|
||||||
try {
|
String currentBrand = ProtocolUtils.readString(message.content().slice());
|
||||||
String currentBrand = ProtocolUtils.readString(Unpooled.wrappedBuffer(message.getData()));
|
ProtocolUtils.writeString(rewrittenBuf, currentBrand + toAppend);
|
||||||
ProtocolUtils.writeString(rewrittenBuf, currentBrand + toAppend);
|
|
||||||
rewrittenData = new byte[rewrittenBuf.readableBytes()];
|
|
||||||
rewrittenBuf.readBytes(rewrittenData);
|
|
||||||
} finally {
|
|
||||||
rewrittenBuf.release();
|
|
||||||
}
|
|
||||||
|
|
||||||
PluginMessage newMsg = new PluginMessage();
|
return new PluginMessage(message.getChannel(), rewrittenBuf);
|
||||||
newMsg.setChannel(message.getChannel());
|
|
||||||
newMsg.setData(rewrittenData);
|
|
||||||
return newMsg;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final Pattern INVALID_IDENTIFIER_REGEX = Pattern.compile("[^a-z0-9\\-_]*");
|
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("");
|
return "legacy:" + INVALID_IDENTIFIER_REGEX.matcher(lower).replaceAll("");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -18,13 +18,15 @@ public class PingSessionHandler implements MinecraftSessionHandler {
|
|||||||
private final CompletableFuture<ServerPing> result;
|
private final CompletableFuture<ServerPing> result;
|
||||||
private final RegisteredServer server;
|
private final RegisteredServer server;
|
||||||
private final MinecraftConnection connection;
|
private final MinecraftConnection connection;
|
||||||
|
private final ProtocolVersion version;
|
||||||
private boolean completed = false;
|
private boolean completed = false;
|
||||||
|
|
||||||
PingSessionHandler(CompletableFuture<ServerPing> result, RegisteredServer server,
|
PingSessionHandler(CompletableFuture<ServerPing> result, RegisteredServer server,
|
||||||
MinecraftConnection connection) {
|
MinecraftConnection connection, ProtocolVersion version) {
|
||||||
this.result = result;
|
this.result = result;
|
||||||
this.server = server;
|
this.server = server;
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
|
this.version = version;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -33,11 +35,13 @@ public class PingSessionHandler implements MinecraftSessionHandler {
|
|||||||
handshake.setNextStatus(StateRegistry.STATUS_ID);
|
handshake.setNextStatus(StateRegistry.STATUS_ID);
|
||||||
handshake.setServerAddress(server.getServerInfo().getAddress().getHostString());
|
handshake.setServerAddress(server.getServerInfo().getAddress().getHostString());
|
||||||
handshake.setPort(server.getServerInfo().getAddress().getPort());
|
handshake.setPort(server.getServerInfo().getAddress().getPort());
|
||||||
handshake.setProtocolVersion(ProtocolVersion.MINIMUM_VERSION);
|
handshake.setProtocolVersion(version);
|
||||||
connection.write(handshake);
|
connection.delayedWrite(handshake);
|
||||||
|
|
||||||
connection.setState(StateRegistry.STATUS);
|
connection.setState(StateRegistry.STATUS);
|
||||||
connection.write(StatusRequest.INSTANCE);
|
connection.delayedWrite(StatusRequest.INSTANCE);
|
||||||
|
|
||||||
|
connection.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -9,6 +9,7 @@ import static com.velocitypowered.proxy.network.Connections.READ_TIMEOUT;
|
|||||||
|
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.velocitypowered.api.network.ProtocolVersion;
|
||||||
import com.velocitypowered.api.proxy.Player;
|
import com.velocitypowered.api.proxy.Player;
|
||||||
import com.velocitypowered.api.proxy.ServerConnection;
|
import com.velocitypowered.api.proxy.ServerConnection;
|
||||||
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
||||||
@ -19,7 +20,6 @@ import com.velocitypowered.proxy.VelocityServer;
|
|||||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
|
||||||
import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder;
|
import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder;
|
||||||
import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder;
|
import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder;
|
||||||
import com.velocitypowered.proxy.protocol.netty.MinecraftVarintFrameDecoder;
|
import com.velocitypowered.proxy.protocol.netty.MinecraftVarintFrameDecoder;
|
||||||
@ -28,6 +28,7 @@ import io.netty.channel.Channel;
|
|||||||
import io.netty.channel.ChannelFuture;
|
import io.netty.channel.ChannelFuture;
|
||||||
import io.netty.channel.ChannelFutureListener;
|
import io.netty.channel.ChannelFutureListener;
|
||||||
import io.netty.channel.ChannelInitializer;
|
import io.netty.channel.ChannelInitializer;
|
||||||
|
import io.netty.channel.EventLoop;
|
||||||
import io.netty.handler.timeout.ReadTimeoutHandler;
|
import io.netty.handler.timeout.ReadTimeoutHandler;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@ -59,11 +60,22 @@ public class VelocityRegisteredServer implements RegisteredServer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CompletableFuture<ServerPing> ping() {
|
public CompletableFuture<ServerPing> ping() {
|
||||||
|
return ping(null, ProtocolVersion.UNKNOWN);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pings the specified server using the specified event {@code loop}, claiming to be
|
||||||
|
* {@code version}.
|
||||||
|
* @param loop the event loop to use
|
||||||
|
* @param version the version to report
|
||||||
|
* @return the server list ping response
|
||||||
|
*/
|
||||||
|
public CompletableFuture<ServerPing> ping(@Nullable EventLoop loop, ProtocolVersion version) {
|
||||||
if (server == null) {
|
if (server == null) {
|
||||||
throw new IllegalStateException("No Velocity proxy instance available");
|
throw new IllegalStateException("No Velocity proxy instance available");
|
||||||
}
|
}
|
||||||
CompletableFuture<ServerPing> pingFuture = new CompletableFuture<>();
|
CompletableFuture<ServerPing> pingFuture = new CompletableFuture<>();
|
||||||
server.initializeGenericBootstrap()
|
server.createBootstrap(loop)
|
||||||
.handler(new ChannelInitializer<Channel>() {
|
.handler(new ChannelInitializer<Channel>() {
|
||||||
@Override
|
@Override
|
||||||
protected void initChannel(Channel ch) throws Exception {
|
protected void initChannel(Channel ch) throws Exception {
|
||||||
@ -87,8 +99,8 @@ public class VelocityRegisteredServer implements RegisteredServer {
|
|||||||
public void operationComplete(ChannelFuture future) throws Exception {
|
public void operationComplete(ChannelFuture future) throws Exception {
|
||||||
if (future.isSuccess()) {
|
if (future.isSuccess()) {
|
||||||
MinecraftConnection conn = future.channel().pipeline().get(MinecraftConnection.class);
|
MinecraftConnection conn = future.channel().pipeline().get(MinecraftConnection.class);
|
||||||
conn.setSessionHandler(
|
conn.setSessionHandler(new PingSessionHandler(
|
||||||
new PingSessionHandler(pingFuture, VelocityRegisteredServer.this, conn));
|
pingFuture, VelocityRegisteredServer.this, conn, version));
|
||||||
} else {
|
} else {
|
||||||
pingFuture.completeExceptionally(future.cause());
|
pingFuture.completeExceptionally(future.cause());
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ import org.checkerframework.checker.nullness.qual.Nullable;
|
|||||||
public class VelocityTabList implements TabList {
|
public class VelocityTabList implements TabList {
|
||||||
|
|
||||||
private final MinecraftConnection connection;
|
private final MinecraftConnection connection;
|
||||||
private final Map<UUID, TabListEntry> entries = new ConcurrentHashMap<>();
|
private final Map<UUID, VelocityTabListEntry> entries = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
public VelocityTabList(MinecraftConnection connection) {
|
public VelocityTabList(MinecraftConnection connection) {
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
@ -46,15 +46,19 @@ public class VelocityTabList implements TabList {
|
|||||||
"The provided entry was not created by this tab list");
|
"The provided entry was not created by this tab list");
|
||||||
Preconditions.checkArgument(!entries.containsKey(entry.getProfile().getId()),
|
Preconditions.checkArgument(!entries.containsKey(entry.getProfile().getId()),
|
||||||
"this TabList already contains an entry with the same uuid");
|
"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);
|
PlayerListItem.Item packetItem = PlayerListItem.Item.from(entry);
|
||||||
connection.write(
|
connection.write(
|
||||||
new PlayerListItem(PlayerListItem.ADD_PLAYER, Collections.singletonList(packetItem)));
|
new PlayerListItem(PlayerListItem.ADD_PLAYER, Collections.singletonList(packetItem)));
|
||||||
entries.put(entry.getProfile().getId(), entry);
|
entries.put(entry.getProfile().getId(), (VelocityTabListEntry) entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<TabListEntry> removeEntry(UUID uuid) {
|
public Optional<TabListEntry> removeEntry(UUID uuid) {
|
||||||
|
Preconditions.checkNotNull(uuid, "uuid");
|
||||||
|
|
||||||
TabListEntry entry = entries.remove(uuid);
|
TabListEntry entry = entries.remove(uuid);
|
||||||
if (entry != null) {
|
if (entry != null) {
|
||||||
PlayerListItem.Item packetItem = PlayerListItem.Item.from(entry);
|
PlayerListItem.Item packetItem = PlayerListItem.Item.from(entry);
|
||||||
@ -65,6 +69,12 @@ public class VelocityTabList implements TabList {
|
|||||||
return Optional.ofNullable(entry);
|
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
|
* 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
|
* {@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) {
|
if (name == null || properties == null) {
|
||||||
throw new IllegalStateException("Got null game profile for ADD_PLAYER");
|
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)
|
.tabList(this)
|
||||||
.profile(new GameProfile(uuid, name, properties))
|
.profile(new GameProfile(uuid, name, properties))
|
||||||
.displayName(item.getDisplayName())
|
.displayName(item.getDisplayName())
|
||||||
@ -124,23 +134,23 @@ public class VelocityTabList implements TabList {
|
|||||||
entries.remove(uuid);
|
entries.remove(uuid);
|
||||||
break;
|
break;
|
||||||
case PlayerListItem.UPDATE_DISPLAY_NAME: {
|
case PlayerListItem.UPDATE_DISPLAY_NAME: {
|
||||||
TabListEntry entry = entries.get(uuid);
|
VelocityTabListEntry entry = entries.get(uuid);
|
||||||
if (entry != null) {
|
if (entry != null) {
|
||||||
entry.setDisplayName(item.getDisplayName());
|
entry.setDisplayNameInternal(item.getDisplayName());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case PlayerListItem.UPDATE_LATENCY: {
|
case PlayerListItem.UPDATE_LATENCY: {
|
||||||
TabListEntry entry = entries.get(uuid);
|
VelocityTabListEntry entry = entries.get(uuid);
|
||||||
if (entry != null) {
|
if (entry != null) {
|
||||||
entry.setLatency(item.getLatency());
|
entry.setLatencyInternal(item.getLatency());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case PlayerListItem.UPDATE_GAMEMODE: {
|
case PlayerListItem.UPDATE_GAMEMODE: {
|
||||||
TabListEntry entry = entries.get(uuid);
|
VelocityTabListEntry entry = entries.get(uuid);
|
||||||
if (entry != null) {
|
if (entry != null) {
|
||||||
entry.setLatency(item.getGameMode());
|
entry.setGameModeInternal(item.getGameMode());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -47,6 +47,10 @@ public class VelocityTabListEntry implements TabListEntry {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setDisplayNameInternal(@Nullable Component displayName) {
|
||||||
|
this.displayName = displayName;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getLatency() {
|
public int getLatency() {
|
||||||
return latency;
|
return latency;
|
||||||
@ -59,6 +63,10 @@ public class VelocityTabListEntry implements TabListEntry {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setLatencyInternal(int latency) {
|
||||||
|
this.latency = latency;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getGameMode() {
|
public int getGameMode() {
|
||||||
return gameMode;
|
return gameMode;
|
||||||
@ -70,4 +78,8 @@ public class VelocityTabListEntry implements TabListEntry {
|
|||||||
tabList.updateEntry(PlayerListItem.UPDATE_GAMEMODE, this);
|
tabList.updateEntry(PlayerListItem.UPDATE_GAMEMODE, this);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setGameModeInternal(int gameMode) {
|
||||||
|
this.gameMode = gameMode;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package com.velocitypowered.proxy.util;
|
package com.velocitypowered.proxy.util;
|
||||||
|
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
|
import com.google.common.net.InetAddresses;
|
||||||
|
import java.net.InetAddress;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
|
||||||
@ -19,7 +21,12 @@ public class AddressUtil {
|
|||||||
public static InetSocketAddress parseAddress(String ip) {
|
public static InetSocketAddress parseAddress(String ip) {
|
||||||
Preconditions.checkNotNull(ip, "ip");
|
Preconditions.checkNotNull(ip, "ip");
|
||||||
URI uri = URI.create("tcp://" + ip);
|
URI uri = URI.create("tcp://" + ip);
|
||||||
return InetSocketAddress.createUnresolved(uri.getHost(), uri.getPort());
|
try {
|
||||||
|
InetAddress ia = InetAddresses.forUriString(uri.getHost());
|
||||||
|
return new InetSocketAddress(ia, uri.getPort());
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return InetSocketAddress.createUnresolved(uri.getHost(), uri.getPort());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -7,7 +7,13 @@ import net.kyori.text.format.TextColor;
|
|||||||
public class VelocityMessages {
|
public class VelocityMessages {
|
||||||
|
|
||||||
public static final Component ONLINE_MODE_ONLY = TextComponent
|
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
|
public static final Component NO_PROXY_BEHIND_PROXY = TextComponent
|
||||||
.of("Running Velocity behind Velocity isn't supported.", TextColor.RED);
|
.of("Running Velocity behind Velocity isn't supported.", TextColor.RED);
|
||||||
public static final Component NO_AVAILABLE_SERVERS = TextComponent
|
public static final Component NO_AVAILABLE_SERVERS = TextComponent
|
||||||
@ -15,7 +21,7 @@ public class VelocityMessages {
|
|||||||
public static final Component ALREADY_CONNECTED = TextComponent
|
public static final Component ALREADY_CONNECTED = TextComponent
|
||||||
.of("You are already connected to this proxy!", TextColor.RED);
|
.of("You are already connected to this proxy!", TextColor.RED);
|
||||||
public static final Component MOVED_TO_NEW_SERVER = TextComponent
|
public static final Component MOVED_TO_NEW_SERVER = TextComponent
|
||||||
.of("You were moved from the server you were on because you were kicked", TextColor.RED);
|
.of("The server you were on kicked you: ", TextColor.RED);
|
||||||
|
|
||||||
private VelocityMessages() {
|
private VelocityMessages() {
|
||||||
throw new AssertionError();
|
throw new AssertionError();
|
||||||
|
@ -271,6 +271,6 @@ public class VelocityBossBar implements com.velocitypowered.api.util.bossbar.Bos
|
|||||||
|
|
||||||
private void sendPacket(Player player, MinecraftPacket packet) {
|
private void sendPacket(Player player, MinecraftPacket packet) {
|
||||||
ConnectedPlayer connected = (ConnectedPlayer) player;
|
ConnectedPlayer connected = (ConnectedPlayer) player;
|
||||||
connected.getMinecraftConnection().write(packet);
|
connected.getConnection().write(packet);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -1 +1,2 @@
|
|||||||
log4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
|
log4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
|
||||||
|
log4j.skipJansi=true
|
||||||
|
Laden…
In neuem Issue referenzieren
Einen Benutzer sperren