Mirror von
https://github.com/PaperMC/Velocity.git
synchronisiert 2024-11-17 05:20:14 +01:00
Merge branch 'master' into tab-complete
Dieser Commit ist enthalten in:
Commit
9a36bd6cae
5
.gitignore
vendored
5
.gitignore
vendored
@ -102,7 +102,7 @@ hs_err_pid*
|
|||||||
|
|
||||||
### Gradle ###
|
### Gradle ###
|
||||||
.gradle
|
.gradle
|
||||||
/build/
|
build/
|
||||||
|
|
||||||
# Ignore Gradle GUI config
|
# Ignore Gradle GUI config
|
||||||
gradle-app.setting
|
gradle-app.setting
|
||||||
@ -121,4 +121,5 @@ gradle-app.setting
|
|||||||
|
|
||||||
# Other trash
|
# Other trash
|
||||||
logs/
|
logs/
|
||||||
/velocity.toml
|
/velocity.toml
|
||||||
|
server-icon.png
|
@ -34,8 +34,6 @@ page.
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
Velocity is far from finished, but most of the essential pieces are in place:
|
Velocity is far from finished, but most of the essential pieces you would
|
||||||
you can switch between two servers running Minecraft 1.8-1.13. More versions
|
expect are in place. Velocity supports Minecraft 1.8-1.13. More functionality
|
||||||
and functionality is planned.
|
is planned.
|
||||||
|
|
||||||
You should join us on **irc.spi.gt** `#velocity` or send us a pull request.
|
|
||||||
|
88
api/src/main/java/com/velocitypowered/api/server/Favicon.java
Normale Datei
88
api/src/main/java/com/velocitypowered/api/server/Favicon.java
Normale Datei
@ -0,0 +1,88 @@
|
|||||||
|
package com.velocitypowered.api.server;
|
||||||
|
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a Minecraft server favicon. A Minecraft server favicon is a 64x64 image that can be displayed to a remote
|
||||||
|
* client that sends a Server List Ping packet, and is automatically displayed in the Minecraft client.
|
||||||
|
*/
|
||||||
|
public final class Favicon {
|
||||||
|
private final String base64Url;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Directly create a favicon using its Base64 URL directly. You are generally better served by the create() series
|
||||||
|
* of functions.
|
||||||
|
* @param base64Url the url for use with this favicon
|
||||||
|
*/
|
||||||
|
public Favicon(@Nonnull String base64Url) {
|
||||||
|
this.base64Url = Preconditions.checkNotNull(base64Url, "base64Url");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Base64-encoded URI for this image.
|
||||||
|
* @return a URL representing this favicon
|
||||||
|
*/
|
||||||
|
public String getBase64Url() {
|
||||||
|
return base64Url;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
Favicon favicon = (Favicon) o;
|
||||||
|
return Objects.equals(base64Url, favicon.base64Url);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(base64Url);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Favicon{" +
|
||||||
|
"base64Url='" + base64Url + '\'' +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@code Favicon} from the specified {@code image}.
|
||||||
|
* @param image the image to use for the favicon
|
||||||
|
* @return the created {@link Favicon} instance
|
||||||
|
*/
|
||||||
|
public static Favicon create(@Nonnull BufferedImage image) {
|
||||||
|
Preconditions.checkNotNull(image, "image");
|
||||||
|
Preconditions.checkArgument(image.getWidth() == 64 && image.getHeight() == 64, "Image does not have" +
|
||||||
|
" 64x64 dimensions (found %sx%s)", image.getWidth(), image.getHeight());
|
||||||
|
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||||
|
try {
|
||||||
|
ImageIO.write(image, "PNG", os);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
return new Favicon("data:image/png;base64," + Base64.getEncoder().encodeToString(os.toByteArray()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@code Favicon} by reading the image from the specified {@code path}.
|
||||||
|
* @param path the path to the image to create a favicon for
|
||||||
|
* @return the created {@link Favicon} instance
|
||||||
|
*/
|
||||||
|
public static Favicon create(@Nonnull Path path) throws IOException {
|
||||||
|
try (InputStream stream = Files.newInputStream(path)) {
|
||||||
|
return create(ImageIO.read(stream));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -28,6 +28,7 @@ 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 "org.apache.logging.log4j:log4j-api:${log4jVersion}"
|
compile "org.apache.logging.log4j:log4j-api:${log4jVersion}"
|
||||||
compile "org.apache.logging.log4j:log4j-core:${log4jVersion}"
|
compile "org.apache.logging.log4j:log4j-core:${log4jVersion}"
|
||||||
|
@ -24,6 +24,7 @@ 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.*;
|
||||||
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;
|
||||||
@ -37,6 +38,7 @@ import org.apache.logging.log4j.Logger;
|
|||||||
|
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.ThreadFactory;
|
import java.util.concurrent.ThreadFactory;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@ -53,46 +55,25 @@ import static com.velocitypowered.network.Connections.READ_TIMEOUT;
|
|||||||
public final class ConnectionManager {
|
public final class ConnectionManager {
|
||||||
private static final Logger logger = LogManager.getLogger(ConnectionManager.class);
|
private static final Logger logger = LogManager.getLogger(ConnectionManager.class);
|
||||||
|
|
||||||
private static final String DISABLE_EPOLL_PROPERTY = "velocity.connection.disable-epoll";
|
|
||||||
private static final boolean DISABLE_EPOLL = Boolean.getBoolean(DISABLE_EPOLL_PROPERTY);
|
|
||||||
private final Set<Channel> endpoints = new HashSet<>();
|
private final Set<Channel> endpoints = new HashSet<>();
|
||||||
private final Class<? extends ServerSocketChannel> serverSocketChannelClass;
|
private final TransportType transportType;
|
||||||
private final Class<? extends SocketChannel> socketChannelClass;
|
|
||||||
private final Class<? extends DatagramChannel> datagramChannelClass;
|
|
||||||
private final EventLoopGroup bossGroup;
|
private final EventLoopGroup bossGroup;
|
||||||
private final EventLoopGroup workerGroup;
|
private final EventLoopGroup workerGroup;
|
||||||
|
|
||||||
public ConnectionManager() {
|
public ConnectionManager() {
|
||||||
final boolean epoll = canUseEpoll();
|
this.transportType = TransportType.bestType();
|
||||||
if (epoll) {
|
this.bossGroup = transportType.createEventLoopGroup(true);
|
||||||
this.serverSocketChannelClass = EpollServerSocketChannel.class;
|
this.workerGroup = transportType.createEventLoopGroup(false);
|
||||||
this.socketChannelClass = EpollSocketChannel.class;
|
this.logChannelInformation();
|
||||||
this.datagramChannelClass = EpollDatagramChannel.class;
|
|
||||||
this.bossGroup = new EpollEventLoopGroup(0, createThreadFactory("Netty Epoll Boss #%d"));
|
|
||||||
this.workerGroup = new EpollEventLoopGroup(0, createThreadFactory("Netty Epoll Worker #%d"));
|
|
||||||
} else {
|
|
||||||
this.serverSocketChannelClass = NioServerSocketChannel.class;
|
|
||||||
this.socketChannelClass = NioSocketChannel.class;
|
|
||||||
this.datagramChannelClass = NioDatagramChannel.class;
|
|
||||||
this.bossGroup = new NioEventLoopGroup(0, createThreadFactory("Netty Nio Boss #%d"));
|
|
||||||
this.workerGroup = new NioEventLoopGroup(0, createThreadFactory("Netty Nio Worker #%d"));
|
|
||||||
}
|
|
||||||
this.logChannelInformation(epoll);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void logChannelInformation(final boolean epoll) {
|
private void logChannelInformation() {
|
||||||
final StringBuilder sb = new StringBuilder();
|
logger.info("Using channel type {}", transportType);
|
||||||
sb.append("Using channel type ");
|
|
||||||
sb.append(epoll ? "epoll": "nio");
|
|
||||||
if(DISABLE_EPOLL) {
|
|
||||||
sb.append(String.format(" - epoll explicitly disabled using -D%s=true", DISABLE_EPOLL_PROPERTY));
|
|
||||||
}
|
|
||||||
logger.info(sb.toString()); // TODO: move to logger
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void bind(final InetSocketAddress address) {
|
public void bind(final InetSocketAddress address) {
|
||||||
final ServerBootstrap bootstrap = new ServerBootstrap()
|
final ServerBootstrap bootstrap = new ServerBootstrap()
|
||||||
.channel(this.serverSocketChannelClass)
|
.channel(this.transportType.serverSocketChannelClass)
|
||||||
.group(this.bossGroup, this.workerGroup)
|
.group(this.bossGroup, this.workerGroup)
|
||||||
.childHandler(new ChannelInitializer<Channel>() {
|
.childHandler(new ChannelInitializer<Channel>() {
|
||||||
@Override
|
@Override
|
||||||
@ -129,7 +110,7 @@ public final class ConnectionManager {
|
|||||||
|
|
||||||
public void queryBind(final String hostname, final int port) {
|
public void queryBind(final String hostname, final int port) {
|
||||||
Bootstrap bootstrap = new Bootstrap()
|
Bootstrap bootstrap = new Bootstrap()
|
||||||
.channel(datagramChannelClass)
|
.channel(transportType.datagramChannelClass)
|
||||||
.group(this.workerGroup)
|
.group(this.workerGroup)
|
||||||
.handler(new GS4QueryHandler())
|
.handler(new GS4QueryHandler())
|
||||||
.localAddress(hostname, port);
|
.localAddress(hostname, port);
|
||||||
@ -147,7 +128,7 @@ public final class ConnectionManager {
|
|||||||
|
|
||||||
public Bootstrap createWorker() {
|
public Bootstrap createWorker() {
|
||||||
return new Bootstrap()
|
return new Bootstrap()
|
||||||
.channel(this.socketChannelClass)
|
.channel(this.transportType.socketChannelClass)
|
||||||
.group(this.workerGroup);
|
.group(this.workerGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,14 +143,61 @@ public final class ConnectionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean canUseEpoll() {
|
|
||||||
return Epoll.isAvailable() && !DISABLE_EPOLL;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ThreadFactory createThreadFactory(final String nameFormat) {
|
private static ThreadFactory createThreadFactory(final String nameFormat) {
|
||||||
return new ThreadFactoryBuilder()
|
return new ThreadFactoryBuilder()
|
||||||
.setNameFormat(nameFormat)
|
.setNameFormat(nameFormat)
|
||||||
.setDaemon(true)
|
.setDaemon(true)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum TransportType {
|
||||||
|
NIO(NioServerSocketChannel.class, NioSocketChannel.class, NioDatagramChannel.class) {
|
||||||
|
@Override
|
||||||
|
public EventLoopGroup createEventLoopGroup(boolean boss) {
|
||||||
|
String name = "Netty NIO " + (boss ? "Boss" : "Worker") + " #%d";
|
||||||
|
return new NioEventLoopGroup(0, createThreadFactory(name));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
EPOLL(EpollServerSocketChannel.class, EpollSocketChannel.class, EpollDatagramChannel.class) {
|
||||||
|
@Override
|
||||||
|
public EventLoopGroup createEventLoopGroup(boolean boss) {
|
||||||
|
String name = "Netty Epoll " + (boss ? "Boss" : "Worker") + " #%d";
|
||||||
|
return new EpollEventLoopGroup(0, createThreadFactory(name));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
KQUEUE(KQueueServerSocketChannel.class, KQueueSocketChannel.class, KQueueDatagramChannel.class) {
|
||||||
|
@Override
|
||||||
|
public EventLoopGroup createEventLoopGroup(boolean boss) {
|
||||||
|
String name = "Netty Kqueue " + (boss ? "Boss" : "Worker") + " #%d";
|
||||||
|
return new KQueueEventLoopGroup(0, createThreadFactory(name));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private final Class<? extends ServerSocketChannel> serverSocketChannelClass;
|
||||||
|
private final Class<? extends SocketChannel> socketChannelClass;
|
||||||
|
private final Class<? extends DatagramChannel> datagramChannelClass;
|
||||||
|
|
||||||
|
TransportType(Class<? extends ServerSocketChannel> serverSocketChannelClass, Class<? extends SocketChannel> socketChannelClass, Class<? extends DatagramChannel> datagramChannelClass) {
|
||||||
|
this.serverSocketChannelClass = serverSocketChannelClass;
|
||||||
|
this.socketChannelClass = socketChannelClass;
|
||||||
|
this.datagramChannelClass = datagramChannelClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return name().toLowerCase(Locale.US);
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract EventLoopGroup createEventLoopGroup(boolean boss);
|
||||||
|
|
||||||
|
public static TransportType bestType() {
|
||||||
|
if (Epoll.isAvailable()) {
|
||||||
|
return EPOLL;
|
||||||
|
} else if (KQueue.isAvailable()) {
|
||||||
|
return KQUEUE;
|
||||||
|
} else {
|
||||||
|
return NIO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,12 @@ package com.velocitypowered.proxy;
|
|||||||
import com.velocitypowered.proxy.console.VelocityConsole;
|
import com.velocitypowered.proxy.console.VelocityConsole;
|
||||||
|
|
||||||
public class Velocity {
|
public class Velocity {
|
||||||
|
static {
|
||||||
|
// We use BufferedImage for favicons, and on macOS this puts the Java application in the dock. How inconvenient.
|
||||||
|
// Force AWT to work with its head chopped off.
|
||||||
|
System.setProperty("java.awt.headless", "true");
|
||||||
|
}
|
||||||
|
|
||||||
public static void main(String... args) {
|
public static void main(String... args) {
|
||||||
final VelocityServer server = VelocityServer.getServer();
|
final VelocityServer server = VelocityServer.getServer();
|
||||||
server.start();
|
server.start();
|
||||||
|
@ -7,6 +7,7 @@ import com.google.gson.GsonBuilder;
|
|||||||
import com.velocitypowered.api.command.CommandInvoker;
|
import com.velocitypowered.api.command.CommandInvoker;
|
||||||
import com.velocitypowered.api.proxy.Player;
|
import com.velocitypowered.api.proxy.Player;
|
||||||
import com.velocitypowered.api.proxy.ProxyServer;
|
import com.velocitypowered.api.proxy.ProxyServer;
|
||||||
|
import com.velocitypowered.api.server.Favicon;
|
||||||
import com.velocitypowered.natives.util.Natives;
|
import com.velocitypowered.natives.util.Natives;
|
||||||
import com.velocitypowered.network.ConnectionManager;
|
import com.velocitypowered.network.ConnectionManager;
|
||||||
import com.velocitypowered.proxy.command.ServerCommand;
|
import com.velocitypowered.proxy.command.ServerCommand;
|
||||||
@ -17,8 +18,10 @@ import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
|||||||
import com.velocitypowered.proxy.connection.http.NettyHttpClient;
|
import com.velocitypowered.proxy.connection.http.NettyHttpClient;
|
||||||
import com.velocitypowered.api.server.ServerInfo;
|
import com.velocitypowered.api.server.ServerInfo;
|
||||||
import com.velocitypowered.proxy.command.CommandManager;
|
import com.velocitypowered.proxy.command.CommandManager;
|
||||||
|
import com.velocitypowered.proxy.protocol.util.FaviconSerializer;
|
||||||
import com.velocitypowered.proxy.util.AddressUtil;
|
import com.velocitypowered.proxy.util.AddressUtil;
|
||||||
import com.velocitypowered.proxy.util.EncryptionUtils;
|
import com.velocitypowered.proxy.util.EncryptionUtils;
|
||||||
|
import com.velocitypowered.proxy.util.Ratelimiter;
|
||||||
import com.velocitypowered.proxy.util.ServerMap;
|
import com.velocitypowered.proxy.util.ServerMap;
|
||||||
import io.netty.bootstrap.Bootstrap;
|
import io.netty.bootstrap.Bootstrap;
|
||||||
import net.kyori.text.Component;
|
import net.kyori.text.Component;
|
||||||
@ -44,6 +47,7 @@ public class VelocityServer implements ProxyServer {
|
|||||||
private static final VelocityServer INSTANCE = new VelocityServer();
|
private static final VelocityServer INSTANCE = new VelocityServer();
|
||||||
public static final Gson GSON = new GsonBuilder()
|
public static final Gson GSON = new GsonBuilder()
|
||||||
.registerTypeHierarchyAdapter(Component.class, new GsonComponentSerializer())
|
.registerTypeHierarchyAdapter(Component.class, new GsonComponentSerializer())
|
||||||
|
.registerTypeHierarchyAdapter(Favicon.class, new FaviconSerializer())
|
||||||
.create();
|
.create();
|
||||||
|
|
||||||
private final ConnectionManager cm = new ConnectionManager();
|
private final ConnectionManager cm = new ConnectionManager();
|
||||||
@ -68,6 +72,7 @@ public class VelocityServer implements ProxyServer {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
private final Ratelimiter ipAttemptLimiter = new Ratelimiter(3000); // TODO: Configurable.
|
||||||
|
|
||||||
private VelocityServer() {
|
private VelocityServer() {
|
||||||
commandManager.registerCommand("velocity", new VelocityCommand());
|
commandManager.registerCommand("velocity", new VelocityCommand());
|
||||||
@ -159,6 +164,10 @@ public class VelocityServer implements ProxyServer {
|
|||||||
return httpClient;
|
return httpClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Ratelimiter getIpAttemptLimiter() {
|
||||||
|
return ipAttemptLimiter;
|
||||||
|
}
|
||||||
|
|
||||||
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) {
|
if (connectionsByName.putIfAbsent(lowerName, connection) != null) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package com.velocitypowered.proxy.config;
|
package com.velocitypowered.proxy.config;
|
||||||
|
|
||||||
public enum IPForwardingMode {
|
public enum PlayerInfoForwarding {
|
||||||
NONE,
|
NONE,
|
||||||
LEGACY,
|
LEGACY,
|
||||||
MODERN
|
MODERN
|
@ -2,8 +2,10 @@ package com.velocitypowered.proxy.config;
|
|||||||
|
|
||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
import com.moandjiezana.toml.Toml;
|
import com.moandjiezana.toml.Toml;
|
||||||
|
import com.velocitypowered.api.server.Favicon;
|
||||||
import com.velocitypowered.proxy.util.AddressUtil;
|
import com.velocitypowered.proxy.util.AddressUtil;
|
||||||
import com.velocitypowered.api.util.LegacyChatColorUtils;
|
import com.velocitypowered.api.util.LegacyChatColorUtils;
|
||||||
|
import io.netty.buffer.ByteBufUtil;
|
||||||
import net.kyori.text.Component;
|
import net.kyori.text.Component;
|
||||||
import net.kyori.text.serializer.ComponentSerializers;
|
import net.kyori.text.serializer.ComponentSerializers;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
@ -15,6 +17,7 @@ import java.net.InetSocketAddress;
|
|||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -26,7 +29,7 @@ public class VelocityConfiguration {
|
|||||||
private final String motd;
|
private final String motd;
|
||||||
private final int showMaxPlayers;
|
private final int showMaxPlayers;
|
||||||
private final boolean onlineMode;
|
private final boolean onlineMode;
|
||||||
private final IPForwardingMode ipForwardingMode;
|
private final PlayerInfoForwarding playerInfoForwardingMode;
|
||||||
private final Map<String, String> servers;
|
private final Map<String, String> servers;
|
||||||
private final List<String> attemptConnectionOrder;
|
private final List<String> attemptConnectionOrder;
|
||||||
private final int compressionThreshold;
|
private final int compressionThreshold;
|
||||||
@ -36,22 +39,27 @@ public class VelocityConfiguration {
|
|||||||
private final int queryPort;
|
private final int queryPort;
|
||||||
|
|
||||||
private Component motdAsComponent;
|
private Component motdAsComponent;
|
||||||
|
private Favicon favicon;
|
||||||
|
|
||||||
|
private final byte[] forwardingSecret;
|
||||||
|
|
||||||
private VelocityConfiguration(String bind, String motd, int showMaxPlayers, boolean onlineMode,
|
private VelocityConfiguration(String bind, String motd, int showMaxPlayers, boolean onlineMode,
|
||||||
IPForwardingMode ipForwardingMode, Map<String, String> servers,
|
PlayerInfoForwarding playerInfoForwardingMode, Map<String, String> servers,
|
||||||
List<String> attemptConnectionOrder, int compressionThreshold,
|
List<String> attemptConnectionOrder, int compressionThreshold,
|
||||||
int compressionLevel, boolean queryEnabled, int queryPort) {
|
int compressionLevel, boolean queryEnabled, int queryPort,
|
||||||
|
byte[] forwardingSecret) {
|
||||||
this.bind = bind;
|
this.bind = bind;
|
||||||
this.motd = motd;
|
this.motd = motd;
|
||||||
this.showMaxPlayers = showMaxPlayers;
|
this.showMaxPlayers = showMaxPlayers;
|
||||||
this.onlineMode = onlineMode;
|
this.onlineMode = onlineMode;
|
||||||
this.ipForwardingMode = ipForwardingMode;
|
this.playerInfoForwardingMode = playerInfoForwardingMode;
|
||||||
this.servers = servers;
|
this.servers = servers;
|
||||||
this.attemptConnectionOrder = attemptConnectionOrder;
|
this.attemptConnectionOrder = attemptConnectionOrder;
|
||||||
this.compressionThreshold = compressionThreshold;
|
this.compressionThreshold = compressionThreshold;
|
||||||
this.compressionLevel = compressionLevel;
|
this.compressionLevel = compressionLevel;
|
||||||
this.queryEnabled = queryEnabled;
|
this.queryEnabled = queryEnabled;
|
||||||
this.queryPort = queryPort;
|
this.queryPort = queryPort;
|
||||||
|
this.forwardingSecret = forwardingSecret;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean validate() {
|
public boolean validate() {
|
||||||
@ -73,9 +81,15 @@ public class VelocityConfiguration {
|
|||||||
logger.info("Proxy is running in offline mode!");
|
logger.info("Proxy is running in offline mode!");
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (ipForwardingMode) {
|
switch (playerInfoForwardingMode) {
|
||||||
case NONE:
|
case NONE:
|
||||||
logger.info("IP forwarding is disabled! All players will appear to be connecting from the proxy and will have offline-mode UUIDs.");
|
logger.info("Player info forwarding is disabled! All players will appear to be connecting from the proxy and will have offline-mode UUIDs.");
|
||||||
|
break;
|
||||||
|
case MODERN:
|
||||||
|
if (forwardingSecret.length == 0) {
|
||||||
|
logger.error("You don't have a forwarding secret set.");
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,9 +138,22 @@ public class VelocityConfiguration {
|
|||||||
logger.warn("ALL packets going through the proxy are going to be compressed. This may hurt performance.");
|
logger.warn("ALL packets going through the proxy are going to be compressed. This may hurt performance.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadFavicon();
|
||||||
|
|
||||||
return valid;
|
return valid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void loadFavicon() {
|
||||||
|
Path faviconPath = Paths.get("server-icon.png");
|
||||||
|
if (Files.exists(faviconPath)) {
|
||||||
|
try {
|
||||||
|
this.favicon = Favicon.create(faviconPath);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.info("Unable to load your server-icon.png, continuing without it.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public InetSocketAddress getBind() {
|
public InetSocketAddress getBind() {
|
||||||
return AddressUtil.parseAddress(bind);
|
return AddressUtil.parseAddress(bind);
|
||||||
}
|
}
|
||||||
@ -162,8 +189,8 @@ public class VelocityConfiguration {
|
|||||||
return onlineMode;
|
return onlineMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IPForwardingMode getIpForwardingMode() {
|
public PlayerInfoForwarding getPlayerInfoForwardingMode() {
|
||||||
return ipForwardingMode;
|
return playerInfoForwardingMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<String, String> getServers() {
|
public Map<String, String> getServers() {
|
||||||
@ -182,6 +209,14 @@ public class VelocityConfiguration {
|
|||||||
return compressionLevel;
|
return compressionLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Favicon getFavicon() {
|
||||||
|
return favicon;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getForwardingSecret() {
|
||||||
|
return forwardingSecret;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "VelocityConfiguration{" +
|
return "VelocityConfiguration{" +
|
||||||
@ -189,7 +224,7 @@ public class VelocityConfiguration {
|
|||||||
", motd='" + motd + '\'' +
|
", motd='" + motd + '\'' +
|
||||||
", showMaxPlayers=" + showMaxPlayers +
|
", showMaxPlayers=" + showMaxPlayers +
|
||||||
", onlineMode=" + onlineMode +
|
", onlineMode=" + onlineMode +
|
||||||
", ipForwardingMode=" + ipForwardingMode +
|
", playerInfoForwardingMode=" + playerInfoForwardingMode +
|
||||||
", servers=" + servers +
|
", servers=" + servers +
|
||||||
", attemptConnectionOrder=" + attemptConnectionOrder +
|
", attemptConnectionOrder=" + attemptConnectionOrder +
|
||||||
", compressionThreshold=" + compressionThreshold +
|
", compressionThreshold=" + compressionThreshold +
|
||||||
@ -197,6 +232,8 @@ public class VelocityConfiguration {
|
|||||||
", queryEnabled=" + queryEnabled +
|
", queryEnabled=" + queryEnabled +
|
||||||
", queryPort=" + queryPort +
|
", queryPort=" + queryPort +
|
||||||
", motdAsComponent=" + motdAsComponent +
|
", motdAsComponent=" + motdAsComponent +
|
||||||
|
", favicon=" + favicon +
|
||||||
|
", forwardingSecret=" + ByteBufUtil.hexDump(forwardingSecret) +
|
||||||
'}';
|
'}';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,18 +252,22 @@ public class VelocityConfiguration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
byte[] forwardingSecret = toml.getString("player-info-forwarding-secret", "5up3r53cr3t")
|
||||||
|
.getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
return new VelocityConfiguration(
|
return new VelocityConfiguration(
|
||||||
toml.getString("bind"),
|
toml.getString("bind", "0.0.0.0:25577"),
|
||||||
toml.getString("motd"),
|
toml.getString("motd", "&3A Velocity Server"),
|
||||||
toml.getLong("show-max-players").intValue(),
|
toml.getLong("show-max-players", 500L).intValue(),
|
||||||
toml.getBoolean("online-mode"),
|
toml.getBoolean("online-mode", true),
|
||||||
IPForwardingMode.valueOf(toml.getString("ip-forwarding").toUpperCase()),
|
PlayerInfoForwarding.valueOf(toml.getString("player-info-forwarding", "MODERN").toUpperCase()),
|
||||||
ImmutableMap.copyOf(servers),
|
ImmutableMap.copyOf(servers),
|
||||||
toml.getTable("servers").getList("try"),
|
toml.getTable("servers").getList("try"),
|
||||||
toml.getTable("advanced").getLong("compression-threshold", 1024L).intValue(),
|
toml.getTable("advanced").getLong("compression-threshold", 1024L).intValue(),
|
||||||
toml.getTable("advanced").getLong("compression-level", -1L).intValue(),
|
toml.getTable("advanced").getLong("compression-level", -1L).intValue(),
|
||||||
toml.getTable("query").getBoolean("enabled"),
|
toml.getTable("query").getBoolean("enabled", false),
|
||||||
toml.getTable("query").getLong("port", 25577L).intValue());
|
toml.getTable("query").getLong("port", 25577L).intValue(),
|
||||||
|
forwardingSecret);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,8 @@ package com.velocitypowered.proxy.connection.backend;
|
|||||||
|
|
||||||
import com.velocitypowered.api.proxy.ConnectionRequestBuilder;
|
import com.velocitypowered.api.proxy.ConnectionRequestBuilder;
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
import com.velocitypowered.proxy.VelocityServer;
|
||||||
import com.velocitypowered.proxy.config.IPForwardingMode;
|
import com.velocitypowered.proxy.config.PlayerInfoForwarding;
|
||||||
|
import com.velocitypowered.proxy.config.VelocityConfiguration;
|
||||||
import com.velocitypowered.proxy.connection.VelocityConstants;
|
import com.velocitypowered.proxy.connection.VelocityConstants;
|
||||||
import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler;
|
import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler;
|
||||||
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults;
|
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults;
|
||||||
@ -17,6 +18,11 @@ import io.netty.buffer.Unpooled;
|
|||||||
import io.netty.channel.ChannelPipeline;
|
import io.netty.channel.ChannelPipeline;
|
||||||
import net.kyori.text.TextComponent;
|
import net.kyori.text.TextComponent;
|
||||||
|
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.concurrent.ScheduledFuture;
|
import java.util.concurrent.ScheduledFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
@ -30,7 +36,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void activated() {
|
public void activated() {
|
||||||
if (VelocityServer.getServer().getConfiguration().getIpForwardingMode() == IPForwardingMode.MODERN) {
|
if (VelocityServer.getServer().getConfiguration().getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN) {
|
||||||
forwardingCheckTask = connection.getMinecraftConnection().getChannel().eventLoop().schedule(() -> {
|
forwardingCheckTask = connection.getMinecraftConnection().getChannel().eventLoop().schedule(() -> {
|
||||||
connection.getProxyPlayer().handleConnectionException(connection.getServerInfo(),
|
connection.getProxyPlayer().handleConnectionException(connection.getServerInfo(),
|
||||||
TextComponent.of("Your server did not send the forwarding request in time. Is it set up correctly?"));
|
TextComponent.of("Your server did not send the forwarding request in time. Is it set up correctly?"));
|
||||||
@ -44,12 +50,14 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
|||||||
throw new IllegalStateException("Backend server is online-mode!");
|
throw new IllegalStateException("Backend server is online-mode!");
|
||||||
} else if (packet instanceof LoginPluginMessage) {
|
} else if (packet instanceof LoginPluginMessage) {
|
||||||
LoginPluginMessage message = (LoginPluginMessage) packet;
|
LoginPluginMessage message = (LoginPluginMessage) packet;
|
||||||
if (VelocityServer.getServer().getConfiguration().getIpForwardingMode() == IPForwardingMode.MODERN &&
|
VelocityConfiguration configuration = VelocityServer.getServer().getConfiguration();
|
||||||
|
if (configuration.getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN &&
|
||||||
message.getChannel().equals(VelocityConstants.VELOCITY_IP_FORWARDING_CHANNEL)) {
|
message.getChannel().equals(VelocityConstants.VELOCITY_IP_FORWARDING_CHANNEL)) {
|
||||||
LoginPluginResponse response = new LoginPluginResponse();
|
LoginPluginResponse response = new LoginPluginResponse();
|
||||||
response.setSuccess(true);
|
response.setSuccess(true);
|
||||||
response.setId(message.getId());
|
response.setId(message.getId());
|
||||||
response.setData(createForwardingData(connection.getProxyPlayer().getRemoteAddress().getHostString(),
|
response.setData(createForwardingData(configuration.getForwardingSecret(),
|
||||||
|
connection.getProxyPlayer().getRemoteAddress().getHostString(),
|
||||||
connection.getProxyPlayer().getProfile()));
|
connection.getProxyPlayer().getProfile()));
|
||||||
connection.getMinecraftConnection().write(response);
|
connection.getMinecraftConnection().write(response);
|
||||||
cancelForwardingCheck();
|
cancelForwardingCheck();
|
||||||
@ -122,23 +130,43 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ByteBuf createForwardingData(String address, GameProfile profile) {
|
static ByteBuf createForwardingData(byte[] hmacSecret, String address, GameProfile profile) {
|
||||||
ByteBuf buf = Unpooled.buffer();
|
ByteBuf dataToForward = Unpooled.buffer();
|
||||||
ProtocolUtils.writeString(buf, address);
|
ByteBuf finalData = Unpooled.buffer();
|
||||||
ProtocolUtils.writeUuid(buf, profile.idAsUuid());
|
try {
|
||||||
ProtocolUtils.writeString(buf, profile.getName());
|
ProtocolUtils.writeString(dataToForward, address);
|
||||||
ProtocolUtils.writeVarInt(buf, profile.getProperties().size());
|
ProtocolUtils.writeUuid(dataToForward, profile.idAsUuid());
|
||||||
for (GameProfile.Property property : profile.getProperties()) {
|
ProtocolUtils.writeString(dataToForward, profile.getName());
|
||||||
ProtocolUtils.writeString(buf, property.getName());
|
ProtocolUtils.writeVarInt(dataToForward, profile.getProperties().size());
|
||||||
ProtocolUtils.writeString(buf, property.getValue());
|
for (GameProfile.Property property : profile.getProperties()) {
|
||||||
String signature = property.getSignature();
|
ProtocolUtils.writeString(dataToForward, property.getName());
|
||||||
if (signature != null) {
|
ProtocolUtils.writeString(dataToForward, property.getValue());
|
||||||
buf.writeBoolean(true);
|
String signature = property.getSignature();
|
||||||
ProtocolUtils.writeString(buf, signature);
|
if (signature != null) {
|
||||||
} else {
|
dataToForward.writeBoolean(true);
|
||||||
buf.writeBoolean(false);
|
ProtocolUtils.writeString(dataToForward, signature);
|
||||||
|
} else {
|
||||||
|
dataToForward.writeBoolean(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SecretKey key = new SecretKeySpec(hmacSecret, "HmacSHA256");
|
||||||
|
Mac mac = Mac.getInstance("HmacSHA256");
|
||||||
|
mac.init(key);
|
||||||
|
mac.update(dataToForward.array(), dataToForward.arrayOffset(), dataToForward.readableBytes());
|
||||||
|
byte[] sig = mac.doFinal();
|
||||||
|
finalData.writeBytes(sig);
|
||||||
|
finalData.writeBytes(dataToForward);
|
||||||
|
return finalData;
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
finalData.release();
|
||||||
|
throw new RuntimeException("Unable to authenticate data", e);
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
// Should never happen
|
||||||
|
finalData.release();
|
||||||
|
throw new AssertionError(e);
|
||||||
|
} finally {
|
||||||
|
dataToForward.release();
|
||||||
}
|
}
|
||||||
return buf;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package com.velocitypowered.proxy.connection.backend;
|
package com.velocitypowered.proxy.connection.backend;
|
||||||
|
|
||||||
import com.velocitypowered.api.proxy.ConnectionRequestBuilder;
|
import com.velocitypowered.api.proxy.ConnectionRequestBuilder;
|
||||||
import com.velocitypowered.proxy.config.IPForwardingMode;
|
import com.velocitypowered.proxy.config.PlayerInfoForwarding;
|
||||||
import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation;
|
import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation;
|
||||||
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults;
|
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults;
|
||||||
import com.velocitypowered.proxy.protocol.ProtocolConstants;
|
import com.velocitypowered.proxy.protocol.ProtocolConstants;
|
||||||
@ -97,7 +97,7 @@ public class ServerConnection implements MinecraftConnectionAssociation {
|
|||||||
Handshake handshake = new Handshake();
|
Handshake handshake = new Handshake();
|
||||||
handshake.setNextStatus(StateRegistry.LOGIN_ID);
|
handshake.setNextStatus(StateRegistry.LOGIN_ID);
|
||||||
handshake.setProtocolVersion(proxyPlayer.getConnection().getProtocolVersion());
|
handshake.setProtocolVersion(proxyPlayer.getConnection().getProtocolVersion());
|
||||||
if (VelocityServer.getServer().getConfiguration().getIpForwardingMode() == IPForwardingMode.LEGACY) {
|
if (VelocityServer.getServer().getConfiguration().getPlayerInfoForwardingMode() == PlayerInfoForwarding.LEGACY) {
|
||||||
handshake.setServerAddress(createBungeeForwardingAddress());
|
handshake.setServerAddress(createBungeeForwardingAddress());
|
||||||
} else {
|
} else {
|
||||||
handshake.setServerAddress(serverInfo.getAddress().getHostString());
|
handshake.setServerAddress(serverInfo.getAddress().getHostString());
|
||||||
@ -111,7 +111,7 @@ public class ServerConnection implements MinecraftConnectionAssociation {
|
|||||||
|
|
||||||
// Send the server login packet for <=1.12.2 and for 1.13+ servers not using "modern" forwarding.
|
// Send the server login packet for <=1.12.2 and for 1.13+ servers not using "modern" forwarding.
|
||||||
if (protocolVersion <= ProtocolConstants.MINECRAFT_1_12_2 ||
|
if (protocolVersion <= ProtocolConstants.MINECRAFT_1_12_2 ||
|
||||||
VelocityServer.getServer().getConfiguration().getIpForwardingMode() != IPForwardingMode.MODERN) {
|
VelocityServer.getServer().getConfiguration().getPlayerInfoForwardingMode() != PlayerInfoForwarding.MODERN) {
|
||||||
ServerLogin login = new ServerLogin();
|
ServerLogin login = new ServerLogin();
|
||||||
login.setUsername(proxyPlayer.getUsername());
|
login.setUsername(proxyPlayer.getUsername());
|
||||||
minecraftConnection.write(login);
|
minecraftConnection.write(login);
|
||||||
|
@ -15,6 +15,7 @@ import net.kyori.text.TextComponent;
|
|||||||
import net.kyori.text.TranslatableComponent;
|
import net.kyori.text.TranslatableComponent;
|
||||||
import net.kyori.text.format.TextColor;
|
import net.kyori.text.format.TextColor;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
|
|
||||||
public class HandshakeSessionHandler implements MinecraftSessionHandler {
|
public class HandshakeSessionHandler implements MinecraftSessionHandler {
|
||||||
@ -50,6 +51,11 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
|
|||||||
connection.closeWith(Disconnect.create(TranslatableComponent.of("multiplayer.disconnect.outdated_client")));
|
connection.closeWith(Disconnect.create(TranslatableComponent.of("multiplayer.disconnect.outdated_client")));
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
|
InetAddress address = ((InetSocketAddress) connection.getChannel().remoteAddress()).getAddress();
|
||||||
|
if (!VelocityServer.getServer().getIpAttemptLimiter().attempt(address)) {
|
||||||
|
connection.closeWith(Disconnect.create(TextComponent.of("You are logging in too fast, try again later.")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
connection.setSessionHandler(new LoginSessionHandler(connection));
|
connection.setSessionHandler(new LoginSessionHandler(connection));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -91,6 +91,11 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
|||||||
VelocityServer.getServer().getHttpClient()
|
VelocityServer.getServer().getHttpClient()
|
||||||
.get(new URL(String.format(MOJANG_SERVER_AUTH_URL, login.getUsername(), serverId, playerIp)))
|
.get(new URL(String.format(MOJANG_SERVER_AUTH_URL, login.getUsername(), serverId, playerIp)))
|
||||||
.thenAcceptAsync(profileResponse -> {
|
.thenAcceptAsync(profileResponse -> {
|
||||||
|
if (inbound.isClosed()) {
|
||||||
|
// The player disconnected after we authenticated them.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
inbound.enableEncryption(decryptedSharedSecret);
|
inbound.enableEncryption(decryptedSharedSecret);
|
||||||
} catch (GeneralSecurityException e) {
|
} catch (GeneralSecurityException e) {
|
||||||
|
@ -41,7 +41,7 @@ public class StatusSessionHandler implements MinecraftSessionHandler {
|
|||||||
new ServerPing.Version(shownVersion, "Velocity " + ProtocolConstants.SUPPORTED_GENERIC_VERSION_STRING),
|
new ServerPing.Version(shownVersion, "Velocity " + ProtocolConstants.SUPPORTED_GENERIC_VERSION_STRING),
|
||||||
new ServerPing.Players(VelocityServer.getServer().getPlayerCount(), configuration.getShowMaxPlayers()),
|
new ServerPing.Players(VelocityServer.getServer().getPlayerCount(), configuration.getShowMaxPlayers()),
|
||||||
configuration.getMotdComponent(),
|
configuration.getMotdComponent(),
|
||||||
null
|
configuration.getFavicon()
|
||||||
);
|
);
|
||||||
StatusResponse response = new StatusResponse();
|
StatusResponse response = new StatusResponse();
|
||||||
response.setStatus(VelocityServer.GSON.toJson(ping));
|
response.setStatus(VelocityServer.GSON.toJson(ping));
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
package com.velocitypowered.proxy.data;
|
package com.velocitypowered.proxy.data;
|
||||||
|
|
||||||
|
import com.velocitypowered.api.server.Favicon;
|
||||||
import net.kyori.text.Component;
|
import net.kyori.text.Component;
|
||||||
|
|
||||||
public class ServerPing {
|
public class ServerPing {
|
||||||
private final Version version;
|
private final Version version;
|
||||||
private final Players players;
|
private final Players players;
|
||||||
private final Component description;
|
private final Component description;
|
||||||
private final String favicon;
|
private final Favicon favicon;
|
||||||
|
|
||||||
public ServerPing(Version version, Players players, Component description, String favicon) {
|
public ServerPing(Version version, Players players, Component description, Favicon favicon) {
|
||||||
this.version = version;
|
this.version = version;
|
||||||
this.players = players;
|
this.players = players;
|
||||||
this.description = description;
|
this.description = description;
|
||||||
@ -27,7 +28,7 @@ public class ServerPing {
|
|||||||
return description;
|
return description;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getFavicon() {
|
public Favicon getFavicon() {
|
||||||
return favicon;
|
return favicon;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
package com.velocitypowered.proxy.protocol.util;
|
||||||
|
|
||||||
|
import com.google.gson.*;
|
||||||
|
import com.velocitypowered.api.server.Favicon;
|
||||||
|
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
|
||||||
|
public class FaviconSerializer implements JsonSerializer<Favicon>, JsonDeserializer<Favicon> {
|
||||||
|
@Override
|
||||||
|
public Favicon deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||||
|
return new Favicon(json.getAsString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JsonElement serialize(Favicon src, Type typeOfSrc, JsonSerializationContext context) {
|
||||||
|
return new JsonPrimitive(src.getBase64Url());
|
||||||
|
}
|
||||||
|
}
|
41
proxy/src/main/java/com/velocitypowered/proxy/util/Ratelimiter.java
Normale Datei
41
proxy/src/main/java/com/velocitypowered/proxy/util/Ratelimiter.java
Normale Datei
@ -0,0 +1,41 @@
|
|||||||
|
package com.velocitypowered.proxy.util;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import com.google.common.base.Ticker;
|
||||||
|
import com.google.common.cache.Cache;
|
||||||
|
import com.google.common.cache.CacheBuilder;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
public class Ratelimiter {
|
||||||
|
private final Cache<InetAddress, Long> expiringCache;
|
||||||
|
private final long timeoutNanos;
|
||||||
|
|
||||||
|
public Ratelimiter(long timeoutMs) {
|
||||||
|
this(timeoutMs, Ticker.systemTicker());
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
Ratelimiter(long timeoutMs, Ticker ticker) {
|
||||||
|
this.timeoutNanos = TimeUnit.MILLISECONDS.toNanos(timeoutMs);
|
||||||
|
this.expiringCache = CacheBuilder.newBuilder()
|
||||||
|
.ticker(ticker)
|
||||||
|
.concurrencyLevel(Runtime.getRuntime().availableProcessors())
|
||||||
|
.expireAfterWrite(timeoutMs, TimeUnit.MILLISECONDS)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean attempt(InetAddress address) {
|
||||||
|
long expectedNewValue = System.nanoTime() + timeoutNanos;
|
||||||
|
long last;
|
||||||
|
try {
|
||||||
|
last = expiringCache.get(address, () -> expectedNewValue);
|
||||||
|
} catch (ExecutionException e) {
|
||||||
|
// It should be impossible for this to fail.
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
return expectedNewValue == last;
|
||||||
|
}
|
||||||
|
}
|
@ -19,7 +19,10 @@ online-mode = true
|
|||||||
# servers using Minecraft 1.12 or lower.
|
# servers using Minecraft 1.12 or lower.
|
||||||
# - "modern": Forward player IPs and UUIDs as part of the login process using Velocity's native
|
# - "modern": Forward player IPs and UUIDs as part of the login process using Velocity's native
|
||||||
# forwarding. Only applicable for Minecraft 1.13 or higher.
|
# forwarding. Only applicable for Minecraft 1.13 or higher.
|
||||||
ip-forwarding = "modern"
|
player-info-forwarding = "modern"
|
||||||
|
|
||||||
|
# If you are using modern IP forwarding, configure an unique secret here.
|
||||||
|
player-info-forwarding-secret = "5up3r53cr3t"
|
||||||
|
|
||||||
[servers]
|
[servers]
|
||||||
# Configure your servers here.
|
# Configure your servers here.
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
package com.velocitypowered.proxy.util;
|
||||||
|
|
||||||
|
import com.google.common.base.Ticker;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class RatelimiterTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void attempt() {
|
||||||
|
long base = System.nanoTime();
|
||||||
|
AtomicLong extra = new AtomicLong();
|
||||||
|
Ticker testTicker = new Ticker() {
|
||||||
|
@Override
|
||||||
|
public long read() {
|
||||||
|
return base + extra.get();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ratelimiter ratelimiter = new Ratelimiter(1000, testTicker);
|
||||||
|
assertTrue(ratelimiter.attempt(InetAddress.getLoopbackAddress()));
|
||||||
|
assertFalse(ratelimiter.attempt(InetAddress.getLoopbackAddress()));
|
||||||
|
extra.addAndGet(TimeUnit.SECONDS.toNanos(2));
|
||||||
|
assertTrue(ratelimiter.attempt(InetAddress.getLoopbackAddress()));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
package com.velocitypowered.proxy.util;
|
||||||
|
|
||||||
|
import com.velocitypowered.api.server.ServerInfo;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class ServerMapTest {
|
||||||
|
private static final InetSocketAddress TEST_ADDRESS = new InetSocketAddress(InetAddress.getLoopbackAddress(), 25565);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void respectsCaseInsensitivity() {
|
||||||
|
ServerMap map = new ServerMap();
|
||||||
|
ServerInfo info = new ServerInfo("TestServer", TEST_ADDRESS);
|
||||||
|
map.register(info);
|
||||||
|
|
||||||
|
assertEquals(Optional.of(info), map.getServer("TestServer"));
|
||||||
|
assertEquals(Optional.of(info), map.getServer("testserver"));
|
||||||
|
assertEquals(Optional.of(info), map.getServer("TESTSERVER"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsRepeatedRegisterAttempts() {
|
||||||
|
ServerMap map = new ServerMap();
|
||||||
|
ServerInfo info = new ServerInfo("TestServer", TEST_ADDRESS);
|
||||||
|
map.register(info);
|
||||||
|
|
||||||
|
ServerInfo willReject = new ServerInfo("TESTSERVER", TEST_ADDRESS);
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> map.register(willReject));
|
||||||
|
}
|
||||||
|
}
|
Laden…
In neuem Issue referenzieren
Einen Benutzer sperren