13
0
geforkt von Mirrors/Velocity

Merge branch 'master' into plugin-message-event

# Conflicts:
#	proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java
#	proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java
Dieser Commit ist enthalten in:
Andrew Steinborn 2018-09-16 02:35:38 -04:00
Commit 5ff36f1ae7
33 geänderte Dateien mit 750 neuen und 215 gelöschten Zeilen

Datei anzeigen

@ -44,5 +44,4 @@ is not currently suitable for production usage. For development and testing
purposes, however, Velocity is fully-fledged and ready to go.
Velocity supports Minecraft 1.8-1.13.1, and has full support for Paper and Sponge.
Forge support is currently not implemented, but Velocity will work with Forge's
vanilla fallback mode.
Forge is fully supported but mod compatibility may vary.

Datei anzeigen

@ -0,0 +1,28 @@
package com.velocitypowered.api.event.connection;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.proxy.Player;
/**
* This event is fired once the player has been successfully authenticated and
* fully initialized and player will be connected to server after this event
*/
public class PostLoginEvent {
private final Player player;
public PostLoginEvent(Player player) {
this.player = Preconditions.checkNotNull(player, "player");
}
public Player getPlayer() {
return player;
}
@Override
public String toString() {
return "PostLoginEvent{"
+ "player=" + player
+ '}';
}
}

Datei anzeigen

