13
0
geforkt von Mirrors/Velocity

Merge branch 'master' into tab-complete

Dieser Commit ist enthalten in:
Andrew Steinborn 2018-08-09 09:47:30 -04:00
Commit 9a36bd6cae
20 geänderte Dateien mit 426 neuen und 87 gelöschten Zeilen

3
.gitignore vendored
Datei anzeigen

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

Datei anzeigen

@ -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.

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

@ -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) {

Datei anzeigen

@ -1,6 +1,6 @@
package com.velocitypowered.proxy.config;
public enum IPForwardingMode {
public enum PlayerInfoForwarding {
NONE,
LEGACY,
MODERN

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

@ -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) {

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

@ -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.

Datei anzeigen

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

Datei anzeigen

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