Update to latest Velocity #1
@ -148,4 +148,59 @@ public interface ProxyConfig {
|
|||||||
* @return read timeout (in milliseconds)
|
* @return read timeout (in milliseconds)
|
||||||
*/
|
*/
|
||||||
int getReadTimeout();
|
int getReadTimeout();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the rate limit for how fast a player can execute commands.
|
||||||
|
*
|
||||||
|
* @return the command rate limit (in milliseconds)
|
||||||
|
*/
|
||||||
|
int getCommandRatelimit();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get whether we should forward commands to the backend if the player is rate limited.
|
||||||
|
*
|
||||||
|
* @return whether to forward commands if rate limited
|
||||||
|
*/
|
||||||
|
boolean isForwardCommandsIfRateLimited();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the kick limit for commands that are rate limited.
|
||||||
|
* If this limit is 0 or less, the player will be not be kicked.
|
||||||
|
*
|
||||||
|
* @return the rate limited command rate limit
|
||||||
|
*/
|
||||||
|
int getKickAfterRateLimitedCommands();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get whether the proxy should kick players who are command rate limited.
|
||||||
|
*
|
||||||
|
* @return whether to kick players who are rate limited
|
||||||
|
*/
|
||||||
|
default boolean isKickOnCommandRateLimit() {
|
||||||
|
return getKickAfterRateLimitedCommands() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the rate limit for how fast a player can tab complete.
|
||||||
|
*
|
||||||
|
* @return the tab complete rate limit (in milliseconds)
|
||||||
|
*/
|
||||||
|
int getTabCompleteRatelimit();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the kick limit for tab completes that are rate limited.
|
||||||
|
* If this limit is 0 or less, the player will be not be kicked.
|
||||||
|
*
|
||||||
|
* @return the rate limited command rate limit
|
||||||
|
*/
|
||||||
|
int getKickAfterRateLimitedTabCompletes();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get whether the proxy should kick players who are tab complete rate limited.
|
||||||
|
*
|
||||||
|
* @return whether to kick players who are rate limited
|
||||||
|
*/
|
||||||
|
default boolean isKickOnTabCompleteRateLimit() {
|
||||||
|
return getKickAfterRateLimitedTabCompletes() > 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,6 @@ import static com.google.common.base.Preconditions.checkArgument;
|
|||||||
|
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.regex.Pattern;
|
|
||||||
import net.kyori.adventure.key.Key;
|
import net.kyori.adventure.key.Key;
|
||||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||||
|
|
||||||
@ -21,8 +20,6 @@ import org.checkerframework.checker.nullness.qual.Nullable;
|
|||||||
*/
|
*/
|
||||||
public final class MinecraftChannelIdentifier implements ChannelIdentifier {
|
public final class MinecraftChannelIdentifier implements ChannelIdentifier {
|
||||||
|
|
||||||
private static final Pattern VALID_IDENTIFIER_REGEX = Pattern.compile("[a-z0-9/\\-_]*");
|
|
||||||
|
|
||||||
private final String namespace;
|
private final String namespace;
|
||||||
private final String name;
|
private final String name;
|
||||||
|
|
||||||
@ -39,7 +36,7 @@ public final class MinecraftChannelIdentifier implements ChannelIdentifier {
|
|||||||
* @return a new channel identifier
|
* @return a new channel identifier
|
||||||
*/
|
*/
|
||||||
public static MinecraftChannelIdentifier forDefaultNamespace(String name) {
|
public static MinecraftChannelIdentifier forDefaultNamespace(String name) {
|
||||||
return new MinecraftChannelIdentifier("minecraft", name);
|
return new MinecraftChannelIdentifier(Key.MINECRAFT_NAMESPACE, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -52,14 +49,10 @@ public final class MinecraftChannelIdentifier implements ChannelIdentifier {
|
|||||||
public static MinecraftChannelIdentifier create(String namespace, String name) {
|
public static MinecraftChannelIdentifier create(String namespace, String name) {
|
||||||
checkArgument(!Strings.isNullOrEmpty(namespace), "namespace is null or empty");
|
checkArgument(!Strings.isNullOrEmpty(namespace), "namespace is null or empty");
|
||||||
checkArgument(name != null, "namespace is null or empty");
|
checkArgument(name != null, "namespace is null or empty");
|
||||||
checkArgument(VALID_IDENTIFIER_REGEX.matcher(namespace).matches(),
|
checkArgument(Key.parseableNamespace(namespace),
|
||||||
"namespace is not valid, must match: %s got %s",
|
"namespace is not valid, must match: [a-z0-9_.-] got %s", namespace);
|
||||||
VALID_IDENTIFIER_REGEX.toString(),
|
checkArgument(Key.parseableValue(name),
|
||||||
namespace);
|
"name is not valid, must match: [a-z0-9/._-] got %s", name);
|
||||||
checkArgument(VALID_IDENTIFIER_REGEX.matcher(name).matches(),
|
|
||||||
"name is not valid, must match: %s got %s",
|
|
||||||
VALID_IDENTIFIER_REGEX.toString(),
|
|
||||||
name);
|
|
||||||
return new MinecraftChannelIdentifier(namespace, name);
|
return new MinecraftChannelIdentifier(namespace, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,10 +65,9 @@ public final class MinecraftChannelIdentifier implements ChannelIdentifier {
|
|||||||
public static MinecraftChannelIdentifier from(String identifier) {
|
public static MinecraftChannelIdentifier from(String identifier) {
|
||||||
int colonPos = identifier.indexOf(':');
|
int colonPos = identifier.indexOf(':');
|
||||||
if (colonPos == -1) {
|
if (colonPos == -1) {
|
||||||
throw new IllegalArgumentException("Identifier does not contain a colon.");
|
return create(Key.MINECRAFT_NAMESPACE, identifier);
|
||||||
}
|
} else if (colonPos == 0) {
|
||||||
if (colonPos + 1 == identifier.length()) {
|
return create(Key.MINECRAFT_NAMESPACE, identifier.substring(1));
|
||||||
throw new IllegalArgumentException("Identifier is empty.");
|
|
||||||
}
|
}
|
||||||
String namespace = identifier.substring(0, colonPos);
|
String namespace = identifier.substring(0, colonPos);
|
||||||
String name = identifier.substring(colonPos + 1);
|
String name = identifier.substring(colonPos + 1);
|
||||||
|
|||||||
@ -517,6 +517,10 @@ public final class ServerPing {
|
|||||||
*/
|
*/
|
||||||
public static final class SamplePlayer {
|
public static final class SamplePlayer {
|
||||||
|
|
||||||
|
public static final SamplePlayer ANONYMOUS = new SamplePlayer(
|
||||||
|
"Anonymous Player",
|
||||||
|
new UUID(0L, 0L)
|
||||||
|
);
|
||||||
private final String name;
|
private final String name;
|
||||||
private final UUID id;
|
private final UUID id;
|
||||||
|
|
||||||
|
|||||||
@ -47,17 +47,25 @@ class MinecraftChannelIdentifierTest {
|
|||||||
create("velocity", "test/test2");
|
create("velocity", "test/test2");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fromIdentifierDefaultNamespace() {
|
||||||
|
assertEquals("minecraft", from("test").getNamespace());
|
||||||
|
assertEquals("minecraft", from(":test").getNamespace());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fromIdentifierAllowsEmptyName() {
|
||||||
|
from("minecraft:");
|
||||||
|
from(":");
|
||||||
|
from("");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void fromIdentifierThrowsOnBadValues() {
|
void fromIdentifierThrowsOnBadValues() {
|
||||||
assertAll(
|
assertAll(
|
||||||
() -> assertThrows(IllegalArgumentException.class, () -> from("")),
|
|
||||||
() -> assertThrows(IllegalArgumentException.class, () -> from(":")),
|
|
||||||
() -> assertThrows(IllegalArgumentException.class, () -> from(":a")),
|
|
||||||
() -> assertThrows(IllegalArgumentException.class, () -> from("a:")),
|
|
||||||
() -> assertThrows(IllegalArgumentException.class, () -> from("hello:$$$$$$")),
|
() -> assertThrows(IllegalArgumentException.class, () -> from("hello:$$$$$$")),
|
||||||
|
() -> assertThrows(IllegalArgumentException.class, () -> from("he/llo:wor/ld")),
|
||||||
() -> assertThrows(IllegalArgumentException.class, () -> from("hello::"))
|
() -> assertThrows(IllegalArgumentException.class, () -> from("hello::"))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ configurate3 = "3.7.3"
|
|||||||
configurate4 = "4.1.2"
|
configurate4 = "4.1.2"
|
||||||
flare = "2.0.1"
|
flare = "2.0.1"
|
||||||
log4j = "2.24.1"
|
log4j = "2.24.1"
|
||||||
netty = "4.1.119.Final"
|
netty = "4.2.0.Final"
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
indra-publishing = "net.kyori.indra.publishing:2.0.6"
|
indra-publishing = "net.kyori.indra.publishing:2.0.6"
|
||||||
@ -53,8 +53,8 @@ netty-codec-haproxy = { module = "io.netty:netty-codec-haproxy", version.ref = "
|
|||||||
netty-codec-http = { module = "io.netty:netty-codec-http", version.ref = "netty" }
|
netty-codec-http = { module = "io.netty:netty-codec-http", version.ref = "netty" }
|
||||||
netty-handler = { module = "io.netty:netty-handler", version.ref = "netty" }
|
netty-handler = { module = "io.netty:netty-handler", version.ref = "netty" }
|
||||||
netty-transport-native-epoll = { module = "io.netty:netty-transport-native-epoll", version.ref = "netty" }
|
netty-transport-native-epoll = { module = "io.netty:netty-transport-native-epoll", version.ref = "netty" }
|
||||||
netty-transport-native-iouring = { module = "io.netty.incubator:netty-incubator-transport-native-io_uring", version = "0.0.25.Final" }
|
|
||||||
netty-transport-native-kqueue = { module = "io.netty:netty-transport-native-kqueue", version.ref = "netty" }
|
netty-transport-native-kqueue = { module = "io.netty:netty-transport-native-kqueue", version.ref = "netty" }
|
||||||
|
netty-transport-native-iouring = { module = "io.netty:netty-transport-native-io_uring", version.ref = "netty" }
|
||||||
nightconfig = "com.electronwill.night-config:toml:3.6.7"
|
nightconfig = "com.electronwill.night-config:toml:3.6.7"
|
||||||
slf4j = "org.slf4j:slf4j-api:2.0.12"
|
slf4j = "org.slf4j:slf4j-api:2.0.12"
|
||||||
snakeyaml = "org.yaml:snakeyaml:1.33"
|
snakeyaml = "org.yaml:snakeyaml:1.33"
|
||||||
|
|||||||
@ -75,6 +75,7 @@ import io.netty.channel.ChannelInitializer;
|
|||||||
import io.netty.channel.EventLoopGroup;
|
import io.netty.channel.EventLoopGroup;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.net.InetAddress;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@ -162,7 +163,9 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
|
|||||||
private final Map<UUID, ConnectedPlayer> connectionsByUuid = new ConcurrentHashMap<>();
|
private final Map<UUID, ConnectedPlayer> connectionsByUuid = new ConcurrentHashMap<>();
|
||||||
private final Map<String, ConnectedPlayer> connectionsByName = new ConcurrentHashMap<>();
|
private final Map<String, ConnectedPlayer> connectionsByName = new ConcurrentHashMap<>();
|
||||||
private final VelocityConsole console;
|
private final VelocityConsole console;
|
||||||
private @MonotonicNonNull Ratelimiter ipAttemptLimiter;
|
private @MonotonicNonNull Ratelimiter<InetAddress> ipAttemptLimiter;
|
||||||
|
private @MonotonicNonNull Ratelimiter<UUID> commandRateLimiter;
|
||||||
|
private @MonotonicNonNull Ratelimiter<UUID> tabCompleteRateLimiter;
|
||||||
private final VelocityEventManager eventManager;
|
private final VelocityEventManager eventManager;
|
||||||
private final VelocityScheduler scheduler;
|
private final VelocityScheduler scheduler;
|
||||||
private final VelocityChannelRegistrar channelRegistrar = new VelocityChannelRegistrar();
|
private final VelocityChannelRegistrar channelRegistrar = new VelocityChannelRegistrar();
|
||||||
@ -295,6 +298,8 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ipAttemptLimiter = Ratelimiters.createWithMilliseconds(configuration.getLoginRatelimit());
|
ipAttemptLimiter = Ratelimiters.createWithMilliseconds(configuration.getLoginRatelimit());
|
||||||
|
commandRateLimiter = Ratelimiters.createWithMilliseconds(configuration.getCommandRatelimit());
|
||||||
|
tabCompleteRateLimiter = Ratelimiters.createWithMilliseconds(configuration.getTabCompleteRatelimit());
|
||||||
loadPlugins();
|
loadPlugins();
|
||||||
|
|
||||||
// Go ahead and fire the proxy initialization event. We block since plugins should have a chance
|
// Go ahead and fire the proxy initialization event. We block since plugins should have a chance
|
||||||
@ -654,10 +659,18 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
|
|||||||
return cm.createHttpClient();
|
return cm.createHttpClient();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Ratelimiter getIpAttemptLimiter() {
|
public @MonotonicNonNull Ratelimiter<InetAddress> getIpAttemptLimiter() {
|
||||||
return ipAttemptLimiter;
|
return ipAttemptLimiter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public @MonotonicNonNull Ratelimiter<UUID> getCommandRateLimiter() {
|
||||||
|
return commandRateLimiter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @MonotonicNonNull Ratelimiter<UUID> getTabCompleteRateLimiter() {
|
||||||
|
return tabCompleteRateLimiter;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the {@code connection} can be registered with the proxy.
|
* Checks if the {@code connection} can be registered with the proxy.
|
||||||
*
|
*
|
||||||
|
|||||||
@ -78,6 +78,8 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
private boolean onlineModeKickExistingPlayers = false;
|
private boolean onlineModeKickExistingPlayers = false;
|
||||||
@Expose
|
@Expose
|
||||||
private PingPassthroughMode pingPassthrough = PingPassthroughMode.DISABLED;
|
private PingPassthroughMode pingPassthrough = PingPassthroughMode.DISABLED;
|
||||||
|
@Expose
|
||||||
|
private boolean samplePlayersInPing = false;
|
||||||
private final Servers servers;
|
private final Servers servers;
|
||||||
private final ForcedHosts forcedHosts;
|
private final ForcedHosts forcedHosts;
|
||||||
@Expose
|
@Expose
|
||||||
@ -105,8 +107,9 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
boolean preventClientProxyConnections, boolean announceForge,
|
boolean preventClientProxyConnections, boolean announceForge,
|
||||||
PlayerInfoForwarding playerInfoForwardingMode, byte[] forwardingSecret,
|
PlayerInfoForwarding playerInfoForwardingMode, byte[] forwardingSecret,
|
||||||
boolean onlineModeKickExistingPlayers, PingPassthroughMode pingPassthrough,
|
boolean onlineModeKickExistingPlayers, PingPassthroughMode pingPassthrough,
|
||||||
boolean enablePlayerAddressLogging, Servers servers, ForcedHosts forcedHosts,
|
boolean samplePlayersInPing, boolean enablePlayerAddressLogging, Servers servers,
|
||||||
Advanced advanced, Query query, Metrics metrics, boolean forceKeyAuthentication) {
|
ForcedHosts forcedHosts, Advanced advanced, Query query, Metrics metrics,
|
||||||
|
boolean forceKeyAuthentication) {
|
||||||
this.bind = bind;
|
this.bind = bind;
|
||||||
this.motd = motd;
|
this.motd = motd;
|
||||||
this.showMaxPlayers = showMaxPlayers;
|
this.showMaxPlayers = showMaxPlayers;
|
||||||
@ -117,6 +120,7 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
this.forwardingSecret = forwardingSecret;
|
this.forwardingSecret = forwardingSecret;
|
||||||
this.onlineModeKickExistingPlayers = onlineModeKickExistingPlayers;
|
this.onlineModeKickExistingPlayers = onlineModeKickExistingPlayers;
|
||||||
this.pingPassthrough = pingPassthrough;
|
this.pingPassthrough = pingPassthrough;
|
||||||
|
this.samplePlayersInPing = samplePlayersInPing;
|
||||||
this.enablePlayerAddressLogging = enablePlayerAddressLogging;
|
this.enablePlayerAddressLogging = enablePlayerAddressLogging;
|
||||||
this.servers = servers;
|
this.servers = servers;
|
||||||
this.forcedHosts = forcedHosts;
|
this.forcedHosts = forcedHosts;
|
||||||
@ -230,6 +234,11 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
valid = false;
|
valid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (advanced.commandRateLimit < 0) {
|
||||||
|
logger.error("Invalid command rate limit {}", advanced.commandRateLimit);
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
|
||||||
loadFavicon();
|
loadFavicon();
|
||||||
|
|
||||||
return valid;
|
return valid;
|
||||||
@ -351,6 +360,31 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
return advanced.getReadTimeout();
|
return advanced.getReadTimeout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCommandRatelimit() {
|
||||||
|
return advanced.getCommandRateLimit();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getTabCompleteRatelimit() {
|
||||||
|
return advanced.getTabCompleteRateLimit();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getKickAfterRateLimitedTabCompletes() {
|
||||||
|
return advanced.getKickAfterRateLimitedTabCompletes();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isForwardCommandsIfRateLimited() {
|
||||||
|
return advanced.isForwardCommandsIfRateLimited();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getKickAfterRateLimitedCommands() {
|
||||||
|
return advanced.getKickAfterRateLimitedCommands();
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isProxyProtocol() {
|
public boolean isProxyProtocol() {
|
||||||
return advanced.isProxyProtocol();
|
return advanced.isProxyProtocol();
|
||||||
}
|
}
|
||||||
@ -371,6 +405,10 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
return pingPassthrough;
|
return pingPassthrough;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean getSamplePlayersInPing() {
|
||||||
|
return samplePlayersInPing;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isPlayerAddressLoggingEnabled() {
|
public boolean isPlayerAddressLoggingEnabled() {
|
||||||
return enablePlayerAddressLogging;
|
return enablePlayerAddressLogging;
|
||||||
}
|
}
|
||||||
@ -507,6 +545,8 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
final PingPassthroughMode pingPassthroughMode = config.getEnumOrElse("ping-passthrough",
|
final PingPassthroughMode pingPassthroughMode = config.getEnumOrElse("ping-passthrough",
|
||||||
PingPassthroughMode.DISABLED);
|
PingPassthroughMode.DISABLED);
|
||||||
|
|
||||||
|
final boolean samplePlayersInPing = config.getOrElse("sample-players-in-ping", false);
|
||||||
|
|
||||||
final String bind = config.getOrElse("bind", "0.0.0.0:25565");
|
final String bind = config.getOrElse("bind", "0.0.0.0:25565");
|
||||||
final int maxPlayers = config.getIntOrElse("show-max-players", 500);
|
final int maxPlayers = config.getIntOrElse("show-max-players", 500);
|
||||||
final boolean onlineMode = config.getOrElse("online-mode", true);
|
final boolean onlineMode = config.getOrElse("online-mode", true);
|
||||||
@ -537,6 +577,7 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
forwardingSecret,
|
forwardingSecret,
|
||||||
kickExisting,
|
kickExisting,
|
||||||
pingPassthroughMode,
|
pingPassthroughMode,
|
||||||
|
samplePlayersInPing,
|
||||||
enablePlayerAddressLogging,
|
enablePlayerAddressLogging,
|
||||||
new Servers(serversConfig),
|
new Servers(serversConfig),
|
||||||
new ForcedHosts(forcedHostsConfig),
|
new ForcedHosts(forcedHostsConfig),
|
||||||
@ -722,6 +763,16 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
private boolean acceptTransfers = false;
|
private boolean acceptTransfers = false;
|
||||||
@Expose
|
@Expose
|
||||||
private boolean enableReusePort = false;
|
private boolean enableReusePort = false;
|
||||||
|
@Expose
|
||||||
|
private int commandRateLimit = 50;
|
||||||
|
@Expose
|
||||||
|
private boolean forwardCommandsIfRateLimited = true;
|
||||||
|
@Expose
|
||||||
|
private int kickAfterRateLimitedCommands = 5;
|
||||||
|
@Expose
|
||||||
|
private int tabCompleteRateLimit = 50;
|
||||||
|
@Expose
|
||||||
|
private int kickAfterRateLimitedTabCompletes = 10;
|
||||||
|
|
||||||
private Advanced() {
|
private Advanced() {
|
||||||
}
|
}
|
||||||
@ -748,6 +799,11 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
this.logPlayerConnections = config.getOrElse("log-player-connections", true);
|
this.logPlayerConnections = config.getOrElse("log-player-connections", true);
|
||||||
this.acceptTransfers = config.getOrElse("accepts-transfers", false);
|
this.acceptTransfers = config.getOrElse("accepts-transfers", false);
|
||||||
this.enableReusePort = config.getOrElse("enable-reuse-port", false);
|
this.enableReusePort = config.getOrElse("enable-reuse-port", false);
|
||||||
|
this.commandRateLimit = config.getIntOrElse("command-rate-limit", 25);
|
||||||
|
this.forwardCommandsIfRateLimited = config.getOrElse("forward-commands-if-rate-limited", true);
|
||||||
|
this.kickAfterRateLimitedCommands = config.getIntOrElse("kick-after-rate-limited-commands", 0);
|
||||||
|
this.tabCompleteRateLimit = config.getIntOrElse("tab-complete-rate-limit", 10); // very lenient
|
||||||
|
this.kickAfterRateLimitedTabCompletes = config.getIntOrElse("kick-after-rate-limited-tab-completes", 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -815,6 +871,26 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
return enableReusePort;
|
return enableReusePort;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getCommandRateLimit() {
|
||||||
|
return commandRateLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isForwardCommandsIfRateLimited() {
|
||||||
|
return forwardCommandsIfRateLimited;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getKickAfterRateLimitedCommands() {
|
||||||
|
return kickAfterRateLimitedCommands;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTabCompleteRateLimit() {
|
||||||
|
return tabCompleteRateLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getKickAfterRateLimitedTabCompletes() {
|
||||||
|
return kickAfterRateLimitedTabCompletes;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "Advanced{"
|
return "Advanced{"
|
||||||
|
|||||||
@ -84,6 +84,7 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
|||||||
private static final Logger logger = LogManager.getLogger(MinecraftConnection.class);
|
private static final Logger logger = LogManager.getLogger(MinecraftConnection.class);
|
||||||
|
|
||||||
private final Channel channel;
|
private final Channel channel;
|
||||||
|
public boolean pendingConfigurationSwitch = false;
|
||||||
private SocketAddress remoteAddress;
|
private SocketAddress remoteAddress;
|
||||||
private StateRegistry state;
|
private StateRegistry state;
|
||||||
private Map<StateRegistry, MinecraftSessionHandler> sessionHandlers;
|
private Map<StateRegistry, MinecraftSessionHandler> sessionHandlers;
|
||||||
|
|||||||
@ -20,6 +20,7 @@ package com.velocitypowered.proxy.connection.backend;
|
|||||||
import com.velocitypowered.api.network.ProtocolVersion;
|
import com.velocitypowered.api.network.ProtocolVersion;
|
||||||
import com.velocitypowered.api.proxy.Player;
|
import com.velocitypowered.api.proxy.Player;
|
||||||
import com.velocitypowered.api.proxy.ServerConnection;
|
import com.velocitypowered.api.proxy.ServerConnection;
|
||||||
|
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
||||||
import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier;
|
import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier;
|
||||||
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
|
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
|
||||||
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||||
@ -316,9 +317,9 @@ public class BungeeCordMessageResponder {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static String getBungeeCordChannel(ProtocolVersion version) {
|
static ChannelIdentifier getBungeeCordChannel(ProtocolVersion version) {
|
||||||
return version.noLessThan(ProtocolVersion.MINECRAFT_1_13) ? MODERN_CHANNEL.getId()
|
return version.noLessThan(ProtocolVersion.MINECRAFT_1_13) ? MODERN_CHANNEL
|
||||||
: LEGACY_CHANNEL.getId();
|
: LEGACY_CHANNEL;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: this method will always release the buffer!
|
// Note: this method will always release the buffer!
|
||||||
@ -329,8 +330,8 @@ public class BungeeCordMessageResponder {
|
|||||||
// Note: this method will always release the buffer!
|
// Note: this method will always release the buffer!
|
||||||
private static void sendServerResponse(ConnectedPlayer player, ByteBuf buf) {
|
private static void sendServerResponse(ConnectedPlayer player, ByteBuf buf) {
|
||||||
MinecraftConnection serverConnection = player.ensureAndGetCurrentServer().ensureConnected();
|
MinecraftConnection serverConnection = player.ensureAndGetCurrentServer().ensureConnected();
|
||||||
String chan = getBungeeCordChannel(serverConnection.getProtocolVersion());
|
ChannelIdentifier chan = getBungeeCordChannel(serverConnection.getProtocolVersion());
|
||||||
PluginMessagePacket msg = new PluginMessagePacket(chan, buf);
|
PluginMessagePacket msg = new PluginMessagePacket(chan.getId(), buf);
|
||||||
serverConnection.write(msg);
|
serverConnection.write(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -74,6 +74,7 @@ import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdatePacket;
|
|||||||
import com.velocitypowered.proxy.protocol.packet.title.GenericTitlePacket;
|
import com.velocitypowered.proxy.protocol.packet.title.GenericTitlePacket;
|
||||||
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
|
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
|
||||||
import com.velocitypowered.proxy.util.CharacterUtil;
|
import com.velocitypowered.proxy.util.CharacterUtil;
|
||||||
|
import com.velocitypowered.proxy.util.except.QuietRuntimeException;
|
||||||
import io.netty.buffer.ByteBuf;
|
import io.netty.buffer.ByteBuf;
|
||||||
import io.netty.buffer.ByteBufUtil;
|
import io.netty.buffer.ByteBufUtil;
|
||||||
import io.netty.buffer.Unpooled;
|
import io.netty.buffer.Unpooled;
|
||||||
@ -113,6 +114,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
|
|
||||||
private CompletableFuture<Void> configSwitchFuture;
|
private CompletableFuture<Void> configSwitchFuture;
|
||||||
|
|
||||||
|
private int failedTabCompleteAttempts;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a client play session handler.
|
* Constructs a client play session handler.
|
||||||
*
|
*
|
||||||
@ -160,7 +163,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
@Override
|
@Override
|
||||||
public void activated() {
|
public void activated() {
|
||||||
configSwitchFuture = new CompletableFuture<>();
|
configSwitchFuture = new CompletableFuture<>();
|
||||||
Collection<String> channels =
|
Collection<ChannelIdentifier> channels =
|
||||||
server.getChannelRegistrar().getChannelsForProtocol(player.getProtocolVersion());
|
server.getChannelRegistrar().getChannelsForProtocol(player.getProtocolVersion());
|
||||||
if (!channels.isEmpty()) {
|
if (!channels.isEmpty()) {
|
||||||
PluginMessagePacket register = constructChannelsPacket(player.getProtocolVersion(), channels);
|
PluginMessagePacket register = constructChannelsPacket(player.getProtocolVersion(), channels);
|
||||||
@ -308,20 +311,17 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
logger.warn("A plugin message was received while the backend server was not "
|
logger.warn("A plugin message was received while the backend server was not "
|
||||||
+ "ready. Channel: {}. Packet discarded.", packet.getChannel());
|
+ "ready. Channel: {}. Packet discarded.", packet.getChannel());
|
||||||
} else if (PluginMessageUtil.isRegister(packet)) {
|
} else if (PluginMessageUtil.isRegister(packet)) {
|
||||||
List<String> channels = PluginMessageUtil.getChannels(packet);
|
List<ChannelIdentifier> channels =
|
||||||
List<ChannelIdentifier> channelIdentifiers = new ArrayList<>();
|
PluginMessageUtil.getChannels(this.player.getClientsideChannels().size(), packet,
|
||||||
for (String channel : channels) {
|
this.player.getProtocolVersion());
|
||||||
try {
|
player.getClientsideChannels().addAll(channels);
|
||||||
channelIdentifiers.add(MinecraftChannelIdentifier.from(channel));
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
channelIdentifiers.add(new LegacyChannelIdentifier(channel));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
server.getEventManager()
|
server.getEventManager()
|
||||||
.fireAndForget(
|
.fireAndForget(
|
||||||
new PlayerChannelRegisterEvent(player, ImmutableList.copyOf(channelIdentifiers)));
|
new PlayerChannelRegisterEvent(player, ImmutableList.copyOf(channels)));
|
||||||
backendConn.write(packet.retain());
|
backendConn.write(packet.retain());
|
||||||
} else if (PluginMessageUtil.isUnregister(packet)) {
|
} else if (PluginMessageUtil.isUnregister(packet)) {
|
||||||
|
player.getClientsideChannels()
|
||||||
|
.removeAll(PluginMessageUtil.getChannels(0, packet, this.player.getProtocolVersion()));
|
||||||
backendConn.write(packet.retain());
|
backendConn.write(packet.retain());
|
||||||
} else if (PluginMessageUtil.isMcBrand(packet)) {
|
} else if (PluginMessageUtil.isMcBrand(packet)) {
|
||||||
String brand = PluginMessageUtil.readBrandMessage(packet.content());
|
String brand = PluginMessageUtil.readBrandMessage(packet.content());
|
||||||
@ -377,10 +377,14 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean handle(FinishedUpdatePacket packet) {
|
public boolean handle(FinishedUpdatePacket packet) {
|
||||||
|
if (!player.getConnection().pendingConfigurationSwitch) {
|
||||||
|
throw new QuietRuntimeException("Not expecting reconfiguration");
|
||||||
|
}
|
||||||
// Complete client switch
|
// Complete client switch
|
||||||
player.getConnection().setActiveSessionHandler(StateRegistry.CONFIG);
|
player.getConnection().setActiveSessionHandler(StateRegistry.CONFIG);
|
||||||
VelocityServerConnection serverConnection = player.getConnectedServer();
|
VelocityServerConnection serverConnection = player.getConnectedServer();
|
||||||
server.getEventManager().fireAndForget(new PlayerEnteredConfigurationEvent(player, serverConnection));
|
server.getEventManager()
|
||||||
|
.fireAndForget(new PlayerEnteredConfigurationEvent(player, serverConnection));
|
||||||
if (serverConnection != null) {
|
if (serverConnection != null) {
|
||||||
MinecraftConnection smc = serverConnection.ensureConnected();
|
MinecraftConnection smc = serverConnection.ensureConnected();
|
||||||
CompletableFuture.runAsync(() -> {
|
CompletableFuture.runAsync(() -> {
|
||||||
@ -567,11 +571,15 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
|
|
||||||
// Tell the server about the proxy's plugin message channels.
|
// Tell the server about the proxy's plugin message channels.
|
||||||
ProtocolVersion serverVersion = serverMc.getProtocolVersion();
|
ProtocolVersion serverVersion = serverMc.getProtocolVersion();
|
||||||
final Collection<String> channels = server.getChannelRegistrar()
|
final Collection<ChannelIdentifier> channels = server.getChannelRegistrar()
|
||||||
.getChannelsForProtocol(serverMc.getProtocolVersion());
|
.getChannelsForProtocol(serverMc.getProtocolVersion());
|
||||||
if (!channels.isEmpty()) {
|
if (!channels.isEmpty()) {
|
||||||
serverMc.delayedWrite(constructChannelsPacket(serverVersion, channels));
|
serverMc.delayedWrite(constructChannelsPacket(serverVersion, channels));
|
||||||
}
|
}
|
||||||
|
// Tell the server about this client's plugin message channels.
|
||||||
|
if (!player.getClientsideChannels().isEmpty()) {
|
||||||
|
serverMc.delayedWrite(constructChannelsPacket(serverVersion, player.getClientsideChannels()));
|
||||||
|
}
|
||||||
|
|
||||||
// If we had plugin messages queued during login/FML handshake, send them now.
|
// If we had plugin messages queued during login/FML handshake, send them now.
|
||||||
PluginMessagePacket pm;
|
PluginMessagePacket pm;
|
||||||
@ -653,6 +661,17 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!server.getTabCompleteRateLimiter().attempt(player.getUniqueId())) {
|
||||||
|
if (server.getConfiguration().isKickOnTabCompleteRateLimit()
|
||||||
|
&& failedTabCompleteAttempts++ >= server.getConfiguration().getKickAfterRateLimitedTabCompletes()) {
|
||||||
|
player.disconnect(Component.translatable("velocity.kick.tab-complete-rate-limit"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
failedTabCompleteAttempts = 0;
|
||||||
|
|
||||||
server.getCommandManager().offerBrigadierSuggestions(player, command)
|
server.getCommandManager().offerBrigadierSuggestions(player, command)
|
||||||
.thenAcceptAsync(suggestions -> {
|
.thenAcceptAsync(suggestions -> {
|
||||||
if (suggestions.isEmpty()) {
|
if (suggestions.isEmpty()) {
|
||||||
|
|||||||
@ -99,6 +99,7 @@ import com.velocitypowered.proxy.tablist.VelocityTabListLegacy;
|
|||||||
import com.velocitypowered.proxy.util.ClosestLocaleMatcher;
|
import com.velocitypowered.proxy.util.ClosestLocaleMatcher;
|
||||||
import com.velocitypowered.proxy.util.DurationUtils;
|
import com.velocitypowered.proxy.util.DurationUtils;
|
||||||
import com.velocitypowered.proxy.util.TranslatableMapper;
|
import com.velocitypowered.proxy.util.TranslatableMapper;
|
||||||
|
import com.velocitypowered.proxy.util.collect.CappedSet;
|
||||||
import io.netty.buffer.ByteBuf;
|
import io.netty.buffer.ByteBuf;
|
||||||
import io.netty.buffer.Unpooled;
|
import io.netty.buffer.Unpooled;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
@ -144,6 +145,7 @@ import org.jetbrains.annotations.NotNull;
|
|||||||
public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, KeyIdentifiable,
|
public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, KeyIdentifiable,
|
||||||
VelocityInboundConnection {
|
VelocityInboundConnection {
|
||||||
|
|
||||||
|
public static final int MAX_CLIENTSIDE_PLUGIN_CHANNELS = 1024;
|
||||||
private static final PlainTextComponentSerializer PASS_THRU_TRANSLATE =
|
private static final PlainTextComponentSerializer PASS_THRU_TRANSLATE =
|
||||||
PlainTextComponentSerializer.builder().flattener(TranslatableMapper.FLATTENER).build();
|
PlainTextComponentSerializer.builder().flattener(TranslatableMapper.FLATTENER).build();
|
||||||
static final PermissionProvider DEFAULT_PERMISSIONS = s -> PermissionFunction.ALWAYS_UNDEFINED;
|
static final PermissionProvider DEFAULT_PERMISSIONS = s -> PermissionFunction.ALWAYS_UNDEFINED;
|
||||||
@ -173,6 +175,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
|||||||
private final InternalTabList tabList;
|
private final InternalTabList tabList;
|
||||||
private final VelocityServer server;
|
private final VelocityServer server;
|
||||||
private ClientConnectionPhase connectionPhase;
|
private ClientConnectionPhase connectionPhase;
|
||||||
|
private final Collection<ChannelIdentifier> clientsideChannels;
|
||||||
private final CompletableFuture<Void> teardownFuture = new CompletableFuture<>();
|
private final CompletableFuture<Void> teardownFuture = new CompletableFuture<>();
|
||||||
private @MonotonicNonNull List<String> serversToTry = null;
|
private @MonotonicNonNull List<String> serversToTry = null;
|
||||||
private final ResourcePackHandler resourcePackHandler;
|
private final ResourcePackHandler resourcePackHandler;
|
||||||
@ -205,6 +208,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
|||||||
this.permissionFunction = PermissionFunction.ALWAYS_UNDEFINED;
|
this.permissionFunction = PermissionFunction.ALWAYS_UNDEFINED;
|
||||||
this.connectionPhase = connection.getType().getInitialClientPhase();
|
this.connectionPhase = connection.getType().getInitialClientPhase();
|
||||||
this.onlineMode = onlineMode;
|
this.onlineMode = onlineMode;
|
||||||
|
this.clientsideChannels = CappedSet.create(MAX_CLIENTSIDE_PLUGIN_CHANNELS);
|
||||||
|
|
||||||
if (connection.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_19_3)) {
|
if (connection.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_19_3)) {
|
||||||
this.tabList = new VelocityTabList(this);
|
this.tabList = new VelocityTabList(this);
|
||||||
@ -1314,6 +1318,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
|||||||
connection.write(BundleDelimiterPacket.INSTANCE);
|
connection.write(BundleDelimiterPacket.INSTANCE);
|
||||||
}
|
}
|
||||||
connection.write(StartUpdatePacket.INSTANCE);
|
connection.write(StartUpdatePacket.INSTANCE);
|
||||||
|
connection.pendingConfigurationSwitch = true;
|
||||||
connection.getChannel().pipeline().get(MinecraftEncoder.class).setState(StateRegistry.CONFIG);
|
connection.getChannel().pipeline().get(MinecraftEncoder.class).setState(StateRegistry.CONFIG);
|
||||||
// Make sure we don't send any play packets to the player after update start
|
// Make sure we don't send any play packets to the player after update start
|
||||||
connection.addPlayPacketQueueHandler();
|
connection.addPlayPacketQueueHandler();
|
||||||
@ -1342,6 +1347,15 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
|||||||
this.connectionPhase = connectionPhase;
|
this.connectionPhase = connectionPhase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all the plugin message channels that registered by client.
|
||||||
|
*
|
||||||
|
* @return the channels
|
||||||
|
*/
|
||||||
|
public Collection<ChannelIdentifier> getClientsideChannels() {
|
||||||
|
return clientsideChannels;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @Nullable IdentifiedKey getIdentifiedKey() {
|
public @Nullable IdentifiedKey getIdentifiedKey() {
|
||||||
return playerKey;
|
return playerKey;
|
||||||
|
|||||||
@ -30,10 +30,12 @@ import com.velocitypowered.proxy.config.VelocityConfiguration;
|
|||||||
import com.velocitypowered.proxy.server.VelocityRegisteredServer;
|
import com.velocitypowered.proxy.server.VelocityRegisteredServer;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Common utilities for handling server list ping results.
|
* Common utilities for handling server list ping results.
|
||||||
@ -51,11 +53,27 @@ public class ServerListPingHandler {
|
|||||||
version = ProtocolVersion.MAXIMUM_VERSION;
|
version = ProtocolVersion.MAXIMUM_VERSION;
|
||||||
}
|
}
|
||||||
VelocityConfiguration configuration = server.getConfiguration();
|
VelocityConfiguration configuration = server.getConfiguration();
|
||||||
|
List<ServerPing.SamplePlayer> samplePlayers;
|
||||||
|
if (configuration.getSamplePlayersInPing()) {
|
||||||
|
List<ServerPing.SamplePlayer> unshuffledPlayers = server.getAllPlayers().stream()
|
||||||
|
.map(p -> {
|
||||||
|
if (p.getPlayerSettings().isClientListingAllowed()) {
|
||||||
|
return new ServerPing.SamplePlayer(p.getUsername(), p.getUniqueId());
|
||||||
|
} else {
|
||||||
|
return ServerPing.SamplePlayer.ANONYMOUS;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
Collections.shuffle(unshuffledPlayers);
|
||||||
|
samplePlayers = unshuffledPlayers.subList(0, Math.min(12, server.getPlayerCount()));
|
||||||
|
} else {
|
||||||
|
samplePlayers = ImmutableList.of();
|
||||||
|
}
|
||||||
return new ServerPing(
|
return new ServerPing(
|
||||||
new ServerPing.Version(version.getProtocol(),
|
new ServerPing.Version(version.getProtocol(),
|
||||||
"Velocity " + ProtocolVersion.SUPPORTED_VERSION_STRING),
|
"Velocity " + ProtocolVersion.SUPPORTED_VERSION_STRING),
|
||||||
new ServerPing.Players(server.getPlayerCount(), configuration.getShowMaxPlayers(),
|
new ServerPing.Players(server.getPlayerCount(), configuration.getShowMaxPlayers(),
|
||||||
ImmutableList.of()),
|
samplePlayers),
|
||||||
configuration.getMotd(),
|
configuration.getMotd(),
|
||||||
configuration.getFavicon().orElse(null),
|
configuration.getFavicon().orElse(null),
|
||||||
configuration.isAnnounceForge() ? ModInfo.DEFAULT : null
|
configuration.isAnnounceForge() ? ModInfo.DEFAULT : null
|
||||||
|
|||||||
@ -104,7 +104,7 @@ public final class ConnectionManager {
|
|||||||
.childOption(ChannelOption.IP_TOS, 0x18)
|
.childOption(ChannelOption.IP_TOS, 0x18)
|
||||||
.localAddress(address);
|
.localAddress(address);
|
||||||
|
|
||||||
if (transportType.supportsTcpFastOpenServer() && server.getConfiguration().useTcpFastOpen()) {
|
if (server.getConfiguration().useTcpFastOpen()) {
|
||||||
bootstrap.option(ChannelOption.TCP_FASTOPEN, 3);
|
bootstrap.option(ChannelOption.TCP_FASTOPEN, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -197,7 +197,7 @@ public final class ConnectionManager {
|
|||||||
this.server.getConfiguration().getConnectTimeout())
|
this.server.getConfiguration().getConnectTimeout())
|
||||||
.group(group == null ? this.workerGroup : group)
|
.group(group == null ? this.workerGroup : group)
|
||||||
.resolver(this.resolver.asGroup());
|
.resolver(this.resolver.asGroup());
|
||||||
if (transportType.supportsTcpFastOpenClient() && server.getConfiguration().useTcpFastOpen()) {
|
if (server.getConfiguration().useTcpFastOpen()) {
|
||||||
bootstrap.option(ChannelOption.TCP_FASTOPEN_CONNECT, true);
|
bootstrap.option(ChannelOption.TCP_FASTOPEN_CONNECT, true);
|
||||||
}
|
}
|
||||||
return bootstrap;
|
return bootstrap;
|
||||||
|
|||||||
@ -20,27 +20,32 @@ package com.velocitypowered.proxy.network;
|
|||||||
import com.velocitypowered.proxy.util.concurrent.VelocityNettyThreadFactory;
|
import com.velocitypowered.proxy.util.concurrent.VelocityNettyThreadFactory;
|
||||||
import io.netty.channel.ChannelFactory;
|
import io.netty.channel.ChannelFactory;
|
||||||
import io.netty.channel.EventLoopGroup;
|
import io.netty.channel.EventLoopGroup;
|
||||||
|
import io.netty.channel.IoHandlerFactory;
|
||||||
|
import io.netty.channel.MultiThreadIoEventLoopGroup;
|
||||||
import io.netty.channel.epoll.Epoll;
|
import io.netty.channel.epoll.Epoll;
|
||||||
import io.netty.channel.epoll.EpollDatagramChannel;
|
import io.netty.channel.epoll.EpollDatagramChannel;
|
||||||
import io.netty.channel.epoll.EpollEventLoopGroup;
|
import io.netty.channel.epoll.EpollIoHandler;
|
||||||
import io.netty.channel.epoll.EpollServerSocketChannel;
|
import io.netty.channel.epoll.EpollServerSocketChannel;
|
||||||
import io.netty.channel.epoll.EpollSocketChannel;
|
import io.netty.channel.epoll.EpollSocketChannel;
|
||||||
import io.netty.channel.kqueue.KQueue;
|
import io.netty.channel.kqueue.KQueue;
|
||||||
import io.netty.channel.kqueue.KQueueDatagramChannel;
|
import io.netty.channel.kqueue.KQueueDatagramChannel;
|
||||||
import io.netty.channel.kqueue.KQueueEventLoopGroup;
|
import io.netty.channel.kqueue.KQueueIoHandler;
|
||||||
import io.netty.channel.kqueue.KQueueServerSocketChannel;
|
import io.netty.channel.kqueue.KQueueServerSocketChannel;
|
||||||
import io.netty.channel.kqueue.KQueueSocketChannel;
|
import io.netty.channel.kqueue.KQueueSocketChannel;
|
||||||
import io.netty.channel.nio.NioEventLoopGroup;
|
import io.netty.channel.nio.NioIoHandler;
|
||||||
import io.netty.channel.socket.DatagramChannel;
|
import io.netty.channel.socket.DatagramChannel;
|
||||||
import io.netty.channel.socket.ServerSocketChannel;
|
import io.netty.channel.socket.ServerSocketChannel;
|
||||||
import io.netty.channel.socket.SocketChannel;
|
import io.netty.channel.socket.SocketChannel;
|
||||||
import io.netty.channel.socket.nio.NioDatagramChannel;
|
import io.netty.channel.socket.nio.NioDatagramChannel;
|
||||||
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
||||||
import io.netty.channel.socket.nio.NioSocketChannel;
|
import io.netty.channel.socket.nio.NioSocketChannel;
|
||||||
import io.netty.incubator.channel.uring.*;
|
import io.netty.channel.uring.IoUring;
|
||||||
|
import io.netty.channel.uring.IoUringDatagramChannel;
|
||||||
|
import io.netty.channel.uring.IoUringIoHandler;
|
||||||
|
import io.netty.channel.uring.IoUringServerSocketChannel;
|
||||||
|
import io.netty.channel.uring.IoUringSocketChannel;
|
||||||
import java.util.concurrent.ThreadFactory;
|
import java.util.concurrent.ThreadFactory;
|
||||||
import java.util.function.BiFunction;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enumerates the supported transports for Velocity.
|
* Enumerates the supported transports for Velocity.
|
||||||
@ -49,50 +54,36 @@ public enum TransportType {
|
|||||||
NIO("NIO", NioServerSocketChannel::new,
|
NIO("NIO", NioServerSocketChannel::new,
|
||||||
NioSocketChannel::new,
|
NioSocketChannel::new,
|
||||||
NioDatagramChannel::new,
|
NioDatagramChannel::new,
|
||||||
(name, type) -> new NioEventLoopGroup(0, createThreadFactory(name, type)),
|
NioIoHandler::newFactory),
|
||||||
false,
|
|
||||||
false),
|
|
||||||
EPOLL("epoll", EpollServerSocketChannel::new,
|
EPOLL("epoll", EpollServerSocketChannel::new,
|
||||||
EpollSocketChannel::new,
|
EpollSocketChannel::new,
|
||||||
EpollDatagramChannel::new,
|
EpollDatagramChannel::new,
|
||||||
(name, type) -> new EpollEventLoopGroup(0, createThreadFactory(name, type)),
|
EpollIoHandler::newFactory),
|
||||||
Epoll.isTcpFastOpenServerSideAvailable(),
|
|
||||||
Epoll.isTcpFastOpenClientSideAvailable()),
|
|
||||||
IO_URING("io_uring", IOUringServerSocketChannel::new,
|
|
||||||
IOUringSocketChannel::new,
|
|
||||||
IOUringDatagramChannel::new,
|
|
||||||
(name, type) -> new IOUringEventLoopGroup(0, createThreadFactory(name, type)),
|
|
||||||
IOUring.isTcpFastOpenServerSideAvailable(),
|
|
||||||
IOUring.isTcpFastOpenClientSideAvailable()),
|
|
||||||
KQUEUE("kqueue", KQueueServerSocketChannel::new,
|
KQUEUE("kqueue", KQueueServerSocketChannel::new,
|
||||||
KQueueSocketChannel::new,
|
KQueueSocketChannel::new,
|
||||||
KQueueDatagramChannel::new,
|
KQueueDatagramChannel::new,
|
||||||
(name, type) -> new KQueueEventLoopGroup(0, createThreadFactory(name, type)),
|
KQueueIoHandler::newFactory),
|
||||||
KQueue.isTcpFastOpenServerSideAvailable(),
|
IO_URING("io_uring", IoUringServerSocketChannel::new,
|
||||||
KQueue.isTcpFastOpenClientSideAvailable());
|
IoUringSocketChannel::new,
|
||||||
|
IoUringDatagramChannel::new,
|
||||||
|
IoUringIoHandler::newFactory);
|
||||||
|
|
||||||
final String name;
|
final String name;
|
||||||
final ChannelFactory<? extends ServerSocketChannel> serverSocketChannelFactory;
|
final ChannelFactory<? extends ServerSocketChannel> serverSocketChannelFactory;
|
||||||
final ChannelFactory<? extends SocketChannel> socketChannelFactory;
|
final ChannelFactory<? extends SocketChannel> socketChannelFactory;
|
||||||
final ChannelFactory<? extends DatagramChannel> datagramChannelFactory;
|
final ChannelFactory<? extends DatagramChannel> datagramChannelFactory;
|
||||||
final BiFunction<String, Type, EventLoopGroup> eventLoopGroupFactory;
|
final Supplier<IoHandlerFactory> ioHandlerFactorySupplier;
|
||||||
final boolean supportsTcpFastOpenServer;
|
|
||||||
final boolean supportsTcpFastOpenClient;
|
|
||||||
|
|
||||||
TransportType(final String name,
|
TransportType(final String name,
|
||||||
final ChannelFactory<? extends ServerSocketChannel> serverSocketChannelFactory,
|
final ChannelFactory<? extends ServerSocketChannel> serverSocketChannelFactory,
|
||||||
final ChannelFactory<? extends SocketChannel> socketChannelFactory,
|
final ChannelFactory<? extends SocketChannel> socketChannelFactory,
|
||||||
final ChannelFactory<? extends DatagramChannel> datagramChannelFactory,
|
final ChannelFactory<? extends DatagramChannel> datagramChannelFactory,
|
||||||
final BiFunction<String, Type, EventLoopGroup> eventLoopGroupFactory,
|
final Supplier<IoHandlerFactory> ioHandlerFactorySupplier) {
|
||||||
final boolean supportsTcpFastOpenServer,
|
|
||||||
final boolean supportsTcpFastOpenClient) {
|
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.serverSocketChannelFactory = serverSocketChannelFactory;
|
this.serverSocketChannelFactory = serverSocketChannelFactory;
|
||||||
this.socketChannelFactory = socketChannelFactory;
|
this.socketChannelFactory = socketChannelFactory;
|
||||||
this.datagramChannelFactory = datagramChannelFactory;
|
this.datagramChannelFactory = datagramChannelFactory;
|
||||||
this.eventLoopGroupFactory = eventLoopGroupFactory;
|
this.ioHandlerFactorySupplier = ioHandlerFactorySupplier;
|
||||||
this.supportsTcpFastOpenServer = supportsTcpFastOpenServer;
|
|
||||||
this.supportsTcpFastOpenClient = supportsTcpFastOpenClient;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -100,16 +91,15 @@ public enum TransportType {
|
|||||||
return this.name;
|
return this.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new event loop group for the given type.
|
||||||
|
*
|
||||||
|
* @param type the type of event loop group to create
|
||||||
|
* @return the event loop group
|
||||||
|
*/
|
||||||
public EventLoopGroup createEventLoopGroup(final Type type) {
|
public EventLoopGroup createEventLoopGroup(final Type type) {
|
||||||
return this.eventLoopGroupFactory.apply(this.name, type);
|
return new MultiThreadIoEventLoopGroup(
|
||||||
}
|
0, createThreadFactory(this.name, type), this.ioHandlerFactorySupplier.get());
|
||||||
|
|
||||||
public boolean supportsTcpFastOpenServer() {
|
|
||||||
return supportsTcpFastOpenServer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean supportsTcpFastOpenClient() {
|
|
||||||
return supportsTcpFastOpenClient;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ThreadFactory createThreadFactory(final String name, final Type type) {
|
private static ThreadFactory createThreadFactory(final String name, final Type type) {
|
||||||
@ -126,7 +116,7 @@ public enum TransportType {
|
|||||||
return NIO;
|
return NIO;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(IOUring.isAvailable()) {
|
if (IoUring.isAvailable() && !Boolean.getBoolean("velocity.disable-iouring-transport")) {
|
||||||
return IO_URING;
|
return IO_URING;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 Velocity Contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.velocitypowered.proxy.protocol.packet.chat;
|
||||||
|
|
||||||
|
import com.velocitypowered.api.proxy.Player;
|
||||||
|
import com.velocitypowered.proxy.VelocityServer;
|
||||||
|
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
|
||||||
|
public abstract class RateLimitedCommandHandler<T extends MinecraftPacket> implements CommandHandler<T> {
|
||||||
|
|
||||||
|
private final Player player;
|
||||||
|
private final VelocityServer velocityServer;
|
||||||
|
|
||||||
|
private int failedAttempts;
|
||||||
|
|
||||||
|
protected RateLimitedCommandHandler(Player player, VelocityServer velocityServer) {
|
||||||
|
this.player = player;
|
||||||
|
this.velocityServer = velocityServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean handlePlayerCommand(MinecraftPacket packet) {
|
||||||
|
if (packetClass().isInstance(packet)) {
|
||||||
|
if (!velocityServer.getCommandRateLimiter().attempt(player.getUniqueId())) {
|
||||||
|
if (velocityServer.getConfiguration().isKickOnCommandRateLimit() && failedAttempts++ >= velocityServer.getConfiguration().getKickAfterRateLimitedCommands()) {
|
||||||
|
player.disconnect(Component.translatable("velocity.kick.command-rate-limit"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (velocityServer.getConfiguration().isForwardCommandsIfRateLimited()) {
|
||||||
|
return false; // Send the packet to the server
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
failedAttempts = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePlayerCommandInternal(packetClass().cast(packet));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -21,17 +21,18 @@ import com.velocitypowered.api.event.command.CommandExecuteEvent;
|
|||||||
import com.velocitypowered.api.proxy.crypto.IdentifiedKey;
|
import com.velocitypowered.api.proxy.crypto.IdentifiedKey;
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
import com.velocitypowered.proxy.VelocityServer;
|
||||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||||
import com.velocitypowered.proxy.protocol.packet.chat.CommandHandler;
|
import com.velocitypowered.proxy.protocol.packet.chat.RateLimitedCommandHandler;
|
||||||
import com.velocitypowered.proxy.protocol.packet.chat.builder.ChatBuilderV2;
|
import com.velocitypowered.proxy.protocol.packet.chat.builder.ChatBuilderV2;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import net.kyori.adventure.text.Component;
|
import net.kyori.adventure.text.Component;
|
||||||
|
|
||||||
public class KeyedCommandHandler implements CommandHandler<KeyedPlayerCommandPacket> {
|
public class KeyedCommandHandler extends RateLimitedCommandHandler<KeyedPlayerCommandPacket> {
|
||||||
|
|
||||||
private final ConnectedPlayer player;
|
private final ConnectedPlayer player;
|
||||||
private final VelocityServer server;
|
private final VelocityServer server;
|
||||||
|
|
||||||
public KeyedCommandHandler(ConnectedPlayer player, VelocityServer server) {
|
public KeyedCommandHandler(ConnectedPlayer player, VelocityServer server) {
|
||||||
|
super(player, server);
|
||||||
this.player = player;
|
this.player = player;
|
||||||
this.server = server;
|
this.server = server;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,16 +20,18 @@ package com.velocitypowered.proxy.protocol.packet.chat.legacy;
|
|||||||
import com.velocitypowered.api.event.command.CommandExecuteEvent;
|
import com.velocitypowered.api.event.command.CommandExecuteEvent;
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
import com.velocitypowered.proxy.VelocityServer;
|
||||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||||
import com.velocitypowered.proxy.protocol.packet.chat.CommandHandler;
|
import com.velocitypowered.proxy.protocol.packet.chat.RateLimitedCommandHandler;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
public class LegacyCommandHandler implements CommandHandler<LegacyChatPacket> {
|
public class LegacyCommandHandler extends RateLimitedCommandHandler<LegacyChatPacket> {
|
||||||
|
|
||||||
private final ConnectedPlayer player;
|
private final ConnectedPlayer player;
|
||||||
private final VelocityServer server;
|
private final VelocityServer server;
|
||||||
|
|
||||||
public LegacyCommandHandler(ConnectedPlayer player, VelocityServer server) {
|
public LegacyCommandHandler(ConnectedPlayer player, VelocityServer server) {
|
||||||
|
super(player, server);
|
||||||
this.player = player;
|
this.player = player;
|
||||||
this.server = server;
|
this.server = server;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,17 +22,19 @@ import com.velocitypowered.proxy.VelocityServer;
|
|||||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.chat.ChatAcknowledgementPacket;
|
import com.velocitypowered.proxy.protocol.packet.chat.ChatAcknowledgementPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.chat.CommandHandler;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
import com.velocitypowered.proxy.protocol.packet.chat.RateLimitedCommandHandler;
|
||||||
import net.kyori.adventure.text.Component;
|
import net.kyori.adventure.text.Component;
|
||||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||||
|
|
||||||
public class SessionCommandHandler implements CommandHandler<SessionPlayerCommandPacket> {
|
public class SessionCommandHandler extends RateLimitedCommandHandler<SessionPlayerCommandPacket> {
|
||||||
|
|
||||||
private final ConnectedPlayer player;
|
private final ConnectedPlayer player;
|
||||||
private final VelocityServer server;
|
private final VelocityServer server;
|
||||||
|
|
||||||
public SessionCommandHandler(ConnectedPlayer player, VelocityServer server) {
|
public SessionCommandHandler(ConnectedPlayer player, VelocityServer server) {
|
||||||
|
super(player, server);
|
||||||
this.player = player;
|
this.player = player;
|
||||||
this.server = server;
|
this.server = server;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,13 +22,20 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
|||||||
|
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.velocitypowered.api.network.ProtocolVersion;
|
import com.velocitypowered.api.network.ProtocolVersion;
|
||||||
|
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
||||||
|
import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier;
|
||||||
|
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
|
||||||
import com.velocitypowered.api.util.ProxyVersion;
|
import com.velocitypowered.api.util.ProxyVersion;
|
||||||
|
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||||
|
import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder;
|
||||||
import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
|
import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
|
||||||
|
import com.velocitypowered.proxy.util.except.QuietDecoderException;
|
||||||
import io.netty.buffer.ByteBuf;
|
import io.netty.buffer.ByteBuf;
|
||||||
import io.netty.buffer.Unpooled;
|
import io.netty.buffer.Unpooled;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
@ -85,13 +92,18 @@ public final class PluginMessageUtil {
|
|||||||
.equals(UNREGISTER_CHANNEL);
|
.equals(UNREGISTER_CHANNEL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final QuietDecoderException ILLEGAL_CHANNEL = new QuietDecoderException("Illegal channel");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches all the channels in a register or unregister plugin message.
|
* Fetches all the channels in a register or unregister plugin message.
|
||||||
*
|
*
|
||||||
|
* @param existingChannels the number of channels already registered
|
||||||
* @param message the message to get the channels from
|
* @param message the message to get the channels from
|
||||||
* @return the channels, as an immutable list
|
* @return the channels, as an immutable list
|
||||||
*/
|
*/
|
||||||
public static List<String> getChannels(PluginMessagePacket message) {
|
public static List<ChannelIdentifier> getChannels(int existingChannels,
|
||||||
|
PluginMessagePacket message,
|
||||||
|
ProtocolVersion protocolVersion) {
|
||||||
checkNotNull(message, "message");
|
checkNotNull(message, "message");
|
||||||
checkArgument(isRegister(message) || isUnregister(message), "Unknown channel type %s",
|
checkArgument(isRegister(message) || isUnregister(message), "Unknown channel type %s",
|
||||||
message.getChannel());
|
message.getChannel());
|
||||||
@ -100,8 +112,28 @@ public final class PluginMessageUtil {
|
|||||||
// has caused issues with 1.13+ compatibility. Just return an empty list.
|
// has caused issues with 1.13+ compatibility. Just return an empty list.
|
||||||
return ImmutableList.of();
|
return ImmutableList.of();
|
||||||
}
|
}
|
||||||
String channels = message.content().toString(StandardCharsets.UTF_8);
|
String payload = message.content().toString(StandardCharsets.UTF_8);
|
||||||
return ImmutableList.copyOf(channels.split("\0"));
|
checkArgument(payload.length() <= Short.MAX_VALUE, "payload too long: %s", payload.length());
|
||||||
|
String[] channels = payload.split("\0");
|
||||||
|
checkArgument(existingChannels + channels.length <= ConnectedPlayer.MAX_CLIENTSIDE_PLUGIN_CHANNELS,
|
||||||
|
"too many channels: %s + %s > %s", existingChannels, channels.length, ConnectedPlayer.MAX_CLIENTSIDE_PLUGIN_CHANNELS);
|
||||||
|
ImmutableList.Builder<ChannelIdentifier> channelIdentifiers = ImmutableList.builderWithExpectedSize(channels.length);
|
||||||
|
try {
|
||||||
|
for (String channel : channels) {
|
||||||
|
if (protocolVersion.noLessThan(ProtocolVersion.MINECRAFT_1_13)) {
|
||||||
|
channelIdentifiers.add(MinecraftChannelIdentifier.from(channel));
|
||||||
|
} else {
|
||||||
|
channelIdentifiers.add(new LegacyChannelIdentifier(channel));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
if (MinecraftDecoder.DEBUG) {
|
||||||
|
throw e;
|
||||||
|
} else {
|
||||||
|
throw ILLEGAL_CHANNEL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return channelIdentifiers.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -112,16 +144,31 @@ public final class PluginMessageUtil {
|
|||||||
* @return the plugin message to send
|
* @return the plugin message to send
|
||||||
*/
|
*/
|
||||||
public static PluginMessagePacket constructChannelsPacket(ProtocolVersion protocolVersion,
|
public static PluginMessagePacket constructChannelsPacket(ProtocolVersion protocolVersion,
|
||||||
Collection<String> channels) {
|
Collection<ChannelIdentifier> channels) {
|
||||||
checkNotNull(channels, "channels");
|
checkNotNull(channels, "channels");
|
||||||
checkArgument(!channels.isEmpty(), "no channels specified");
|
checkArgument(!channels.isEmpty(), "no channels specified");
|
||||||
String channelName = protocolVersion.noLessThan(ProtocolVersion.MINECRAFT_1_13)
|
String channelName = protocolVersion.noLessThan(ProtocolVersion.MINECRAFT_1_13)
|
||||||
? REGISTER_CHANNEL : REGISTER_CHANNEL_LEGACY;
|
? REGISTER_CHANNEL : REGISTER_CHANNEL_LEGACY;
|
||||||
ByteBuf contents = Unpooled.buffer();
|
ByteBuf contents = Unpooled.buffer();
|
||||||
contents.writeCharSequence(String.join("\0", channels), StandardCharsets.UTF_8);
|
contents.writeCharSequence(joinChannels(channels), StandardCharsets.UTF_8);
|
||||||
return new PluginMessagePacket(channelName, contents);
|
return new PluginMessagePacket(channelName, contents);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String joinChannels(Collection<ChannelIdentifier> channels) {
|
||||||
|
checkNotNull(channels, "channels");
|
||||||
|
checkArgument(!channels.isEmpty(), "no channels specified");
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
Iterator<ChannelIdentifier> iterator = channels.iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
ChannelIdentifier channel = iterator.next();
|
||||||
|
sb.append(channel.getId());
|
||||||
|
if (iterator.hasNext()) {
|
||||||
|
sb.append('\0');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rewrites the brand message to indicate the presence of Velocity.
|
* Rewrites the brand message to indicate the presence of Velocity.
|
||||||
*
|
*
|
||||||
|
|||||||
@ -79,10 +79,10 @@ public class VelocityChannelRegistrar implements ChannelRegistrar {
|
|||||||
*
|
*
|
||||||
* @return all legacy channel IDs
|
* @return all legacy channel IDs
|
||||||
*/
|
*/
|
||||||
public Collection<String> getLegacyChannelIds() {
|
public Collection<ChannelIdentifier> getLegacyChannelIds() {
|
||||||
Collection<String> ids = new HashSet<>();
|
Collection<ChannelIdentifier> ids = new HashSet<>();
|
||||||
for (ChannelIdentifier value : identifierMap.values()) {
|
for (ChannelIdentifier value : identifierMap.values()) {
|
||||||
ids.add(value.getId());
|
ids.add(new LegacyChannelIdentifier(value.getId()));
|
||||||
}
|
}
|
||||||
return ids;
|
return ids;
|
||||||
}
|
}
|
||||||
@ -92,13 +92,13 @@ public class VelocityChannelRegistrar implements ChannelRegistrar {
|
|||||||
*
|
*
|
||||||
* @return the channel IDs for Minecraft 1.13 and above
|
* @return the channel IDs for Minecraft 1.13 and above
|
||||||
*/
|
*/
|
||||||
public Collection<String> getModernChannelIds() {
|
public Collection<ChannelIdentifier> getModernChannelIds() {
|
||||||
Collection<String> ids = new HashSet<>();
|
Collection<ChannelIdentifier> ids = new HashSet<>();
|
||||||
for (ChannelIdentifier value : identifierMap.values()) {
|
for (ChannelIdentifier value : identifierMap.values()) {
|
||||||
if (value instanceof MinecraftChannelIdentifier) {
|
if (value instanceof MinecraftChannelIdentifier) {
|
||||||
ids.add(value.getId());
|
ids.add(value);
|
||||||
} else {
|
} else {
|
||||||
ids.add(PluginMessageUtil.transformLegacyToModernChannel(value.getId()));
|
ids.add(MinecraftChannelIdentifier.from(PluginMessageUtil.transformLegacyToModernChannel(value.getId())));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ids;
|
return ids;
|
||||||
@ -114,7 +114,7 @@ public class VelocityChannelRegistrar implements ChannelRegistrar {
|
|||||||
* @param protocolVersion the protocol version in use
|
* @param protocolVersion the protocol version in use
|
||||||
* @return the list of channels to register
|
* @return the list of channels to register
|
||||||
*/
|
*/
|
||||||
public Collection<String> getChannelsForProtocol(ProtocolVersion protocolVersion) {
|
public Collection<ChannelIdentifier> getChannelsForProtocol(ProtocolVersion protocolVersion) {
|
||||||
if (protocolVersion.noLessThan(ProtocolVersion.MINECRAFT_1_13)) {
|
if (protocolVersion.noLessThan(ProtocolVersion.MINECRAFT_1_13)) {
|
||||||
return getModernChannelIds();
|
return getModernChannelIds();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019-2023 Velocity Contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.velocitypowered.proxy.util.collect;
|
||||||
|
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
import com.google.common.collect.ForwardingSet;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An unsynchronized collection that puts an upper bound on the size of the collection.
|
||||||
|
*/
|
||||||
|
public final class CappedSet<T> extends ForwardingSet<T> {
|
||||||
|
|
||||||
|
private final Set<T> delegate;
|
||||||
|
private final int upperSize;
|
||||||
|
|
||||||
|
private CappedSet(Set<T> delegate, int upperSize) {
|
||||||
|
this.delegate = delegate;
|
||||||
|
this.upperSize = upperSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a capped collection backed by a {@link HashSet}.
|
||||||
|
*
|
||||||
|
* @param maxSize the maximum size of the collection
|
||||||
|
* @param <T> the type of elements in the collection
|
||||||
|
* @return the new collection
|
||||||
|
*/
|
||||||
|
public static <T> Set<T> create(int maxSize) {
|
||||||
|
return new CappedSet<>(new HashSet<>(), maxSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Set<T> delegate() {
|
||||||
|
return delegate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean add(T element) {
|
||||||
|
if (this.delegate.size() >= upperSize) {
|
||||||
|
Preconditions.checkState(this.delegate.contains(element),
|
||||||
|
"collection is too large (%s >= %s)",
|
||||||
|
this.delegate.size(), this.upperSize);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.delegate.add(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean addAll(Collection<? extends T> collection) {
|
||||||
|
return this.standardAddAll(collection);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -22,15 +22,15 @@ import com.github.benmanes.caffeine.cache.Caffeine;
|
|||||||
import com.github.benmanes.caffeine.cache.Ticker;
|
import com.github.benmanes.caffeine.cache.Ticker;
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple rate-limiter based on a Caffeine {@link Cache}.
|
* A simple rate-limiter based on a Caffeine {@link Cache}.
|
||||||
*/
|
*/
|
||||||
public class CaffeineCacheRatelimiter implements Ratelimiter {
|
public class CaffeineCacheRatelimiter<T> implements Ratelimiter<T> {
|
||||||
|
|
||||||
private final Cache<InetAddress, Long> expiringCache;
|
private final Cache<T, Long> expiringCache;
|
||||||
private final long timeoutNanos;
|
private final long timeoutNanos;
|
||||||
|
|
||||||
CaffeineCacheRatelimiter(long time, TimeUnit unit) {
|
CaffeineCacheRatelimiter(long time, TimeUnit unit) {
|
||||||
@ -49,16 +49,15 @@ public class CaffeineCacheRatelimiter implements Ratelimiter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempts to rate-limit the client.
|
* Attempts to rate-limit the object.
|
||||||
*
|
*
|
||||||
* @param address the address to rate limit
|
* @param key the object to rate limit
|
||||||
* @return true if we should allow the client, false if we should rate-limit
|
* @return true if we should allow the object, false if we should rate-limit
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public boolean attempt(InetAddress address) {
|
public boolean attempt(@NotNull T key) {
|
||||||
Preconditions.checkNotNull(address, "address");
|
|
||||||
long expectedNewValue = System.nanoTime() + timeoutNanos;
|
long expectedNewValue = System.nanoTime() + timeoutNanos;
|
||||||
long last = expiringCache.get(address, (address1) -> expectedNewValue);
|
long last = expiringCache.get(key, (key1) -> expectedNewValue);
|
||||||
return expectedNewValue == last;
|
return expectedNewValue == last;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,16 +17,16 @@
|
|||||||
|
|
||||||
package com.velocitypowered.proxy.util.ratelimit;
|
package com.velocitypowered.proxy.util.ratelimit;
|
||||||
|
|
||||||
import java.net.InetAddress;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link Ratelimiter} that does no rate-limiting.
|
* A {@link Ratelimiter} that does no rate-limiting.
|
||||||
*/
|
*/
|
||||||
enum NoopCacheRatelimiter implements Ratelimiter {
|
enum NoopCacheRatelimiter implements Ratelimiter<Object> {
|
||||||
INSTANCE;
|
INSTANCE;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean attempt(InetAddress address) {
|
public boolean attempt(@NotNull Object key) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,18 +17,18 @@
|
|||||||
|
|
||||||
package com.velocitypowered.proxy.util.ratelimit;
|
package com.velocitypowered.proxy.util.ratelimit;
|
||||||
|
|
||||||
import java.net.InetAddress;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows rate limiting of clients.
|
* Allows rate limiting of objects.
|
||||||
*/
|
*/
|
||||||
public interface Ratelimiter {
|
public interface Ratelimiter<T> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines whether or not to allow the connection.
|
* Attempts to rate-limit the object.
|
||||||
*
|
*
|
||||||
* @param address the address to rate limit
|
* @param key the object to rate limit
|
||||||
* @return true if allowed, false if not
|
* @return true if we should allow the object, false if we should rate-limit
|
||||||
*/
|
*/
|
||||||
boolean attempt(InetAddress address);
|
boolean attempt(@NotNull T key);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,8 +28,9 @@ public final class Ratelimiters {
|
|||||||
throw new AssertionError();
|
throw new AssertionError();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Ratelimiter createWithMilliseconds(long ms) {
|
@SuppressWarnings("unchecked")
|
||||||
return ms <= 0 ? NoopCacheRatelimiter.INSTANCE : new CaffeineCacheRatelimiter(ms,
|
public static <T> Ratelimiter<T> createWithMilliseconds(long ms) {
|
||||||
|
return ms <= 0 ? (Ratelimiter<T>) NoopCacheRatelimiter.INSTANCE : new CaffeineCacheRatelimiter(ms,
|
||||||
TimeUnit.MILLISECONDS);
|
TimeUnit.MILLISECONDS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -62,4 +62,6 @@ velocity.command.dump-server-error=An error occurred on the Velocity servers and
|
|||||||
velocity.command.dump-offline=Likely cause: Invalid system DNS settings or no internet connection
|
velocity.command.dump-offline=Likely cause: Invalid system DNS settings or no internet connection
|
||||||
velocity.command.send-usage=/send <player> <server>
|
velocity.command.send-usage=/send <player> <server>
|
||||||
# Kick
|
# Kick
|
||||||
velocity.kick.shutdown=Proxy shutting down.
|
velocity.kick.shutdown=Proxy shutting down.
|
||||||
|
velocity.kick.command-rate-limit=You are sending too many commands too quickly.
|
||||||
|
velocity.kick.tab-complete-rate-limit=You are sending too many tab complete requests too quickly.
|
||||||
@ -66,6 +66,11 @@ kick-existing-players = false
|
|||||||
# configuration is used if no servers could be contacted.
|
# configuration is used if no servers could be contacted.
|
||||||
ping-passthrough = "DISABLED"
|
ping-passthrough = "DISABLED"
|
||||||
|
|
||||||
|
# If enabled (default is false), then a sample of the online players on the proxy will be visible
|
||||||
|
# when hovering over the player count in the server list.
|
||||||
|
# This doesn't have any effect when ping passthrough is set to either "description" or "all".
|
||||||
|
sample-players-in-ping = false
|
||||||
|
|
||||||
# If not enabled (default is true) player IP addresses will be replaced by <ip address withheld> in logs
|
# If not enabled (default is true) player IP addresses will be replaced by <ip address withheld> in logs
|
||||||
enable-player-address-logging = true
|
enable-player-address-logging = true
|
||||||
|
|
||||||
@ -151,6 +156,27 @@ accepts-transfers = false
|
|||||||
# threads. Disabled by default. Requires Linux or macOS.
|
# threads. Disabled by default. Requires Linux or macOS.
|
||||||
enable-reuse-port = false
|
enable-reuse-port = false
|
||||||
|
|
||||||
|
# How fast (in milliseconds) are clients allowed to send commands after the last command
|
||||||
|
# By default this is 50ms (20 commands per second)
|
||||||
|
command-rate-limit = 50
|
||||||
|
|
||||||
|
# Should we forward commands to the backend upon being rate limited?
|
||||||
|
# This will forward the command to the server instead of processing it on the proxy.
|
||||||
|
# Since most server implementations have a rate limit, this will prevent the player
|
||||||
|
# from being able to send excessive commands to the server.
|
||||||
|
forward-commands-if-rate-limited = true
|
||||||
|
|
||||||
|
# How many commands are allowed to be sent after the rate limit is hit before the player is kicked?
|
||||||
|
# Setting this to 0 or lower will disable this feature.
|
||||||
|
kick-after-rate-limited-commands = 0
|
||||||
|
|
||||||
|
# How fast (in milliseconds) are clients allowed to send tab completions after the last tab completion
|
||||||
|
tab-complete-rate-limit = 10
|
||||||
|
|
||||||
|
# How many tab completions are allowed to be sent after the rate limit is hit before the player is kicked?
|
||||||
|
# Setting this to 0 or lower will disable this feature.
|
||||||
|
kick-after-rate-limited-tab-completes = 0
|
||||||
|
|
||||||
[query]
|
[query]
|
||||||
# Whether to enable responding to GameSpy 4 query responses or not.
|
# Whether to enable responding to GameSpy 4 query responses or not.
|
||||||
enabled = false
|
enabled = false
|
||||||
|
|||||||
@ -20,8 +20,10 @@ package com.velocitypowered.proxy.util;
|
|||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
||||||
import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier;
|
import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier;
|
||||||
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
|
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
class VelocityChannelRegistrarTest {
|
class VelocityChannelRegistrarTest {
|
||||||
@ -46,9 +48,9 @@ class VelocityChannelRegistrarTest {
|
|||||||
// Two channels cover the modern channel (velocity:test) and the legacy-mapped channel
|
// Two channels cover the modern channel (velocity:test) and the legacy-mapped channel
|
||||||
// (legacy:velocitytest). Make sure they're what we expect.
|
// (legacy:velocitytest). Make sure they're what we expect.
|
||||||
assertEquals(ImmutableSet.of(MODERN.getId(), SIMPLE_LEGACY_REMAPPED), registrar
|
assertEquals(ImmutableSet.of(MODERN.getId(), SIMPLE_LEGACY_REMAPPED), registrar
|
||||||
.getModernChannelIds());
|
.getModernChannelIds().stream().map(ChannelIdentifier::getId).collect(Collectors.toSet()));
|
||||||
assertEquals(ImmutableSet.of(SIMPLE_LEGACY.getId(), MODERN.getId()), registrar
|
assertEquals(ImmutableSet.of(SIMPLE_LEGACY.getId(), MODERN.getId()), registrar
|
||||||
.getLegacyChannelIds());
|
.getLegacyChannelIds().stream().map(ChannelIdentifier::getId).collect(Collectors.toSet()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -57,9 +59,10 @@ class VelocityChannelRegistrarTest {
|
|||||||
registrar.register(SPECIAL_REMAP_LEGACY, MODERN_SPECIAL_REMAP);
|
registrar.register(SPECIAL_REMAP_LEGACY, MODERN_SPECIAL_REMAP);
|
||||||
|
|
||||||
// This one, just one channel for the modern case.
|
// This one, just one channel for the modern case.
|
||||||
assertEquals(ImmutableSet.of(MODERN_SPECIAL_REMAP.getId()), registrar.getModernChannelIds());
|
assertEquals(ImmutableSet.of(MODERN_SPECIAL_REMAP.getId()),
|
||||||
|
registrar.getModernChannelIds().stream().map(ChannelIdentifier::getId).collect(Collectors.toSet()));
|
||||||
assertEquals(ImmutableSet.of(MODERN_SPECIAL_REMAP.getId(), SPECIAL_REMAP_LEGACY.getId()),
|
assertEquals(ImmutableSet.of(MODERN_SPECIAL_REMAP.getId(), SPECIAL_REMAP_LEGACY.getId()),
|
||||||
registrar.getLegacyChannelIds());
|
registrar.getLegacyChannelIds().stream().map(ChannelIdentifier::getId).collect(Collectors.toSet()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -68,7 +71,9 @@ class VelocityChannelRegistrarTest {
|
|||||||
registrar.register(MODERN, SIMPLE_LEGACY);
|
registrar.register(MODERN, SIMPLE_LEGACY);
|
||||||
registrar.unregister(SIMPLE_LEGACY);
|
registrar.unregister(SIMPLE_LEGACY);
|
||||||
|
|
||||||
assertEquals(ImmutableSet.of(MODERN.getId()), registrar.getModernChannelIds());
|
assertEquals(ImmutableSet.of(MODERN.getId()),
|
||||||
assertEquals(ImmutableSet.of(MODERN.getId()), registrar.getLegacyChannelIds());
|
registrar.getModernChannelIds().stream().map(ChannelIdentifier::getId).collect(Collectors.toSet()));;
|
||||||
|
assertEquals(ImmutableSet.of(MODERN.getId()),
|
||||||
|
registrar.getLegacyChannelIds().stream().map(ChannelIdentifier::getId).collect(Collectors.toSet()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019-2021 Velocity Contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.velocitypowered.proxy.util.collect;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Set;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class CappedSetTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void basicVerification() {
|
||||||
|
Collection<String> coll = CappedSet.create(1);
|
||||||
|
assertTrue(coll.add("coffee"), "did not add single item");
|
||||||
|
assertThrows(IllegalStateException.class, () -> coll.add("tea"),
|
||||||
|
"item was added to collection although it is too full");
|
||||||
|
assertEquals(1, coll.size(), "collection grew in size unexpectedly");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testAddAll() {
|
||||||
|
Set<String> doesFill1 = ImmutableSet.of("coffee", "tea");
|
||||||
|
Set<String> doesFill2 = ImmutableSet.of("chocolate");
|
||||||
|
Set<String> overfill = ImmutableSet.of("Coke", "Pepsi");
|
||||||
|
|
||||||
|
Collection<String> coll = CappedSet.create(3);
|
||||||
|
assertTrue(coll.addAll(doesFill1), "did not add items");
|
||||||
|
assertTrue(coll.addAll(doesFill2), "did not add items");
|
||||||
|
assertThrows(IllegalStateException.class, () -> coll.addAll(overfill),
|
||||||
|
"items added to collection although it is too full");
|
||||||
|
assertEquals(3, coll.size(), "collection grew in size unexpectedly");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void handlesSetBehaviorCorrectly() {
|
||||||
|
Set<String> doesFill1 = ImmutableSet.of("coffee", "tea");
|
||||||
|
Set<String> doesFill2 = ImmutableSet.of("coffee", "chocolate");
|
||||||
|
Set<String> overfill = ImmutableSet.of("coffee", "Coke", "Pepsi");
|
||||||
|
|
||||||
|
Collection<String> coll = CappedSet.create(3);
|
||||||
|
assertTrue(coll.addAll(doesFill1), "did not add items");
|
||||||
|
assertTrue(coll.addAll(doesFill2), "did not add items");
|
||||||
|
assertThrows(IllegalStateException.class, () -> coll.addAll(overfill),
|
||||||
|
"items added to collection although it is too full");
|
||||||
|
|
||||||
|
assertFalse(coll.addAll(doesFill1), "added items?!?");
|
||||||
|
|
||||||
|
assertEquals(3, coll.size(), "collection grew in size unexpectedly");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user