@ -3,8 +3,10 @@ package com.velocitypowered.api.event.connection;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.event.ResultedEvent;
import com.velocitypowered.api.proxy.InboundConnection;
import java.util.Optional;
import net.kyori.text.Component;
import net.kyori.text.serializer.ComponentSerializers;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
@ -52,44 +54,59 @@ public class PreLoginEvent implements ResultedEvent<PreLoginEvent.PreLoginCompon
}
/**
* Represents an "allowed/allowed with online mode/denied" result with a reason allowed for denial.
* Represents an "allowed/allowed with forced online\offline mode/denied" result with a reason allowed for denial.
*/
public static class PreLoginComponentResult extends ResultedEvent.ComponentResult {
private static final PreLoginComponentResult ALLOWED = new PreLoginComponentResult((Component) null);
private static final PreLoginComponentResult FORCE_ONLINEMODE = new PreLoginComponentResult(true);
public static class PreLoginComponentResult implements ResultedEvent.Result {
private final boolean onlineMode;
private static final PreLoginComponentResult ALLOWED = new PreLoginComponentResult(Result.ALLOWED, null);
private static final PreLoginComponentResult FORCE_ONLINEMODE = new PreLoginComponentResult(Result.FORCE_ONLINE, null);
private static final PreLoginComponentResult FORCE_OFFLINEMODE = new PreLoginComponentResult(Result.FORCE_OFFLINE, null);
/**
* Allows online mode to be enabled for the player connection, if Velocity is running in offline mode.
* @param allowedOnlineMode if true, online mode will be used for the connection
*/
private PreLoginComponentResult(boolean allowedOnlineMode) {
super(true, null);
this.onlineMode = allowedOnlineMode;
private final Result result;
private final Optional<Component> reason;
private PreLoginComponentResult(Result result, @Nullable Component reason) {
this.result = result;
this.reason = Optional.ofNullable(reason);
}
private PreLoginComponentResult(@Nullable Component reason) {
super(reason == null, reason);
// Don't care about this
this.onlineMode = false;
@Override
public boolean isAllowed() {
return result != Result.DISALLOWED;
}
public Optional<Component> getReason() {
return reason;
}
public boolean isOnlineModeAllowed() {
return this.onlineMode;
return result == Result.FORCE_ONLINE;
}
public boolean isForceOfflineMode() {
return result == Result.FORCE_OFFLINE;
}
@Override
public String toString() {
if (isForceOfflineMode()) {
return "allowed with force offline mode";
}
if (isOnlineModeAllowed()) {
return "allowed with online mode";
}
return super.toString();
if (isAllowed()) {
return "allowed";
}
if (reason.isPresent()) {
return "denied: " + ComponentSerializers.PLAIN.serialize(reason.get());
}
return "denied";
}
/**
* Returns a result indicating the connection will be allowed through the proxy.
* Returns a result indicating the connection will be allowed through
* the proxy.
* @return the allowed result
*/
public static PreLoginComponentResult allowed() {
@ -97,23 +114,41 @@ public class PreLoginEvent implements ResultedEvent<PreLoginEvent.PreLoginCompon
}
/**
* Returns a result indicating the connection will be allowed through the proxy, but the connection will be
* forced to use online mode provided that the proxy is in offline mode. This acts similarly to {@link #allowed()}
* on an online-mode proxy.
* Returns a result indicating the connection will be allowed through
* the proxy, but the connection will be forced to use online mode
* provided that the proxy is in offline mode. This acts similarly to
* {@link #allowed()} on an online-mode proxy.
* @return the result
*/
public static PreLoginComponentResult forceOnlineMode() {
return FORCE_ONLINEMODE;
}
/**
* Returns a result indicating the connection will be allowed through
* the proxy, but the connection will be forced to use offline mode even
* when proxy running in online mode
* @return the result
*/
public static PreLoginComponentResult forceOfflineMode() {
return FORCE_OFFLINEMODE;
}
/**
* Denies the login with the specified reason.
* @param reason the reason for disallowing the connection
* @return a new result
*/
public static PreLoginComponentResult denied(@NonNull Component reason) {
public static PreLoginComponentResult denied(Component reason) {
Preconditions.checkNotNull(reason, "reason");
return new PreLoginComponentResult(reason);
return new PreLoginComponentResult(Result.DISALLOWED, reason);
}
private enum Result {
ALLOWED,
FORCE_ONLINE,
FORCE_OFFLINE,
DISALLOWED
}
}
}

Datei anzeigen

@ -0,0 +1,118 @@
package com.velocitypowered.api.event.player;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.event.ResultedEvent;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.server.ServerInfo;
import net.kyori.text.Component;
import org.checkerframework.checker.nullness.qual.NonNull;
/**
* Fired when a player is kicked from a server. You may either allow Velocity to kick the player (with an optional reason
* override) or redirect the player to a separate server.
*/
public class KickedFromServerEvent implements ResultedEvent<KickedFromServerEvent.ServerKickResult> {
private final Player player;
private final ServerInfo server;
private final Component originalReason;
private final boolean duringLogin;
private ServerKickResult result;
public KickedFromServerEvent(Player player, ServerInfo server, Component originalReason, boolean duringLogin, Component fancyReason) {
this.player = Preconditions.checkNotNull(player, "player");
this.server = Preconditions.checkNotNull(server, "server");
this.originalReason = Preconditions.checkNotNull(originalReason, "originalReason");
this.duringLogin = duringLogin;
this.result = new DisconnectPlayer(fancyReason);
}
@Override
public ServerKickResult getResult() {
return result;
}
@Override
public void setResult(@NonNull ServerKickResult result) {
this.result = Preconditions.checkNotNull(result, "result");
}
public Player getPlayer() {
return player;
}
public ServerInfo getServer() {
return server;
}
public Component getOriginalReason() {
return originalReason;
}
public boolean kickedDuringLogin() {
return duringLogin;
}
/**
* Represents the base interface for {@link KickedFromServerEvent} results.
*/
public interface ServerKickResult extends ResultedEvent.Result {}
/**
* Tells the proxy to disconnect the player with the specified reason.
*/
public static class DisconnectPlayer implements ServerKickResult {
private final Component component;
private DisconnectPlayer(Component component) {
this.component = Preconditions.checkNotNull(component, "component");
}
@Override
public boolean isAllowed() {
return true;
}
public Component getReason() {
return component;
}
/**
* Creates a new {@link DisconnectPlayer} with the specified reason.
* @param reason the reason to use when disconnecting the player
* @return the disconnect result
*/
public static DisconnectPlayer create(Component reason) {
return new DisconnectPlayer(reason);
}
}
/**
* Tells the proxy to redirect the player to another server. No messages will be sent from the proxy
* when this result is used.
*/
public static class RedirectPlayer implements ServerKickResult {
private final ServerInfo server;
private RedirectPlayer(ServerInfo server) {
this.server = Preconditions.checkNotNull(server, "server");
}
@Override
public boolean isAllowed() {
return false;
}
public ServerInfo getServer() {
return server;
}
/**
* Creates a new redirect result to forward the player to the specified {@code server}.
* @param server the server to send the player to
* @return the redirect result
*/
public static RedirectPlayer create(ServerInfo server) {
return new RedirectPlayer(server);
}
}
}

Datei anzeigen

@ -14,11 +14,13 @@ import java.util.Optional;
*/
public class ServerPreConnectEvent implements ResultedEvent<ServerPreConnectEvent.ServerResult> {
private final Player player;
private final ServerInfo originalServer;
private ServerResult result;
public ServerPreConnectEvent(Player player, ServerResult result) {
public ServerPreConnectEvent(Player player, ServerInfo originalServer) {
this.player = Preconditions.checkNotNull(player, "player");
this.result = Preconditions.checkNotNull(result, "result");
this.originalServer = Preconditions.checkNotNull(originalServer, "originalServer");
this.result = ServerResult.allowed(originalServer);
}
public Player getPlayer() {
@ -35,10 +37,15 @@ public class ServerPreConnectEvent implements ResultedEvent<ServerPreConnectEven
this.result = Preconditions.checkNotNull(result, "result");
}
public ServerInfo getOriginalServer() {
return originalServer;
}
@Override
public String toString() {
return "ServerPreConnectEvent{" +
"player=" + player +
", originalServer=" + originalServer +
", result=" + result +
'}';
}
@ -50,11 +57,11 @@ public class ServerPreConnectEvent implements ResultedEvent<ServerPreConnectEven
private static final ServerResult DENIED = new ServerResult(false, null);
private final boolean allowed;
private final ServerInfo info;
private final ServerInfo server;
private ServerResult(boolean allowed, @Nullable ServerInfo info) {
private ServerResult(boolean allowed, @Nullable ServerInfo server) {
this.allowed = allowed;
this.info = info;
this.server = server;
}
@Override
@ -62,8 +69,8 @@ public class ServerPreConnectEvent implements ResultedEvent<ServerPreConnectEven
return allowed;
}
public Optional<ServerInfo> getInfo() {
return Optional.ofNullable(info);
public Optional<ServerInfo> getServer() {
return Optional.ofNullable(server);
}
@Override
@ -71,7 +78,7 @@ public class ServerPreConnectEvent implements ResultedEvent<ServerPreConnectEven
if (!allowed) {
return "denied";
}
return "allowed: connect to " + info.getName();
return "allowed: connect to " + server.getName();
}
public static ServerResult denied() {

Datei anzeigen

@ -4,7 +4,6 @@ import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.velocitypowered.api.util.Favicon;
import net.kyori.text.Component;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.*;
@ -17,12 +16,18 @@ public class ServerPing {
private final Players players;
private final Component description;
private final @Nullable Favicon favicon;
private final Modinfo modinfo;
public ServerPing(Version version, @Nullable Players players, Component description, @Nullable Favicon favicon) {
this(version, players, description, favicon, Modinfo.DEFAULT);
}
public ServerPing(Version version, @Nullable Players players, Component description, @Nullable Favicon favicon, @Nullable Modinfo modinfo) {
this.version = Preconditions.checkNotNull(version, "version");
this.players = players;
this.description = Preconditions.checkNotNull(description, "description");
this.favicon = favicon;
this.modinfo = modinfo;
}
public Version getVersion() {
@ -41,6 +46,10 @@ public class ServerPing {
return Optional.ofNullable(favicon);
}
public Optional<Modinfo> getModinfo() {
return Optional.ofNullable(modinfo);
}
@Override
public String toString() {
return "ServerPing{" +
@ -54,11 +63,19 @@ public class ServerPing {
public Builder asBuilder() {
Builder builder = new Builder();
builder.version = version;
builder.onlinePlayers = players.online;
builder.maximumPlayers = players.max;
builder.samplePlayers.addAll(players.sample);
if (players != null) {
builder.onlinePlayers = players.online;
builder.maximumPlayers = players.max;
builder.samplePlayers.addAll(players.sample);
} else {
builder.nullOutPlayers = true;
}
builder.description = description;
builder.favicon = favicon;
builder.nullOutModinfo = modinfo == null;
if (modinfo != null) {
builder.mods.addAll(modinfo.modList);
}
return builder;
}
@ -74,9 +91,11 @@ public class ServerPing {
private int onlinePlayers;
private int maximumPlayers;
private final List<SamplePlayer> samplePlayers = new ArrayList<>();
private final List<Mod> mods = new ArrayList<>();
private Component description;
private Favicon favicon;
private boolean nullOutPlayers;
private boolean nullOutModinfo;
private Builder() {
@ -102,11 +121,26 @@ public class ServerPing {
return this;
}
public Builder mods(Mod... mods) {
this.mods.addAll(Arrays.asList(mods));
return this;
}
public Builder clearMods() {
this.mods.clear();
return this;
}
public Builder clearSamplePlayers() {
this.samplePlayers.clear();
return this;
}
public Builder notModCompatible() {
this.nullOutModinfo = true;
return this;
}
public Builder nullPlayers() {
this.nullOutPlayers = true;
return this;
@ -123,7 +157,8 @@ public class ServerPing {
}
public ServerPing build() {
return new ServerPing(version, nullOutPlayers ? null : new Players(onlinePlayers, maximumPlayers, samplePlayers), description, favicon);
return new ServerPing(version, nullOutPlayers ? null : new Players(onlinePlayers, maximumPlayers, samplePlayers), description, favicon,
nullOutModinfo ? null : new Modinfo(mods));
}
public Version getVersion() {
@ -150,6 +185,10 @@ public class ServerPing {
return favicon;
}
public List<Mod> getMods() {
return mods;
}
@Override
public String toString() {
return "Builder{" +
@ -157,8 +196,11 @@ public class ServerPing {
", onlinePlayers=" + onlinePlayers +
", maximumPlayers=" + maximumPlayers +
", samplePlayers=" + samplePlayers +
", mods=" + mods +
", description=" + description +
", favicon=" + favicon +
", nullOutPlayers=" + nullOutPlayers +
", nullOutModinfo=" + nullOutModinfo +
'}';
}
}
@ -247,4 +289,25 @@ public class ServerPing {
'}';
}
}
public static class Modinfo {
public static final Modinfo DEFAULT = new Modinfo(ImmutableList.of());
private final String type = "FML";
private final List<Mod> modList;
public Modinfo(List<Mod> modList) {
this.modList = ImmutableList.copyOf(modList);
}
}
public static class Mod {
private final String id;
private final String version;
public Mod(String id, String version) {
this.id = Preconditions.checkNotNull(id, "id");
this.version = Preconditions.checkNotNull(version, "version");
}
}
}

Datei anzeigen

@ -57,7 +57,7 @@ public final class GameProfile {
'}';
}
public final class Property {
public final static class Property {
private final String name;
private final String value;
private final String signature;

Datei anzeigen

@ -13,8 +13,8 @@ allprojects {
// dependency versions
junitVersion = '5.3.0-M1'
slf4jVersion = '1.7.25'
log4jVersion = '2.11.0'
nettyVersion = '4.1.28.Final'
log4jVersion = '2.11.1'
nettyVersion = '4.1.29.Final'
guavaVersion = '25.1-jre'
getCurrentBranchName = {
@ -27,6 +27,17 @@ allprojects {
return os.toString().trim()
}
}
getCurrentShortRevision = {
new ByteArrayOutputStream().withStream { os ->
exec {
executable = "git"
args = ["rev-parse", "HEAD"]
standardOutput = os
}
return os.toString().trim().substring(0, 8)
}
}
}
repositories {

Datei anzeigen

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10-all.zip

Datei anzeigen

@ -14,8 +14,11 @@ compileTestJava {
jar {
manifest {
def buildNumber = System.getenv("BUILD_NUMBER") ?: "unknown"
def version = "${project.version} (git-${project.ext.getCurrentShortRevision()}, build ${buildNumber})"
attributes 'Main-Class': 'com.velocitypowered.proxy.Velocity'
attributes 'Implementation-Version': project.version
attributes 'Implementation-Version': version
}
}

Datei anzeigen

@ -19,7 +19,7 @@ public class Velocity {
public static void main(String... args) {
startTime = System.currentTimeMillis();
logger.info("Booting up Velocity...");
logger.info("Booting up Velocity {}...", Velocity.class.getPackage().getImplementationVersion());
VelocityServer server = new VelocityServer();
server.start();

Datei anzeigen

@ -29,7 +29,7 @@ public class VelocityCommand implements Command {
.append(TextComponent.of(" or the ").resetStyle())
.append(TextComponent.builder("Velocity GitHub")
.color(TextColor.GREEN)
.clickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://github.com/astei/velocity"))
.clickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://github.com/VelocityPowered/Velocity"))
.build())
.build();

Datei anzeigen

@ -1,5 +1,6 @@
package com.velocitypowered.proxy.config;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableMap;
import com.moandjiezana.toml.Toml;
import com.velocitypowered.api.util.Favicon;
@ -48,7 +49,7 @@ public class VelocityConfiguration extends AnnotatedConfig {
@Comment({
"Should we forward IP addresses and other data to backend servers?",
"Available options:",
"- \"none\": No forwarding will be done. All players will appear to be Should we forward IP addresses and other data to backend servers?connecting from the proxy",
"- \"none\": No forwarding will be done. All players will appear to be connecting from the proxy",
" and will have offline-mode UUIDs.",
"- \"legacy\": Forward player IPs and UUIDs in BungeeCord-compatible fashion. Use this if you run",
" servers using Minecraft 1.12 or lower.",
@ -62,6 +63,10 @@ public class VelocityConfiguration extends AnnotatedConfig {
@ConfigKey("forwarding-secret")
private byte[] forwardingSecret = generateRandomString(12).getBytes(StandardCharsets.UTF_8);
@Comment("Announce whether or not your server supports Forge/FML. If you run a modded server, we suggest turning this on.")
@ConfigKey("announce-forge")
private boolean announceForge = false;
@Table("[servers]")
private final Servers servers;
@ -83,12 +88,13 @@ public class VelocityConfiguration extends AnnotatedConfig {
}
private VelocityConfiguration(String bind, String motd, int showMaxPlayers, boolean onlineMode,
PlayerInfoForwarding playerInfoForwardingMode, byte[] forwardingSecret, Servers servers,
Advanced advanced, Query query) {
boolean announceForge, PlayerInfoForwarding playerInfoForwardingMode, byte[] forwardingSecret,
Servers servers, Advanced advanced, Query query) {
this.bind = bind;
this.motd = motd;
this.showMaxPlayers = showMaxPlayers;
this.onlineMode = onlineMode;
this.announceForge = announceForge;
this.playerInfoForwardingMode = playerInfoForwardingMode;
this.forwardingSecret = forwardingSecret;
this.servers = servers;
@ -103,13 +109,13 @@ public class VelocityConfiguration extends AnnotatedConfig {
if (bind.isEmpty()) {
logger.error("'bind' option is empty.");
valid = false;
}
try {
AddressUtil.parseAddress(bind);
} catch (IllegalArgumentException e) {
logger.error("'bind' option does not specify a valid IP address.", e);
valid = false;
} else {
try {
AddressUtil.parseAddress(bind);
} catch (IllegalArgumentException e) {
logger.error("'bind' option does not specify a valid IP address.", e);
valid = false;
}
}
if (!onlineMode) {
@ -118,11 +124,11 @@ public class VelocityConfiguration extends AnnotatedConfig {
switch (playerInfoForwardingMode) {
case NONE:
logger.info("Player info forwarding is disabled! All players will appear to be connecting from the proxy and will have offline-mode UUIDs.");
logger.warn("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.");
if (forwardingSecret == null || forwardingSecret.length == 0) {
logger.error("You don't have a forwarding secret set. This is required for security.");
valid = false;
}
break;
@ -148,7 +154,7 @@ public class VelocityConfiguration extends AnnotatedConfig {
for (String s : servers.getAttemptConnectionOrder()) {
if (!servers.getServers().containsKey(s)) {
logger.error("Fallback server " + s + " doesn't exist!");
logger.error("Fallback server " + s + " is not registered in your configuration!");
valid = false;
}
}
@ -165,18 +171,18 @@ public class VelocityConfiguration extends AnnotatedConfig {
logger.error("Invalid compression level {}", advanced.compressionLevel);
valid = false;
} else if (advanced.compressionLevel == 0) {
logger.warn("ALL packets going through the proxy are going to be uncompressed. This will increase bandwidth usage.");
logger.warn("ALL packets going through the proxy will be uncompressed. This will increase bandwidth usage.");
}
if (advanced.compressionThreshold < -1) {
logger.error("Invalid compression threshold {}", advanced.compressionLevel);
valid = false;
} else if (advanced.compressionThreshold == 0) {
logger.warn("ALL packets going through the proxy are going to be compressed. This may hurt performance.");
logger.warn("ALL packets going through the proxy will be compressed. This will compromise throughput and increase CPU usage!");
}
if (advanced.loginRatelimit < 0) {
logger.error("Invalid login ratelimit {}", advanced.loginRatelimit);
logger.error("Invalid login ratelimit {}ms", advanced.loginRatelimit);
valid = false;
}
@ -217,7 +223,7 @@ public class VelocityConfiguration extends AnnotatedConfig {
if (motd.startsWith("{")) {
motdAsComponent = ComponentSerializers.JSON.deserialize(motd);
} else {
motdAsComponent = ComponentSerializers.LEGACY.deserialize(LegacyChatColorUtils.translate('&', motd));
motdAsComponent = ComponentSerializers.LEGACY.deserialize(motd, '&');
}
}
return motdAsComponent;
@ -263,54 +269,34 @@ public class VelocityConfiguration extends AnnotatedConfig {
return favicon;
}
private void setBind(String bind) {
this.bind = bind;
public boolean isAnnounceForge() {
return announceForge;
}
private void setMotd(String motd) {
this.motd = motd;
public int getConnectTimeout() {
return advanced.getConnectionTimeout();
}
private void setShowMaxPlayers(int showMaxPlayers) {
this.showMaxPlayers = showMaxPlayers;
}
private void setOnlineMode(boolean onlineMode) {
this.onlineMode = onlineMode;
}
private void setPlayerInfoForwardingMode(PlayerInfoForwarding playerInfoForwardingMode) {
this.playerInfoForwardingMode = playerInfoForwardingMode;
}
private void setForwardingSecret(byte[] forwardingSecret) {
this.forwardingSecret = forwardingSecret;
}
private void setMotdAsComponent(Component motdAsComponent) {
this.motdAsComponent = motdAsComponent;
}
private void setFavicon(Favicon favicon) {
this.favicon = favicon;
public int getReadTimeout() {
return advanced.getReadTimeout();
}
@Override
public String toString() {
return "VelocityConfiguration{"
+ "bind='" + bind + '\''
+ ", motd='" + motd + '\''
+ ", showMaxPlayers=" + showMaxPlayers
+ ", onlineMode=" + onlineMode
+ ", playerInfoForwardingMode=" + playerInfoForwardingMode
+ ", forwardingSecret=" + ByteBufUtil.hexDump(forwardingSecret)
+ ", servers=" + servers
+ ", advanced=" + advanced
+ ", query=" + query
+ ", motdAsComponent=" + motdAsComponent
+ ", favicon=" + favicon
+ '}';
return MoreObjects.toStringHelper(this)
.add("configVersion", configVersion)
.add("bind", bind)
.add("motd", motd)
.add("showMaxPlayers", showMaxPlayers)
.add("onlineMode", onlineMode)
.add("playerInfoForwardingMode", playerInfoForwardingMode)
.add("forwardingSecret", forwardingSecret)
.add("announceForge", announceForge)
.add("servers", servers)
.add("advanced", advanced)
.add("query", query)
.add("favicon", favicon)
.toString();
}
public static VelocityConfiguration read(Path path) throws IOException {
@ -335,6 +321,7 @@ public class VelocityConfiguration extends AnnotatedConfig {
toml.getString("motd", "&3A Velocity Server"),
toml.getLong("show-max-players", 500L).intValue(),
toml.getBoolean("online-mode", true),
toml.getBoolean("announce-forge", false),
PlayerInfoForwarding.valueOf(toml.getString("player-info-forwarding-mode", "MODERN").toUpperCase()),
forwardingSecret,
servers,
@ -441,21 +428,23 @@ public class VelocityConfiguration extends AnnotatedConfig {
"Disable by setting to 0"})
@ConfigKey("login-ratelimit")
private int loginRatelimit = 3000;
@Comment({"Specify a custom timeout for connection timeouts here. The default is five seconds."})
@ConfigKey("connection-timeout")
private int connectionTimeout = 5000;
@Comment({"Specify a read timeout for connections here. The default is 30 seconds."})
@ConfigKey("read-timeout")
private int readTimeout = 30000;
private Advanced() {
}
private Advanced(int compressionThreshold, int compressionLevel, int loginRatelimit) {
this.compressionThreshold = compressionThreshold;
this.compressionLevel = compressionLevel;
this.loginRatelimit = loginRatelimit;
}
private Advanced(Toml toml) {
if (toml != null) {
this.compressionThreshold = toml.getLong("compression-threshold", 1024L).intValue();
this.compressionLevel = toml.getLong("compression-level", -1L).intValue();
this.loginRatelimit = toml.getLong("login-ratelimit", 3000L).intValue();
this.connectionTimeout = toml.getLong("connection-timeout", 5000L).intValue();
this.readTimeout = toml.getLong("read-timeout", 30000L).intValue();
}
}
@ -463,33 +452,31 @@ public class VelocityConfiguration extends AnnotatedConfig {
return compressionThreshold;
}
public void setCompressionThreshold(int compressionThreshold) {
this.compressionThreshold = compressionThreshold;
}
public int getCompressionLevel() {
return compressionLevel;
}
public void setCompressionLevel(int compressionLevel) {
this.compressionLevel = compressionLevel;
}
public int getLoginRatelimit() {
return loginRatelimit;
}
public void setLoginRatelimit(int loginRatelimit) {
this.loginRatelimit = loginRatelimit;
public int getConnectionTimeout() {
return connectionTimeout;
}
public int getReadTimeout() {
return readTimeout;
}
@Override
public String toString() {
return "Advanced{"
+ "compressionThreshold=" + compressionThreshold
+ ", compressionLevel=" + compressionLevel
+ ", loginRatelimit=" + loginRatelimit
+ '}';
return "Advanced{" +
"compressionThreshold=" + compressionThreshold +
", compressionLevel=" + compressionLevel +
", loginRatelimit=" + loginRatelimit +
", connectionTimeout=" + connectionTimeout +
", readTimeout=" + readTimeout +
'}';
}
}
@ -521,18 +508,10 @@ public class VelocityConfiguration extends AnnotatedConfig {
return queryEnabled;
}
public void setQueryEnabled(boolean queryEnabled) {
this.queryEnabled = queryEnabled;
}
public int getQueryPort() {
return queryPort;
}
public void setQueryPort(int queryPort) {
this.queryPort = queryPort;
}
@Override
public String toString() {
return "Query{"

Datei anzeigen

@ -45,7 +45,9 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
private MinecraftSessionHandler sessionHandler;
private int protocolVersion;
private MinecraftConnectionAssociation association;
private boolean isLegacyForge;
private final VelocityServer server;
private boolean canSendLegacyFMLResetPacket = false;
public MinecraftConnection(Channel channel, VelocityServer server) {
this.channel = channel;
@ -105,6 +107,13 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
}
}
@Override
public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
if (sessionHandler != null) {
sessionHandler.writabilityChanged();
}
}
public void write(Object msg) {
if (channel.isActive()) {
channel.writeAndFlush(msg, channel.voidPromise());
@ -222,4 +231,20 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
public void setAssociation(MinecraftConnectionAssociation association) {
this.association = association;
}
public boolean isLegacyForge() {
return isLegacyForge;
}
public void setLegacyForge(boolean isForge) {
this.isLegacyForge = isForge;
}
public boolean canSendLegacyFMLResetPacket() {
return canSendLegacyFMLResetPacket;
}
public void setCanSendLegacyFMLResetPacket(boolean canSendLegacyFMLResetPacket) {
this.canSendLegacyFMLResetPacket = isLegacyForge && canSendLegacyFMLResetPacket;
}
}

Datei anzeigen

@ -29,4 +29,8 @@ public interface MinecraftSessionHandler {
default void exception(Throwable throwable) {
}
default void writabilityChanged() {
}
}

Datei anzeigen

@ -6,4 +6,10 @@ public class VelocityConstants {
}
public static final String VELOCITY_IP_FORWARDING_CHANNEL = "velocity:player_info";
public static final String FORGE_LEGACY_HANDSHAKE_CHANNEL = "FML|HS";
public static final String FORGE_LEGACY_CHANNEL = "FML";
public static final String FORGE_MULTIPART_LEGACY_CHANNEL = "FML|MP";
public static final byte[] FORGE_LEGACY_HANDSHAKE_RESET_DATA = new byte[] { -2, 0 };
}

Datei anzeigen

@ -3,7 +3,9 @@ package com.velocitypowered.proxy.connection.backend;
import com.velocitypowered.api.event.connection.PluginMessageEvent;
import com.velocitypowered.api.event.player.ServerConnectedEvent;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.VelocityConstants;
import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler;
import com.velocitypowered.proxy.connection.util.ConnectionMessages;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.packet.*;
@ -30,7 +32,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
if (!connection.getPlayer().isActive()) {
// Connection was left open accidentally. Close it so as to avoid "You logged in from another location"
// errors.
connection.getMinecraftConnection().close();
connection.disconnect();
return;
}
@ -42,6 +44,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
connection.getPlayer().getConnection().write(packet);
} else if (packet instanceof Disconnect) {
Disconnect original = (Disconnect) packet;
connection.disconnect();
connection.getPlayer().handleConnectionException(connection.getServerInfo(), original);
} else if (packet instanceof JoinGame) {
playerHandler.handleBackendJoinGame((JoinGame) packet);
@ -67,6 +70,20 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
return;
}
if (!connection.hasCompletedJoin() && pm.getChannel().equals(VelocityConstants.FORGE_LEGACY_HANDSHAKE_CHANNEL)) {
if (!connection.isLegacyForge()) {
connection.setLegacyForge(true);
// We must always reset the handshake before a modded connection is established if
// we haven't done so already.
connection.getPlayer().sendLegacyForgeHandshakeResetPacket();
}
// Always forward these messages during login
connection.getPlayer().getConnection().write(pm);
return;
}
PluginMessageEvent event = new PluginMessageEvent(connection, connection.getPlayer(), server.getChannelRegistrar().getFromId(pm.getChannel()),
pm.getData());
server.getEventManager().fire(event)
@ -86,10 +103,13 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
if (!connection.getPlayer().isActive()) {
// Connection was left open accidentally. Close it so as to avoid "You logged in from another location"
// errors.
connection.getMinecraftConnection().close();
connection.disconnect();
return;
}
connection.getPlayer().getConnection().write(buf.retain());
if (connection.hasCompletedJoin()) {
connection.getPlayer().getConnection().write(buf.retain());
}
}
@Override
@ -97,16 +117,29 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
connection.getPlayer().handleConnectionException(connection.getServerInfo(), throwable);
}
public VelocityServer getServer() {
return server;
}
@Override
public void disconnected() {
if (connection.isGracefulDisconnect()) {
return;
}
connection.getPlayer().handleConnectionException(connection.getServerInfo(), Disconnect.create(ConnectionMessages.UNEXPECTED_DISCONNECT));
}
private boolean canForwardPluginMessage(PluginMessage message) {
ClientPlaySessionHandler playerHandler =
(ClientPlaySessionHandler) connection.getPlayer().getConnection().getSessionHandler();
boolean isMCMessage;
boolean isMCOrFMLMessage;
if (connection.getMinecraftConnection().getProtocolVersion() <= ProtocolConstants.MINECRAFT_1_12_2) {
isMCMessage = message.getChannel().startsWith("MC|");
String channel = message.getChannel();
isMCOrFMLMessage = channel.startsWith("MC|") || channel.startsWith(VelocityConstants.FORGE_LEGACY_HANDSHAKE_CHANNEL);
} else {
isMCMessage = message.getChannel().startsWith("minecraft:");
isMCOrFMLMessage = message.getChannel().startsWith("minecraft:");
}
return isMCMessage || playerHandler.getClientPluginMsgChannels().contains(message.getChannel()) ||
return isMCOrFMLMessage || playerHandler.getClientPluginMsgChannels().contains(message.getChannel()) ||
server.getChannelRegistrar().registered(message.getChannel());
}
}

Datei anzeigen

@ -84,6 +84,10 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
connection.getPlayer().getConnection().setSessionHandler(new ClientPlaySessionHandler(server, connection.getPlayer()));
} else {
// The previous server connection should become obsolete.
// Before we remove it, if the server we are departing is modded, we must always reset the client state.
if (existingConnection.isLegacyForge()) {
connection.getPlayer().sendLegacyForgeHandshakeResetPacket();
}
existingConnection.disconnect();
}
@ -119,7 +123,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
}
}
static ByteBuf createForwardingData(byte[] hmacSecret, String address, GameProfile profile) {
private static ByteBuf createForwardingData(byte[] hmacSecret, String address, GameProfile profile) {
ByteBuf dataToForward = Unpooled.buffer();
ByteBuf finalData = Unpooled.buffer();
try {

Datei anzeigen

@ -33,7 +33,6 @@ import static com.velocitypowered.proxy.network.Connections.HANDLER;
import static com.velocitypowered.proxy.network.Connections.MINECRAFT_DECODER;
import static com.velocitypowered.proxy.network.Connections.MINECRAFT_ENCODER;
import static com.velocitypowered.proxy.network.Connections.READ_TIMEOUT;
import static com.velocitypowered.proxy.network.Connections.SERVER_READ_TIMEOUT_SECONDS;
public class VelocityServerConnection implements MinecraftConnectionAssociation, ServerConnection {
static final AttributeKey<CompletableFuture<ConnectionRequestBuilder.Result>> CONNECTION_NOTIFIER =
@ -43,6 +42,9 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
private final ConnectedPlayer proxyPlayer;
private final VelocityServer server;
private MinecraftConnection minecraftConnection;
private boolean legacyForge = false;
private boolean hasCompletedJoin = false;
private boolean gracefulDisconnect = false;
public VelocityServerConnection(ServerInfo target, ConnectedPlayer proxyPlayer, VelocityServer server) {
this.serverInfo = target;
@ -57,7 +59,7 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline()
.addLast(READ_TIMEOUT, new ReadTimeoutHandler(SERVER_READ_TIMEOUT_SECONDS, TimeUnit.SECONDS))
.addLast(READ_TIMEOUT, new ReadTimeoutHandler(server.getConfiguration().getReadTimeout(), TimeUnit.SECONDS))
.addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder())
.addLast(FRAME_ENCODER, MinecraftVarintLengthEncoder.INSTANCE)
.addLast(MINECRAFT_DECODER, new MinecraftDecoder(ProtocolConstants.Direction.CLIENTBOUND))
@ -107,6 +109,8 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
handshake.setProtocolVersion(proxyPlayer.getConnection().getProtocolVersion());
if (forwardingMode == PlayerInfoForwarding.LEGACY) {
handshake.setServerAddress(createBungeeForwardingAddress());
} else if (proxyPlayer.getConnection().isLegacyForge()) {
handshake.setServerAddress(handshake.getServerAddress() + "\0FML\0");
} else {
handshake.setServerAddress(serverInfo.getAddress().getHostString());
}
@ -122,6 +126,12 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
minecraftConnection.write(login);
}
public void writeIfJoined(PluginMessage message) {
if (hasCompletedJoin) {
minecraftConnection.write(message);
}
}
public MinecraftConnection getMinecraftConnection() {
return minecraftConnection;
}
@ -136,8 +146,11 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
}
public void disconnect() {
minecraftConnection.close();
minecraftConnection = null;
if (minecraftConnection != null) {
minecraftConnection.close();
minecraftConnection = null;
gracefulDisconnect = true;
}
}
@Override
@ -154,4 +167,24 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
message.setData(data);
minecraftConnection.write(message);
}
public boolean isLegacyForge() {
return legacyForge;
}
public void setLegacyForge(boolean modded) {
legacyForge = modded;
}
public boolean hasCompletedJoin() {
return hasCompletedJoin;
}
public void setHasCompletedJoin(boolean hasCompletedJoin) {
this.hasCompletedJoin = hasCompletedJoin;
}
public boolean isGracefulDisconnect() {
return gracefulDisconnect;
}
}

Datei anzeigen

@ -3,6 +3,8 @@ package com.velocitypowered.proxy.connection.client;
import com.velocitypowered.api.event.connection.DisconnectEvent;
import com.velocitypowered.api.event.connection.PluginMessageEvent;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.VelocityConstants;
import com.velocitypowered.proxy.connection.backend.VelocityServerConnection;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.packet.*;
@ -23,7 +25,7 @@ import java.util.*;
*/
public class ClientPlaySessionHandler implements MinecraftSessionHandler {
private static final Logger logger = LogManager.getLogger(ClientPlaySessionHandler.class);
private static final int MAX_PLUGIN_CHANNELS = 128;
private static final int MAX_PLUGIN_CHANNELS = 1024;
private final ConnectedPlayer player;
private long lastPingID = -1;
@ -31,6 +33,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
private boolean spawned = false;
private final List<UUID> serverBossBars = new ArrayList<>();
private final Set<String> clientPluginMsgChannels = new HashSet<>();
private final Queue<PluginMessage> loginPluginMessages = new ArrayDeque<>();
private final VelocityServer server;
public ClientPlaySessionHandler(VelocityServer server, ConnectedPlayer player) {
@ -51,6 +54,12 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
@Override
public void handle(MinecraftPacket packet) {
VelocityServerConnection serverConnection = player.getConnectedServer();
if (serverConnection == null) {
// No server connection yet, probably transitioning.
return;
}
if (packet instanceof KeepAlive) {
KeepAlive keepAlive = (KeepAlive) packet;
if (keepAlive.getRandomId() != lastPingID) {
@ -60,6 +69,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
}
player.setPing(System.currentTimeMillis() - lastPingSent);
resetPingData();
serverConnection.getMinecraftConnection().write(packet);
return;
}
if (packet instanceof ClientSettings) {
@ -106,7 +117,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
player.getConnection().write(response);
} else {
player.getConnectedServer().getMinecraftConnection().write(packet);
serverConnection.getMinecraftConnection().write(packet);
}
} catch (Exception e) {
logger.error("Unable to provide tab list completions for " + player.getUsername() + " for command '" + req.getCommand() + "'", e);
@ -121,12 +132,22 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
}
// If we don't want to handle this packet, just forward it on.
player.getConnectedServer().getMinecraftConnection().write(packet);
if (serverConnection.hasCompletedJoin()) {
serverConnection.getMinecraftConnection().write(packet);
}
}
@Override
public void handleUnknown(ByteBuf buf) {
player.getConnectedServer().getMinecraftConnection().write(buf.retain());
VelocityServerConnection serverConnection = player.getConnectedServer();
if (serverConnection == null) {
// No server connection yet, probably transitioning.
return;
}
if (serverConnection.hasCompletedJoin()) {
serverConnection.getMinecraftConnection().write(buf.retain());
}
}
@Override
@ -144,12 +165,31 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
.build());
}
@Override
public void writabilityChanged() {
VelocityServerConnection server = player.getConnectedServer();
if (server != null) {
boolean writable = player.getConnection().getChannel().isWritable();
server.getMinecraftConnection().getChannel().config().setAutoRead(writable);
}
}
public void handleBackendJoinGame(JoinGame joinGame) {
resetPingData(); // reset ping data;
resetPingData(); // reset ping data
if (!spawned) {
// nothing special to do here
// Nothing special to do with regards to spawning the player
spawned = true;
player.getConnection().delayedWrite(joinGame);
// We have something special to do for legacy Forge servers - during first connection the FML handshake
// will transition to complete regardless. Thus, we need to ensure that a reset packet is ALWAYS sent on
// first switch.
//
// As we know that calling this branch only happens on first join, we set that if we are a Forge
// client that we must reset on the next switch.
//
// The call will handle if the player is not a Forge player appropriately.
player.getConnection().setCanSendLegacyFMLResetPacket(true);
} else {
// Ah, this is the meat and potatoes of the whole venture!
//
@ -193,17 +233,38 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
channel, toRegister));
}
// If we had plugin messages queued during login/FML handshake, send them now.
PluginMessage pm;
while ((pm = loginPluginMessages.poll()) != null) {
player.getConnectedServer().getMinecraftConnection().delayedWrite(pm);
}
// Flush everything
player.getConnection().flush();
player.getConnectedServer().getMinecraftConnection().flush();
player.getConnectedServer().setHasCompletedJoin(true);
if (player.getConnectedServer().isLegacyForge()) {
// We only need to indicate we can send a reset packet if we complete a handshake, that is,
// logged onto a Forge server.
//
// The special case is if we log onto a Vanilla server as our first server, FML will treat this
// as complete and **will** need a reset packet sending at some point. We will handle this
// during initial player connection if the player is detected to be forge.
//
// This is why we use an if statement rather than the result of VelocityServerConnection#isLegacyForge()
// because we don't want to set it false if this is a first connection to a Vanilla server.
//
// See LoginSessionHandler#handle for where the counterpart to this method is
player.getConnection().setCanSendLegacyFMLResetPacket(true);
}
}
public List<UUID> getServerBossBars() {
return serverBossBars;
}
public void handleClientPluginMessage(PluginMessage packet) {
if (packet.getChannel().equals("REGISTER") || packet.getChannel().equals("minecraft:register")) {
private void handleClientPluginMessage(PluginMessage packet) {
if (PluginMessageUtil.isMCRegister(packet)) {
List<String> actuallyRegistered = new ArrayList<>();
List<String> channels = PluginMessageUtil.getChannels(packet);
for (String channel : channels) {
@ -220,28 +281,32 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
PluginMessage newRegisterPacket = PluginMessageUtil.constructChannelsPacket(packet.getChannel(), actuallyRegistered);
player.getConnectedServer().getMinecraftConnection().write(newRegisterPacket);
}
return;
}
if (packet.getChannel().equals("UNREGISTER") || packet.getChannel().equals("minecraft:unregister")) {
} else if (PluginMessageUtil.isMCUnregister(packet)) {
List<String> channels = PluginMessageUtil.getChannels(packet);
clientPluginMsgChannels.removeAll(channels);
}
if (PluginMessageUtil.isMCBrand(packet)) {
player.getConnectedServer().getMinecraftConnection().write(packet);
} else if (PluginMessageUtil.isMCBrand(packet)) {
player.getConnectedServer().getMinecraftConnection().write(PluginMessageUtil.rewriteMCBrand(packet));
return;
} else if (player.getConnectedServer().isLegacyForge() && !player.getConnectedServer().hasCompletedJoin()) {
if (packet.getChannel().equals(VelocityConstants.FORGE_LEGACY_HANDSHAKE_CHANNEL)) {
// Always forward the FML handshake to the remote server.
player.getConnectedServer().getMinecraftConnection().write(packet);
} else {
// The client is trying to send messages too early. This is primarily caused by mods, but it's further
// aggravated by Velocity. To work around these issues, we will queue any non-FML handshake messages to
// be sent once the JoinGame packet has been received by the proxy.
loginPluginMessages.add(packet);
}
} else {
PluginMessageEvent event = new PluginMessageEvent(player, player.getConnectedServer(),
server.getChannelRegistrar().getFromId(packet.getChannel()), packet.getData());
server.getEventManager().fire(event)
.thenAcceptAsync(pme -> {
if (pme.getResult().isAllowed()) {
player.getConnectedServer().getMinecraftConnection().write(packet);
}
}, player.getConnectedServer().getMinecraftConnection().getChannel().eventLoop());
}
PluginMessageEvent event = new PluginMessageEvent(player, player.getConnectedServer(),
server.getChannelRegistrar().getFromId(packet.getChannel()), packet.getData());
server.getEventManager().fire(event)
.thenAcceptAsync(pme -> {
if (pme.getResult().isAllowed()) {
player.getConnectedServer().getMinecraftConnection().write(packet);
}
}, player.getConnectedServer().getMinecraftConnection().getChannel().eventLoop());
}
public Set<String> getClientPluginMsgChannels() {

Datei anzeigen

@ -12,7 +12,7 @@ public class ClientSettingsWrapper implements PlayerSettings {
private final SkinParts parts;
private Locale locale = null;
public ClientSettingsWrapper(ClientSettings settings) {
ClientSettingsWrapper(ClientSettings settings) {
this.settings = settings;
this.parts = new SkinParts((byte) settings.getSkinParts());
}

Datei anzeigen

@ -2,6 +2,7 @@ package com.velocitypowered.proxy.connection.client;
import com.google.common.base.Preconditions;
import com.google.gson.JsonObject;
import com.velocitypowered.api.event.player.KickedFromServerEvent;
import com.velocitypowered.api.event.player.PlayerSettingsChangedEvent;
import com.velocitypowered.api.event.player.ServerPreConnectEvent;
import com.velocitypowered.api.permission.PermissionFunction;
@ -14,6 +15,7 @@ import com.velocitypowered.api.util.MessagePosition;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation;
import com.velocitypowered.proxy.connection.VelocityConstants;
import com.velocitypowered.proxy.connection.util.ConnectionMessages;
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults;
import com.velocitypowered.api.util.GameProfile;
@ -36,6 +38,7 @@ import net.kyori.text.serializer.PlainComponentSerializer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.net.InetSocketAddress;
import java.util.List;
@ -190,7 +193,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
logger.error("{}: unable to connect to server {}", this, info.getName(), throwable);
userMessage = "Exception connecting to server " + info.getName();
}
handleConnectionException(info, TextComponent.builder()
handleConnectionException(info, null, TextComponent.builder()
.content(userMessage + ": ")
.color(TextColor.RED)
.append(TextComponent.of(error, TextColor.WHITE))
@ -202,17 +205,23 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
String plainTextReason = PASS_THRU_TRANSLATE.serialize(disconnectReason);
if (connectedServer != null && connectedServer.getServerInfo().equals(info)) {
logger.error("{}: kicked from server {}: {}", this, info.getName(), plainTextReason);
handleConnectionException(info, disconnectReason, TextComponent.builder()
.content("Kicked from " + info.getName() + ": ")
.color(TextColor.RED)
.append(disconnectReason)
.build());
} else {
logger.error("{}: disconnected while connecting to {}: {}", this, info.getName(), plainTextReason);
handleConnectionException(info, disconnectReason, TextComponent.builder()
.content("Unable to connect to " + info.getName() + ": ")
.color(TextColor.RED)
.append(disconnectReason)
.build());
}
handleConnectionException(info, TextComponent.builder()
.content("Unable to connect to " + info.getName() + ": ")
.color(TextColor.RED)
.append(disconnectReason)
.build());
}
public void handleConnectionException(ServerInfo info, Component disconnectReason) {
private void handleConnectionException(ServerInfo info, @Nullable Component kickReason, Component friendlyReason) {
boolean alreadyConnected = connectedServer != null && connectedServer.getServerInfo().equals(info);;
connectionInFlight = null;
if (connectedServer == null) {
// The player isn't yet connected to a server.
@ -220,14 +229,29 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
if (nextServer.isPresent()) {
createConnectionRequest(nextServer.get()).fireAndForget();
} else {
connection.closeWith(Disconnect.create(disconnectReason));
connection.closeWith(Disconnect.create(friendlyReason));
}
} else if (connectedServer.getServerInfo().equals(info)) {
// Already connected to the server being disconnected from.
// TODO: ServerKickEvent
connection.closeWith(Disconnect.create(disconnectReason));
if (kickReason != null) {
server.getEventManager().fire(new KickedFromServerEvent(this, info, kickReason, !alreadyConnected, friendlyReason))
.thenAcceptAsync(event -> {
if (event.getResult() instanceof KickedFromServerEvent.DisconnectPlayer) {
KickedFromServerEvent.DisconnectPlayer res = (KickedFromServerEvent.DisconnectPlayer) event.getResult();
connection.closeWith(Disconnect.create(res.getReason()));
} else if (event.getResult() instanceof KickedFromServerEvent.RedirectPlayer) {
KickedFromServerEvent.RedirectPlayer res = (KickedFromServerEvent.RedirectPlayer) event.getResult();
createConnectionRequest(res.getServer()).fireAndForget();
} else {
// In case someone gets creative, assume we want to disconnect the player.
connection.closeWith(Disconnect.create(friendlyReason));
}
}, connection.getChannel().eventLoop());
} else {
connection.closeWith(Disconnect.create(friendlyReason));
}
} else {
connection.write(Chat.create(disconnectReason));
connection.write(Chat.create(friendlyReason));
}
}
@ -256,7 +280,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
}
// Otherwise, initiate the connection.
ServerPreConnectEvent event = new ServerPreConnectEvent(this, ServerPreConnectEvent.ServerResult.allowed(request.getServer()));
ServerPreConnectEvent event = new ServerPreConnectEvent(this, request.getServer());
return server.getEventManager().fire(event)
.thenCompose((newEvent) -> {
if (!newEvent.getResult().isAllowed()) {
@ -265,7 +289,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
);
}
return new VelocityServerConnection(newEvent.getResult().getInfo().get(), this, server).connect();
return new VelocityServerConnection(newEvent.getResult().getServer().get(), this, server).connect();
});
}
@ -276,6 +300,16 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
this.connectedServer = serverConnection;
}
public void sendLegacyForgeHandshakeResetPacket() {
if (connection.canSendLegacyFMLResetPacket()) {
PluginMessage resetPacket = new PluginMessage();
resetPacket.setChannel(VelocityConstants.FORGE_LEGACY_HANDSHAKE_CHANNEL);
resetPacket.setData(VelocityConstants.FORGE_LEGACY_HANDSHAKE_RESET_DATA);
connection.write(resetPacket);
connection.setCanSendLegacyFMLResetPacket(false);
}
}
public void close(TextComponent reason) {
connection.closeWith(Disconnect.create(reason));
}

Datei anzeigen

@ -70,9 +70,12 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
return;
}
// Make sure legacy forwarding is not in use on this connection. Make sure that we do _not_ reject Forge,
// although Velocity does not yet support Forge.
if (handshake.getServerAddress().contains("\0") && !handshake.getServerAddress().endsWith("\0FML\0")) {
// Determine if we're using Forge (1.8 to 1.12, may not be the case in 1.13) and store that in the connection
boolean isForge = handshake.getServerAddress().endsWith("\0FML\0");
connection.setLegacyForge(isForge);
// Make sure legacy forwarding is not in use on this connection. Make sure that we do _not_ reject Forge
if (handshake.getServerAddress().contains("\0") && !isForge) {
connection.closeWith(Disconnect.create(TextComponent.of("Running Velocity behind Velocity is unsupported.")));
return;
}
@ -105,6 +108,7 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
new ServerPing.Version(ProtocolConstants.MAXIMUM_GENERIC_VERSION, "Velocity " + ProtocolConstants.SUPPORTED_GENERIC_VERSION_STRING),
new ServerPing.Players(server.getPlayerCount(), configuration.getShowMaxPlayers(), ImmutableList.of()),
configuration.getMotdComponent(),
null,
null
);
ProxyPingEvent event = new ProxyPingEvent(new LegacyInboundConnection(connection), ping);

Datei anzeigen

@ -2,12 +2,14 @@ package com.velocitypowered.proxy.connection.client;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.event.connection.LoginEvent;
import com.velocitypowered.api.event.connection.PostLoginEvent;
import com.velocitypowered.api.event.connection.PreLoginEvent;
import com.velocitypowered.api.event.connection.PreLoginEvent.PreLoginComponentResult;
import com.velocitypowered.api.event.permission.PermissionsSetupEvent;
import com.velocitypowered.api.event.player.GameProfileRequestEvent;
import com.velocitypowered.api.proxy.InboundConnection;
import com.velocitypowered.api.proxy.server.ServerInfo;
import com.velocitypowered.proxy.config.PlayerInfoForwarding;
import com.velocitypowered.proxy.connection.VelocityConstants;
import com.velocitypowered.api.util.GameProfile;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
@ -31,11 +33,14 @@ import java.net.MalformedURLException;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ThreadLocalRandom;
public class LoginSessionHandler implements MinecraftSessionHandler {
private static final Logger logger = LogManager.getLogger(LoginSessionHandler.class);
private static final String MOJANG_SERVER_AUTH_URL =
"https://sessionserver.mojang.com/session/minecraft/hasJoined?username=%s&serverId=%s&ip=%s";
@ -154,7 +159,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
return;
}
if (server.getConfiguration().isOnlineMode() || result.isOnlineModeAllowed()) {
if (!result.isForceOfflineMode() && (server.getConfiguration().isOnlineMode() || result.isOnlineModeAllowed())) {
// Request encryption.
EncryptionRequest request = generateRequest();
this.verify = Arrays.copyOf(request.getVerifyToken(), 4);
@ -176,6 +181,12 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
}
private void initializePlayer(GameProfile profile, boolean onlineMode) {
if (inbound.isLegacyForge() && server.getConfiguration().getPlayerInfoForwardingMode() == PlayerInfoForwarding.LEGACY) {
// We want to add the FML token to the properties
List<GameProfile.Property> properties = new ArrayList<>(profile.getProperties());
properties.add(new GameProfile.Property("forgeClient", "true", ""));
profile = new GameProfile(profile.getId(), profile.getName(), properties);
}
GameProfileRequestEvent profileRequestEvent = new GameProfileRequestEvent(apiInbound, profile, onlineMode);
server.getEventManager().fire(profileRequestEvent).thenCompose(profileEvent -> {
@ -235,7 +246,9 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
logger.info("{} has connected", player);
inbound.setSessionHandler(new InitialConnectSessionHandler(player));
player.createConnectionRequest(toTry.get()).fireAndForget();
server.getEventManager().fire(new PostLoginEvent(player)).thenRun(() -> {
player.createConnectionRequest(toTry.get()).fireAndForget();
});
}
@Override

Datei anzeigen

@ -48,7 +48,8 @@ public class StatusSessionHandler implements MinecraftSessionHandler {
new ServerPing.Version(shownVersion, "Velocity " + ProtocolConstants.SUPPORTED_GENERIC_VERSION_STRING),
new ServerPing.Players(server.getPlayerCount(), configuration.getShowMaxPlayers(), ImmutableList.of()),
configuration.getMotdComponent(),
configuration.getFavicon()
configuration.getFavicon(),
configuration.isAnnounceForge() ? ServerPing.Modinfo.DEFAULT : null
);
ProxyPingEvent event = new ProxyPingEvent(inboundWrapper, initialPing);

Datei anzeigen

@ -7,6 +7,7 @@ public class ConnectionMessages {
public static final TextComponent ALREADY_CONNECTED = TextComponent.of("You are already connected to this server!", TextColor.RED);
public static final TextComponent IN_PROGRESS = TextComponent.of("You are already connecting to a server!", TextColor.RED);
public static final TextComponent INTERNAL_SERVER_CONNECTION_ERROR = TextComponent.of("Internal server connection error");
public static final TextComponent UNEXPECTED_DISCONNECT = TextComponent.of("Unexpectedly disconnected from server - crash?");
private ConnectionMessages() {
throw new AssertionError();

Datei anzeigen

@ -16,11 +16,7 @@ import com.velocitypowered.proxy.protocol.netty.MinecraftVarintFrameDecoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftVarintLengthEncoder;
import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.*;
import io.netty.channel.epoll.Epoll;
import io.netty.channel.epoll.EpollDatagramChannel;
import io.netty.channel.epoll.EpollEventLoopGroup;
@ -48,6 +44,8 @@ import java.util.concurrent.TimeUnit;
import static com.velocitypowered.proxy.network.Connections.*;
public final class ConnectionManager {
private static final WriteBufferWaterMark SERVER_WRITE_MARK = new WriteBufferWaterMark(1 << 16, 1 << 18);
private static final Logger logger = LogManager.getLogger(ConnectionManager.class);
private final Set<Channel> endpoints = new HashSet<>();
@ -72,11 +70,12 @@ public final class ConnectionManager {
final ServerBootstrap bootstrap = new ServerBootstrap()
.channel(this.transportType.serverSocketChannelClass)
.group(this.bossGroup, this.workerGroup)
.childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, SERVER_WRITE_MARK)
.childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(final Channel ch) {
ch.pipeline()
.addLast(READ_TIMEOUT, new ReadTimeoutHandler(CLIENT_READ_TIMEOUT_SECONDS, TimeUnit.SECONDS))
.addLast(READ_TIMEOUT, new ReadTimeoutHandler(server.getConfiguration().getReadTimeout(), TimeUnit.SECONDS))
.addLast(LEGACY_PING_DECODER, new LegacyPingDecoder())
.addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder())
.addLast(LEGACY_PING_ENCODER, LegacyPingEncoder.INSTANCE)
@ -126,7 +125,9 @@ public final class ConnectionManager {
public Bootstrap createWorker() {
return new Bootstrap()
.channel(this.transportType.socketChannelClass)
.group(this.workerGroup);
.group(this.workerGroup)
.option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, server.getConfiguration().getConnectTimeout());
}
public void shutdown() {

Datei anzeigen

@ -13,7 +13,4 @@ public interface Connections {
String MINECRAFT_DECODER = "minecraft-decoder";
String MINECRAFT_ENCODER = "minecraft-encoder";
String READ_TIMEOUT = "read-timeout";
int CLIENT_READ_TIMEOUT_SECONDS = 30; // client -> proxy
int SERVER_READ_TIMEOUT_SECONDS = 30; // proxy -> server
}

Datei anzeigen

@ -1,5 +1,6 @@
package com.velocitypowered.proxy.protocol;
import com.google.common.base.Strings;
import com.google.common.primitives.ImmutableIntArray;
import com.velocitypowered.proxy.protocol.packet.*;
import io.netty.util.collection.IntObjectHashMap;
@ -258,9 +259,18 @@ public enum StateRegistry {
@Override
public String toString() {
StringBuilder mappingAsString = new StringBuilder("{");
for (Object2IntMap.Entry<Class<? extends MinecraftPacket>> entry : packetClassToId.object2IntEntrySet()) {
mappingAsString.append(entry.getKey().getSimpleName()).append(" -> ")
.append("0x")
.append(Strings.padStart(Integer.toHexString(entry.getIntValue()), 2, '0'))
.append(", ");
}
mappingAsString.setLength(mappingAsString.length() - 2);
mappingAsString.append("}");
return "ProtocolVersion{" +
"id=" + id +
", packetClassToId=" + packetClassToId +
", packetClassToId=" + mappingAsString.toString() +
'}';
}
}

Datei anzeigen

@ -38,11 +38,11 @@ public class MinecraftDecoder extends MessageToMessageDecoder<ByteBuf> {
packet.decode(msg, direction, protocolVersion.id);
} catch (Exception e) {
throw new CorruptedFrameException("Error decoding " + packet.getClass() + " Direction " + direction
+ " Protocol " + protocolVersion + " State " + state + " ID " + Integer.toHexString(packetId), e);
+ " Protocol " + protocolVersion.id + " State " + state + " ID " + Integer.toHexString(packetId), e);
}
if (msg.isReadable()) {
throw new CorruptedFrameException("Did not read full packet for " + packet.getClass() + " Direction " + direction
+ " Protocol " + protocolVersion + " State " + state + " ID " + Integer.toHexString(packetId));
+ " Protocol " + protocolVersion.id + " State " + state + " ID " + Integer.toHexString(packetId));
}
out.add(packet);
}

Datei anzeigen

@ -2,8 +2,10 @@ package com.velocitypowered.proxy.protocol.netty;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.CorruptedFrameException;
import java.util.List;
@ -15,12 +17,31 @@ public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder {
}
in.markReaderIndex();
int packetLength = ProtocolUtils.readVarInt(in);
if (in.readableBytes() < packetLength) {
in.resetReaderIndex();
return;
byte[] lenBuf = new byte[3];
for (int i = 0; i < lenBuf.length; i++) {
if (!in.isReadable()) {
in.resetReaderIndex();
return;
}
lenBuf[i] = in.readByte();
if (lenBuf[i] > 0) {
int packetLength = ProtocolUtils.readVarInt(Unpooled.wrappedBuffer(lenBuf));
if (packetLength == 0) {
return;
}
if (in.readableBytes() < packetLength) {
in.resetReaderIndex();
return;
}
out.add(in.readRetainedSlice(packetLength));
return;
}
}
out.add(in.readRetainedSlice(packetLength));
throw new CorruptedFrameException("VarInt too big");
}
}

Datei anzeigen

@ -20,13 +20,20 @@ public enum PluginMessageUtil {
return message.getChannel().equals("MC|Brand") || message.getChannel().equals("minecraft:brand");
}
public static boolean isMCRegister(PluginMessage message) {
Preconditions.checkNotNull(message, "message");
return message.getChannel().equals("REGISTER") || message.getChannel().equals("minecraft:register");
}
public static boolean isMCUnregister(PluginMessage message) {
Preconditions.checkNotNull(message, "message");
return message.getChannel().equals("UNREGISTER") || message.getChannel().equals("minecraft:unregister");
}
public static List<String> getChannels(PluginMessage message) {
Preconditions.checkNotNull(message, "message");
Preconditions.checkArgument(message.getChannel().equals("REGISTER") ||
message.getChannel().equals("UNREGISTER") ||
message.getChannel().equals("minecraft:register") ||
message.getChannel().equals("minecraft:unregister"),
"Unknown channel type " + message.getChannel());
Preconditions.checkArgument(isMCRegister(message) || isMCUnregister(message),"Unknown channel type %s",
message.getChannel());
String channels = new String(message.getData(), StandardCharsets.UTF_8);
return ImmutableList.copyOf(channels.split("\0"));
}

Datei anzeigen

@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import static org.junit.jupiter.api.Assertions.*;
@ -33,4 +34,36 @@ class RecordingThreadFactoryTest {
Thread.sleep(10);
assertEquals(0, factory.size());
}
@Test
void cleanUpAfterExceptionThrown() throws Exception {
CountDownLatch started = new CountDownLatch(1);
CountDownLatch endThread = new CountDownLatch(1);
CountDownLatch hasEnded = new CountDownLatch(1);
RecordingThreadFactory factory = new RecordingThreadFactory((ThreadFactory) r -> {
Thread t = new Thread(r);
t.setUncaughtExceptionHandler((t1, e) -> hasEnded.countDown());
return t;
});
factory.newThread(() -> {
started.countDown();
assertTrue(factory.currentlyInFactory());
assertEquals(1, factory.size());
try {
endThread.await();
} catch (InterruptedException e) {
fail(e);
}
throw new RuntimeException("");
}).start();
started.await();
assertFalse(factory.currentlyInFactory());
assertEquals(1, factory.size());
endThread.countDown();
hasEnded.await();
// Wait a little bit to ensure the thread got shut down
Thread.sleep(10);
assertEquals(0, factory.size());
}
}