geforkt von Mirrors/Velocity
Merge branch 'master' into tab-complete
Dieser Commit ist enthalten in:
Commit
9a36bd6cae
3
.gitignore
vendored
3
.gitignore
vendored
@ -102,7 +102,7 @@ hs_err_pid*
|
||||
|
||||
### Gradle ###
|
||||
.gradle
|
||||
/build/
|
||||
build/
|
||||
|
||||
# Ignore Gradle GUI config
|
||||
gradle-app.setting
|
||||
@ -122,3 +122,4 @@ gradle-app.setting
|
||||
# Other trash
|
||||
logs/
|
||||
/velocity.toml
|
||||
server-icon.png
|
@ -34,8 +34,6 @@ page.
|
||||
|
||||
## Status
|
||||
|
||||
Velocity is far from finished, but most of the essential pieces are in place:
|
||||
you can switch between two servers running Minecraft 1.8-1.13. More versions
|
||||
and functionality is planned.
|
||||
|
||||
You should join us on **irc.spi.gt** `#velocity` or send us a pull request.
|
||||
Velocity is far from finished, but most of the essential pieces you would
|
||||
expect are in place. Velocity supports Minecraft 1.8-1.13. More functionality
|
||||
is planned.
|
||||
|
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-transport-native-epoll:${nettyVersion}"
|
||||
compile "io.netty:netty-transport-native-epoll:${nettyVersion}:linux-x86_64"
|
||||
compile "io.netty:netty-transport-native-kqueue:${nettyVersion}:osx-x86_64"
|
||||
|
||||
compile "org.apache.logging.log4j:log4j-api:${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.EpollServerSocketChannel;
|
||||
import io.netty.channel.epoll.EpollSocketChannel;
|
||||
import io.netty.channel.kqueue.*;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.channel.socket.DatagramChannel;
|
||||
import io.netty.channel.socket.ServerSocketChannel;
|
||||
@ -37,6 +38,7 @@ import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@ -53,46 +55,25 @@ import static com.velocitypowered.network.Connections.READ_TIMEOUT;
|
||||
public final class ConnectionManager {
|
||||
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 Class<? extends ServerSocketChannel> serverSocketChannelClass;
|
||||
private final Class<? extends SocketChannel> socketChannelClass;
|
||||
private final Class<? extends DatagramChannel> datagramChannelClass;
|
||||
private final TransportType transportType;
|
||||
private final EventLoopGroup bossGroup;
|
||||
private final EventLoopGroup workerGroup;
|
||||
|
||||
public ConnectionManager() {
|
||||
final boolean epoll = canUseEpoll();
|
||||
if (epoll) {
|
||||
this.serverSocketChannelClass = EpollServerSocketChannel.class;
|
||||
this.socketChannelClass = EpollSocketChannel.class;
|
||||
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);
|
||||
this.transportType = TransportType.bestType();
|
||||
this.bossGroup = transportType.createEventLoopGroup(true);
|
||||
this.workerGroup = transportType.createEventLoopGroup(false);
|
||||
this.logChannelInformation();
|
||||
}
|
||||
|
||||
private void logChannelInformation(final boolean epoll) {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
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
|
||||
private void logChannelInformation() {
|
||||
logger.info("Using channel type {}", transportType);
|
||||
}
|
||||
|
||||
public void bind(final InetSocketAddress address) {
|
||||
final ServerBootstrap bootstrap = new ServerBootstrap()
|
||||
.channel(this.serverSocketChannelClass)
|
||||
.channel(this.transportType.serverSocketChannelClass)
|
||||
.group(this.bossGroup, this.workerGroup)
|
||||
.childHandler(new ChannelInitializer<Channel>() {
|
||||
@Override
|
||||
@ -129,7 +110,7 @@ public final class ConnectionManager {
|
||||
|
||||
public void queryBind(final String hostname, final int port) {
|
||||
Bootstrap bootstrap = new Bootstrap()
|
||||
.channel(datagramChannelClass)
|
||||
.channel(transportType.datagramChannelClass)
|
||||
.group(this.workerGroup)
|
||||
.handler(new GS4QueryHandler())
|
||||
.localAddress(hostname, port);
|
||||
@ -147,7 +128,7 @@ public final class ConnectionManager {
|
||||
|
||||
public Bootstrap createWorker() {
|
||||
return new Bootstrap()
|
||||
.channel(this.socketChannelClass)
|
||||
.channel(this.transportType.socketChannelClass)
|
||||
.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) {
|
||||
return new ThreadFactoryBuilder()
|
||||
.setNameFormat(nameFormat)
|
||||
.setDaemon(true)
|
||||
.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;
|
||||
|
||||
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) {
|
||||
final VelocityServer server = VelocityServer.getServer();
|
||||
server.start();
|
||||
|
@ -7,6 +7,7 @@ import com.google.gson.GsonBuilder;
|
||||
import com.velocitypowered.api.command.CommandInvoker;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import com.velocitypowered.api.proxy.ProxyServer;
|
||||
import com.velocitypowered.api.server.Favicon;
|
||||
import com.velocitypowered.natives.util.Natives;
|
||||
import com.velocitypowered.network.ConnectionManager;
|
||||
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.api.server.ServerInfo;
|
||||
import com.velocitypowered.proxy.command.CommandManager;
|
||||
import com.velocitypowered.proxy.protocol.util.FaviconSerializer;
|
||||
import com.velocitypowered.proxy.util.AddressUtil;
|
||||
import com.velocitypowered.proxy.util.EncryptionUtils;
|
||||
import com.velocitypowered.proxy.util.Ratelimiter;
|
||||
import com.velocitypowered.proxy.util.ServerMap;
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
import net.kyori.text.Component;
|
||||
@ -44,6 +47,7 @@ public class VelocityServer implements ProxyServer {
|
||||
private static final VelocityServer INSTANCE = new VelocityServer();
|
||||
public static final Gson GSON = new GsonBuilder()
|
||||
.registerTypeHierarchyAdapter(Component.class, new GsonComponentSerializer())
|
||||
.registerTypeHierarchyAdapter(Favicon.class, new FaviconSerializer())
|
||||
.create();
|
||||
|
||||
private final ConnectionManager cm = new ConnectionManager();
|
||||
@ -68,6 +72,7 @@ public class VelocityServer implements ProxyServer {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
private final Ratelimiter ipAttemptLimiter = new Ratelimiter(3000); // TODO: Configurable.
|
||||
|
||||
private VelocityServer() {
|
||||
commandManager.registerCommand("velocity", new VelocityCommand());
|
||||
@ -159,6 +164,10 @@ public class VelocityServer implements ProxyServer {
|
||||
return httpClient;
|
||||
}
|
||||
|
||||
public Ratelimiter getIpAttemptLimiter() {
|
||||
return ipAttemptLimiter;
|
||||
}
|
||||
|
||||
public boolean registerConnection(ConnectedPlayer connection) {
|
||||
String lowerName = connection.getUsername().toLowerCase(Locale.US);
|
||||
if (connectionsByName.putIfAbsent(lowerName, connection) != null) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
package com.velocitypowered.proxy.config;
|
||||
|
||||
public enum IPForwardingMode {
|
||||
public enum PlayerInfoForwarding {
|
||||
NONE,
|
||||
LEGACY,
|
||||
MODERN
|
@ -2,8 +2,10 @@ package com.velocitypowered.proxy.config;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.moandjiezana.toml.Toml;
|
||||
import com.velocitypowered.api.server.Favicon;
|
||||
import com.velocitypowered.proxy.util.AddressUtil;
|
||||
import com.velocitypowered.api.util.LegacyChatColorUtils;
|
||||
import io.netty.buffer.ByteBufUtil;
|
||||
import net.kyori.text.Component;
|
||||
import net.kyori.text.serializer.ComponentSerializers;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
@ -15,6 +17,7 @@ import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -26,7 +29,7 @@ public class VelocityConfiguration {
|
||||
private final String motd;
|
||||
private final int showMaxPlayers;
|
||||
private final boolean onlineMode;
|
||||
private final IPForwardingMode ipForwardingMode;
|
||||
private final PlayerInfoForwarding playerInfoForwardingMode;
|
||||
private final Map<String, String> servers;
|
||||
private final List<String> attemptConnectionOrder;
|
||||
private final int compressionThreshold;
|
||||
@ -36,22 +39,27 @@ public class VelocityConfiguration {
|
||||
private final int queryPort;
|
||||
|
||||
private Component motdAsComponent;
|
||||
private Favicon favicon;
|
||||
|
||||
private final byte[] forwardingSecret;
|
||||
|
||||
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,
|
||||
int compressionLevel, boolean queryEnabled, int queryPort) {
|
||||
int compressionLevel, boolean queryEnabled, int queryPort,
|
||||
byte[] forwardingSecret) {
|
||||
this.bind = bind;
|
||||
this.motd = motd;
|
||||
this.showMaxPlayers = showMaxPlayers;
|
||||
this.onlineMode = onlineMode;
|
||||
this.ipForwardingMode = ipForwardingMode;
|
||||
this.playerInfoForwardingMode = playerInfoForwardingMode;
|
||||
this.servers = servers;
|
||||
this.attemptConnectionOrder = attemptConnectionOrder;
|
||||
this.compressionThreshold = compressionThreshold;
|
||||
this.compressionLevel = compressionLevel;
|
||||
this.queryEnabled = queryEnabled;
|
||||
this.queryPort = queryPort;
|
||||
this.forwardingSecret = forwardingSecret;
|
||||
}
|
||||
|
||||
public boolean validate() {
|
||||
@ -73,9 +81,15 @@ public class VelocityConfiguration {
|
||||
logger.info("Proxy is running in offline mode!");
|
||||
}
|
||||
|
||||
switch (ipForwardingMode) {
|
||||
switch (playerInfoForwardingMode) {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -124,9 +138,22 @@ public class VelocityConfiguration {
|
||||
logger.warn("ALL packets going through the proxy are going to be compressed. This may hurt performance.");
|
||||
}
|
||||
|
||||
loadFavicon();
|
||||
|
||||
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() {
|
||||
return AddressUtil.parseAddress(bind);
|
||||
}
|
||||
@ -162,8 +189,8 @@ public class VelocityConfiguration {
|
||||
return onlineMode;
|
||||
}
|
||||
|
||||
public IPForwardingMode getIpForwardingMode() {
|
||||
return ipForwardingMode;
|
||||
public PlayerInfoForwarding getPlayerInfoForwardingMode() {
|
||||
return playerInfoForwardingMode;
|
||||
}
|
||||
|
||||
public Map<String, String> getServers() {
|
||||
@ -182,6 +209,14 @@ public class VelocityConfiguration {
|
||||
return compressionLevel;
|
||||
}
|
||||
|
||||
public Favicon getFavicon() {
|
||||
return favicon;
|
||||
}
|
||||
|
||||
public byte[] getForwardingSecret() {
|
||||
return forwardingSecret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "VelocityConfiguration{" +
|
||||
@ -189,7 +224,7 @@ public class VelocityConfiguration {
|
||||
", motd='" + motd + '\'' +
|
||||
", showMaxPlayers=" + showMaxPlayers +
|
||||
", onlineMode=" + onlineMode +
|
||||
", ipForwardingMode=" + ipForwardingMode +
|
||||
", playerInfoForwardingMode=" + playerInfoForwardingMode +
|
||||
", servers=" + servers +
|
||||
", attemptConnectionOrder=" + attemptConnectionOrder +
|
||||
", compressionThreshold=" + compressionThreshold +
|
||||
@ -197,6 +232,8 @@ public class VelocityConfiguration {
|
||||
", queryEnabled=" + queryEnabled +
|
||||
", queryPort=" + queryPort +
|
||||
", 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(
|
||||
toml.getString("bind"),
|
||||
toml.getString("motd"),
|
||||
toml.getLong("show-max-players").intValue(),
|
||||
toml.getBoolean("online-mode"),
|
||||
IPForwardingMode.valueOf(toml.getString("ip-forwarding").toUpperCase()),
|
||||
toml.getString("bind", "0.0.0.0:25577"),
|
||||
toml.getString("motd", "&3A Velocity Server"),
|
||||
toml.getLong("show-max-players", 500L).intValue(),
|
||||
toml.getBoolean("online-mode", true),
|
||||
PlayerInfoForwarding.valueOf(toml.getString("player-info-forwarding", "MODERN").toUpperCase()),
|
||||
ImmutableMap.copyOf(servers),
|
||||
toml.getTable("servers").getList("try"),
|
||||
toml.getTable("advanced").getLong("compression-threshold", 1024L).intValue(),
|
||||
toml.getTable("advanced").getLong("compression-level", -1L).intValue(),
|
||||
toml.getTable("query").getBoolean("enabled"),
|
||||
toml.getTable("query").getLong("port", 25577L).intValue());
|
||||
toml.getTable("query").getBoolean("enabled", false),
|
||||
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.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.client.ClientPlaySessionHandler;
|
||||
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults;
|
||||
@ -17,6 +18,11 @@ import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
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.TimeUnit;
|
||||
|
||||
@ -30,7 +36,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
||||
|
||||
@Override
|
||||
public void activated() {
|
||||
if (VelocityServer.getServer().getConfiguration().getIpForwardingMode() == IPForwardingMode.MODERN) {
|
||||
if (VelocityServer.getServer().getConfiguration().getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN) {
|
||||
forwardingCheckTask = connection.getMinecraftConnection().getChannel().eventLoop().schedule(() -> {
|
||||
connection.getProxyPlayer().handleConnectionException(connection.getServerInfo(),
|
||||
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!");
|
||||
} else if (packet instanceof LoginPluginMessage) {
|
||||
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)) {
|
||||
LoginPluginResponse response = new LoginPluginResponse();
|
||||
response.setSuccess(true);
|
||||
response.setId(message.getId());
|
||||
response.setData(createForwardingData(connection.getProxyPlayer().getRemoteAddress().getHostString(),
|
||||
response.setData(createForwardingData(configuration.getForwardingSecret(),
|
||||
connection.getProxyPlayer().getRemoteAddress().getHostString(),
|
||||
connection.getProxyPlayer().getProfile()));
|
||||
connection.getMinecraftConnection().write(response);
|
||||
cancelForwardingCheck();
|
||||
@ -122,23 +130,43 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private static ByteBuf createForwardingData(String address, GameProfile profile) {
|
||||
ByteBuf buf = Unpooled.buffer();
|
||||
ProtocolUtils.writeString(buf, address);
|
||||
ProtocolUtils.writeUuid(buf, profile.idAsUuid());
|
||||
ProtocolUtils.writeString(buf, profile.getName());
|
||||
ProtocolUtils.writeVarInt(buf, profile.getProperties().size());
|
||||
static ByteBuf createForwardingData(byte[] hmacSecret, String address, GameProfile profile) {
|
||||
ByteBuf dataToForward = Unpooled.buffer();
|
||||
ByteBuf finalData = Unpooled.buffer();
|
||||
try {
|
||||
ProtocolUtils.writeString(dataToForward, address);
|
||||
ProtocolUtils.writeUuid(dataToForward, profile.idAsUuid());
|
||||
ProtocolUtils.writeString(dataToForward, profile.getName());
|
||||
ProtocolUtils.writeVarInt(dataToForward, profile.getProperties().size());
|
||||
for (GameProfile.Property property : profile.getProperties()) {
|
||||
ProtocolUtils.writeString(buf, property.getName());
|
||||
ProtocolUtils.writeString(buf, property.getValue());
|
||||
ProtocolUtils.writeString(dataToForward, property.getName());
|
||||
ProtocolUtils.writeString(dataToForward, property.getValue());
|
||||
String signature = property.getSignature();
|
||||
if (signature != null) {
|
||||
buf.writeBoolean(true);
|
||||
ProtocolUtils.writeString(buf, signature);
|
||||
dataToForward.writeBoolean(true);
|
||||
ProtocolUtils.writeString(dataToForward, signature);
|
||||
} else {
|
||||
buf.writeBoolean(false);
|
||||
dataToForward.writeBoolean(false);
|
||||
}
|
||||
}
|
||||
return buf;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
package com.velocitypowered.proxy.connection.backend;
|
||||
|
||||
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.util.ConnectionRequestResults;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolConstants;
|
||||
@ -97,7 +97,7 @@ public class ServerConnection implements MinecraftConnectionAssociation {
|
||||
Handshake handshake = new Handshake();
|
||||
handshake.setNextStatus(StateRegistry.LOGIN_ID);
|
||||
handshake.setProtocolVersion(proxyPlayer.getConnection().getProtocolVersion());
|
||||
if (VelocityServer.getServer().getConfiguration().getIpForwardingMode() == IPForwardingMode.LEGACY) {
|
||||
if (VelocityServer.getServer().getConfiguration().getPlayerInfoForwardingMode() == PlayerInfoForwarding.LEGACY) {
|
||||
handshake.setServerAddress(createBungeeForwardingAddress());
|
||||
} else {
|
||||
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.
|
||||
if (protocolVersion <= ProtocolConstants.MINECRAFT_1_12_2 ||
|
||||
VelocityServer.getServer().getConfiguration().getIpForwardingMode() != IPForwardingMode.MODERN) {
|
||||
VelocityServer.getServer().getConfiguration().getPlayerInfoForwardingMode() != PlayerInfoForwarding.MODERN) {
|
||||
ServerLogin login = new ServerLogin();
|
||||
login.setUsername(proxyPlayer.getUsername());
|
||||
minecraftConnection.write(login);
|
||||
|
@ -15,6 +15,7 @@ import net.kyori.text.TextComponent;
|
||||
import net.kyori.text.TranslatableComponent;
|
||||
import net.kyori.text.format.TextColor;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
public class HandshakeSessionHandler implements MinecraftSessionHandler {
|
||||
@ -50,6 +51,11 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
|
||||
connection.closeWith(Disconnect.create(TranslatableComponent.of("multiplayer.disconnect.outdated_client")));
|
||||
return;
|
||||
} 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));
|
||||
}
|
||||
break;
|
||||
|
@ -91,6 +91,11 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
||||
VelocityServer.getServer().getHttpClient()
|
||||
.get(new URL(String.format(MOJANG_SERVER_AUTH_URL, login.getUsername(), serverId, playerIp)))
|
||||
.thenAcceptAsync(profileResponse -> {
|
||||
if (inbound.isClosed()) {
|
||||
// The player disconnected after we authenticated them.
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
inbound.enableEncryption(decryptedSharedSecret);
|
||||
} catch (GeneralSecurityException e) {
|
||||
|
@ -41,7 +41,7 @@ public class StatusSessionHandler implements MinecraftSessionHandler {
|
||||
new ServerPing.Version(shownVersion, "Velocity " + ProtocolConstants.SUPPORTED_GENERIC_VERSION_STRING),
|
||||
new ServerPing.Players(VelocityServer.getServer().getPlayerCount(), configuration.getShowMaxPlayers()),
|
||||
configuration.getMotdComponent(),
|
||||
null
|
||||
configuration.getFavicon()
|
||||
);
|
||||
StatusResponse response = new StatusResponse();
|
||||
response.setStatus(VelocityServer.GSON.toJson(ping));
|
||||
|
@ -1,14 +1,15 @@
|
||||
package com.velocitypowered.proxy.data;
|
||||
|
||||
import com.velocitypowered.api.server.Favicon;
|
||||
import net.kyori.text.Component;
|
||||
|
||||
public class ServerPing {
|
||||
private final Version version;
|
||||
private final Players players;
|
||||
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.players = players;
|
||||
this.description = description;
|
||||
@ -27,7 +28,7 @@ public class ServerPing {
|
||||
return description;
|
||||
}
|
||||
|
||||
public String getFavicon() {
|
||||
public Favicon getFavicon() {
|
||||
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.
|
||||
# - "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.
|
||||
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]
|
||||
# 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