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. 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. 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 Forge is fully supported but mod compatibility may vary.
vanilla fallback mode.

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.google.common.base.Preconditions;
import com.velocitypowered.api.event.ResultedEvent; import com.velocitypowered.api.event.ResultedEvent;
import com.velocitypowered.api.proxy.InboundConnection; import com.velocitypowered.api.proxy.InboundConnection;
import java.util.Optional;
import net.kyori.text.Component; import net.kyori.text.Component;
import net.kyori.text.serializer.ComponentSerializers;
import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable; 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 { public static class PreLoginComponentResult implements ResultedEvent.Result {
private static final PreLoginComponentResult ALLOWED = new PreLoginComponentResult((Component) null);
private static final PreLoginComponentResult FORCE_ONLINEMODE = new PreLoginComponentResult(true);
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);
/** private final Result result;
* Allows online mode to be enabled for the player connection, if Velocity is running in offline mode. private final Optional<Component> reason;
* @param allowedOnlineMode if true, online mode will be used for the connection
*/ private PreLoginComponentResult(Result result, @Nullable Component reason) {
private PreLoginComponentResult(boolean allowedOnlineMode) { this.result = result;
super(true, null); this.reason = Optional.ofNullable(reason);
this.onlineMode = allowedOnlineMode;
} }
private PreLoginComponentResult(@Nullable Component reason) { @Override
super(reason == null, reason); public boolean isAllowed() {
// Don't care about this return result != Result.DISALLOWED;
this.onlineMode = false; }
public Optional<Component> getReason() {
return reason;
} }
public boolean isOnlineModeAllowed() { public boolean isOnlineModeAllowed() {
return this.onlineMode; return result == Result.FORCE_ONLINE;
}
public boolean isForceOfflineMode() {
return result == Result.FORCE_OFFLINE;
} }
@Override @Override
public String toString() { public String toString() {
if (isForceOfflineMode()) {
return "allowed with force offline mode";
}
if (isOnlineModeAllowed()) { if (isOnlineModeAllowed()) {
return "allowed with online mode"; return "allowed with online mode";
} }
if (isAllowed()) {
return super.toString(); 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 * @return the allowed result
*/ */
public static PreLoginComponentResult allowed() { 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 * Returns a result indicating the connection will be allowed through
* forced to use online mode provided that the proxy is in offline mode. This acts similarly to {@link #allowed()} * the proxy, but the connection will be forced to use online mode
* on an online-mode proxy. * provided that the proxy is in offline mode. This acts similarly to
* {@link #allowed()} on an online-mode proxy.
* @return the result * @return the result
*/ */
public static PreLoginComponentResult forceOnlineMode() { public static PreLoginComponentResult forceOnlineMode() {
return FORCE_ONLINEMODE; 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. * Denies the login with the specified reason.
* @param reason the reason for disallowing the connection * @param reason the reason for disallowing the connection
* @return a new result * @return a new result
*/ */
public static PreLoginComponentResult denied(@NonNull Component reason) { public static PreLoginComponentResult denied(Component reason) {
Preconditions.checkNotNull(reason, "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> { public class ServerPreConnectEvent implements ResultedEvent<ServerPreConnectEvent.ServerResult> {
private final Player player; private final Player player;
private final ServerInfo originalServer;
private ServerResult result; private ServerResult result;
public ServerPreConnectEvent(Player player, ServerResult result) { public ServerPreConnectEvent(Player player, ServerInfo originalServer) {
this.player = Preconditions.checkNotNull(player, "player"); 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() { public Player getPlayer() {
@ -35,10 +37,15 @@ public class ServerPreConnectEvent implements ResultedEvent<ServerPreConnectEven
this.result = Preconditions.checkNotNull(result, "result"); this.result = Preconditions.checkNotNull(result, "result");
} }
public ServerInfo getOriginalServer() {
return originalServer;
}
@Override @Override
public String toString() { public String toString() {
return "ServerPreConnectEvent{" + return "ServerPreConnectEvent{" +
"player=" + player + "player=" + player +
", originalServer=" + originalServer +
", result=" + result + ", result=" + result +
'}'; '}';
} }
@ -50,11 +57,11 @@ public class ServerPreConnectEvent implements ResultedEvent<ServerPreConnectEven
private static final ServerResult DENIED = new ServerResult(false, null); private static final ServerResult DENIED = new ServerResult(false, null);
private final boolean allowed; 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.allowed = allowed;
this.info = info; this.server = server;
} }
@Override @Override
@ -62,8 +69,8 @@ public class ServerPreConnectEvent implements ResultedEvent<ServerPreConnectEven
return allowed; return allowed;
} }
public Optional<ServerInfo> getInfo() { public Optional<ServerInfo> getServer() {
return Optional.ofNullable(info); return Optional.ofNullable(server);
} }
@Override @Override
@ -71,7 +78,7 @@ public class ServerPreConnectEvent implements ResultedEvent<ServerPreConnectEven
if (!allowed) { if (!allowed) {
return "denied"; return "denied";
} }
return "allowed: connect to " + info.getName(); return "allowed: connect to " + server.getName();
} }
public static ServerResult denied() { public static ServerResult denied() {

Datei anzeigen

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

Datei anzeigen

@ -13,8 +13,8 @@ allprojects {
// dependency versions // dependency versions
junitVersion = '5.3.0-M1' junitVersion = '5.3.0-M1'
slf4jVersion = '1.7.25' slf4jVersion = '1.7.25'
log4jVersion = '2.11.0' log4jVersion = '2.11.1'
nettyVersion = '4.1.28.Final' nettyVersion = '4.1.29.Final'
guavaVersion = '25.1-jre' guavaVersion = '25.1-jre'
getCurrentBranchName = { getCurrentBranchName = {
@ -27,6 +27,17 @@ allprojects {
return os.toString().trim() 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 { repositories {

Datei anzeigen

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists 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 { jar {
manifest { 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 '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) { public static void main(String... args) {
startTime = System.currentTimeMillis(); startTime = System.currentTimeMillis();
logger.info("Booting up Velocity..."); logger.info("Booting up Velocity {}...", Velocity.class.getPackage().getImplementationVersion());
VelocityServer server = new VelocityServer(); VelocityServer server = new VelocityServer();
server.start(); server.start();

Datei anzeigen

@ -29,7 +29,7 @@ public class VelocityCommand implements Command {
.append(TextComponent.of(" or the ").resetStyle()) .append(TextComponent.of(" or the ").resetStyle())
.append(TextComponent.builder("Velocity GitHub") .append(TextComponent.builder("Velocity GitHub")
.color(TextColor.GREEN) .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())
.build(); .build();

Datei anzeigen

@ -1,5 +1,6 @@
package com.velocitypowered.proxy.config; package com.velocitypowered.proxy.config;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.moandjiezana.toml.Toml; import com.moandjiezana.toml.Toml;
import com.velocitypowered.api.util.Favicon; import com.velocitypowered.api.util.Favicon;
@ -48,7 +49,7 @@ public class VelocityConfiguration extends AnnotatedConfig {
@Comment({ @Comment({
"Should we forward IP addresses and other data to backend servers?", "Should we forward IP addresses and other data to backend servers?",
"Available options:", "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.", " and will have offline-mode UUIDs.",
"- \"legacy\": Forward player IPs and UUIDs in BungeeCord-compatible fashion. Use this if you run", "- \"legacy\": Forward player IPs and UUIDs in BungeeCord-compatible fashion. Use this if you run",
" servers using Minecraft 1.12 or lower.", " servers using Minecraft 1.12 or lower.",
@ -62,6 +63,10 @@ public class VelocityConfiguration extends AnnotatedConfig {
@ConfigKey("forwarding-secret") @ConfigKey("forwarding-secret")
private byte[] forwardingSecret = generateRandomString(12).getBytes(StandardCharsets.UTF_8); 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]") @Table("[servers]")
private final Servers servers; private final Servers servers;
@ -83,12 +88,13 @@ public class VelocityConfiguration extends AnnotatedConfig {
} }
private VelocityConfiguration(String bind, String motd, int showMaxPlayers, boolean onlineMode, private VelocityConfiguration(String bind, String motd, int showMaxPlayers, boolean onlineMode,
PlayerInfoForwarding playerInfoForwardingMode, byte[] forwardingSecret, Servers servers, boolean announceForge, PlayerInfoForwarding playerInfoForwardingMode, byte[] forwardingSecret,
Advanced advanced, Query query) { Servers servers, Advanced advanced, Query query) {
this.bind = bind; this.bind = bind;
this.motd = motd; this.motd = motd;
this.showMaxPlayers = showMaxPlayers; this.showMaxPlayers = showMaxPlayers;
this.onlineMode = onlineMode; this.onlineMode = onlineMode;
this.announceForge = announceForge;
this.playerInfoForwardingMode = playerInfoForwardingMode; this.playerInfoForwardingMode = playerInfoForwardingMode;
this.forwardingSecret = forwardingSecret; this.forwardingSecret = forwardingSecret;
this.servers = servers; this.servers = servers;
@ -103,14 +109,14 @@ public class VelocityConfiguration extends AnnotatedConfig {
if (bind.isEmpty()) { if (bind.isEmpty()) {
logger.error("'bind' option is empty."); logger.error("'bind' option is empty.");
valid = false; valid = false;
} } else {
try { try {
AddressUtil.parseAddress(bind); AddressUtil.parseAddress(bind);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
logger.error("'bind' option does not specify a valid IP address.", e); logger.error("'bind' option does not specify a valid IP address.", e);
valid = false; valid = false;
} }
}
if (!onlineMode) { if (!onlineMode) {
logger.info("Proxy is running in offline mode!"); logger.info("Proxy is running in offline mode!");
@ -118,11 +124,11 @@ public class VelocityConfiguration extends AnnotatedConfig {
switch (playerInfoForwardingMode) { switch (playerInfoForwardingMode) {
case NONE: 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; break;
case MODERN: case MODERN:
if (forwardingSecret.length == 0) { if (forwardingSecret == null || forwardingSecret.length == 0) {
logger.error("You don't have a forwarding secret set."); logger.error("You don't have a forwarding secret set. This is required for security.");
valid = false; valid = false;
} }
break; break;
@ -148,7 +154,7 @@ public class VelocityConfiguration extends AnnotatedConfig {
for (String s : servers.getAttemptConnectionOrder()) { for (String s : servers.getAttemptConnectionOrder()) {
if (!servers.getServers().containsKey(s)) { 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; valid = false;
} }
} }
@ -165,18 +171,18 @@ public class VelocityConfiguration extends AnnotatedConfig {
logger.error("Invalid compression level {}", advanced.compressionLevel); logger.error("Invalid compression level {}", advanced.compressionLevel);
valid = false; valid = false;
} else if (advanced.compressionLevel == 0) { } 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) { if (advanced.compressionThreshold < -1) {
logger.error("Invalid compression threshold {}", advanced.compressionLevel); logger.error("Invalid compression threshold {}", advanced.compressionLevel);
valid = false; valid = false;
} else if (advanced.compressionThreshold == 0) { } 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) { if (advanced.loginRatelimit < 0) {
logger.error("Invalid login ratelimit {}", advanced.loginRatelimit); logger.error("Invalid login ratelimit {}ms", advanced.loginRatelimit);
valid = false; valid = false;
} }
@ -217,7 +223,7 @@ public class VelocityConfiguration extends AnnotatedConfig {
if (motd.startsWith("{")) { if (motd.startsWith("{")) {
motdAsComponent = ComponentSerializers.JSON.deserialize(motd); motdAsComponent = ComponentSerializers.JSON.deserialize(motd);
} else { } else {
motdAsComponent = ComponentSerializers.LEGACY.deserialize(LegacyChatColorUtils.translate('&', motd)); motdAsComponent = ComponentSerializers.LEGACY.deserialize(motd, '&');
} }
} }
return motdAsComponent; return motdAsComponent;
@ -263,54 +269,34 @@ public class VelocityConfiguration extends AnnotatedConfig {
return favicon; return favicon;
} }
private void setBind(String bind) { public boolean isAnnounceForge() {
this.bind = bind; return announceForge;
} }
private void setMotd(String motd) { public int getConnectTimeout() {
this.motd = motd; return advanced.getConnectionTimeout();
} }
private void setShowMaxPlayers(int showMaxPlayers) { public int getReadTimeout() {
this.showMaxPlayers = showMaxPlayers; return advanced.getReadTimeout();
}
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;
} }
@Override @Override
public String toString() { public String toString() {
return MoreObjects.toStringHelper(this)
return "VelocityConfiguration{" .add("configVersion", configVersion)
+ "bind='" + bind + '\'' .add("bind", bind)
+ ", motd='" + motd + '\'' .add("motd", motd)
+ ", showMaxPlayers=" + showMaxPlayers .add("showMaxPlayers", showMaxPlayers)
+ ", onlineMode=" + onlineMode .add("onlineMode", onlineMode)
+ ", playerInfoForwardingMode=" + playerInfoForwardingMode .add("playerInfoForwardingMode", playerInfoForwardingMode)
+ ", forwardingSecret=" + ByteBufUtil.hexDump(forwardingSecret) .add("forwardingSecret", forwardingSecret)
+ ", servers=" + servers .add("announceForge", announceForge)
+ ", advanced=" + advanced .add("servers", servers)
+ ", query=" + query .add("advanced", advanced)
+ ", motdAsComponent=" + motdAsComponent .add("query", query)
+ ", favicon=" + favicon .add("favicon", favicon)
+ '}'; .toString();
} }
public static VelocityConfiguration read(Path path) throws IOException { public static VelocityConfiguration read(Path path) throws IOException {
@ -335,6 +321,7 @@ public class VelocityConfiguration extends AnnotatedConfig {
toml.getString("motd", "&3A Velocity Server"), toml.getString("motd", "&3A Velocity Server"),
toml.getLong("show-max-players", 500L).intValue(), toml.getLong("show-max-players", 500L).intValue(),
toml.getBoolean("online-mode", true), toml.getBoolean("online-mode", true),
toml.getBoolean("announce-forge", false),
PlayerInfoForwarding.valueOf(toml.getString("player-info-forwarding-mode", "MODERN").toUpperCase()), PlayerInfoForwarding.valueOf(toml.getString("player-info-forwarding-mode", "MODERN").toUpperCase()),
forwardingSecret, forwardingSecret,
servers, servers,
@ -441,21 +428,23 @@ public class VelocityConfiguration extends AnnotatedConfig {
"Disable by setting to 0"}) "Disable by setting to 0"})
@ConfigKey("login-ratelimit") @ConfigKey("login-ratelimit")
private int loginRatelimit = 3000; 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() {
} }
private Advanced(int compressionThreshold, int compressionLevel, int loginRatelimit) {
this.compressionThreshold = compressionThreshold;
this.compressionLevel = compressionLevel;
this.loginRatelimit = loginRatelimit;
}
private Advanced(Toml toml) { private Advanced(Toml toml) {
if (toml != null) { if (toml != null) {
this.compressionThreshold = toml.getLong("compression-threshold", 1024L).intValue(); this.compressionThreshold = toml.getLong("compression-threshold", 1024L).intValue();
this.compressionLevel = toml.getLong("compression-level", -1L).intValue(); this.compressionLevel = toml.getLong("compression-level", -1L).intValue();
this.loginRatelimit = toml.getLong("login-ratelimit", 3000L).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; return compressionThreshold;
} }
public void setCompressionThreshold(int compressionThreshold) {
this.compressionThreshold = compressionThreshold;
}
public int getCompressionLevel() { public int getCompressionLevel() {
return compressionLevel; return compressionLevel;
} }
public void setCompressionLevel(int compressionLevel) {
this.compressionLevel = compressionLevel;
}
public int getLoginRatelimit() { public int getLoginRatelimit() {
return loginRatelimit; return loginRatelimit;
} }
public void setLoginRatelimit(int loginRatelimit) { public int getConnectionTimeout() {
this.loginRatelimit = loginRatelimit; return connectionTimeout;
}
public int getReadTimeout() {
return readTimeout;
} }
@Override @Override
public String toString() { public String toString() {
return "Advanced{" return "Advanced{" +
+ "compressionThreshold=" + compressionThreshold "compressionThreshold=" + compressionThreshold +
+ ", compressionLevel=" + compressionLevel ", compressionLevel=" + compressionLevel +
+ ", loginRatelimit=" + loginRatelimit ", loginRatelimit=" + loginRatelimit +
+ '}'; ", connectionTimeout=" + connectionTimeout +
", readTimeout=" + readTimeout +
'}';
} }
} }
@ -521,18 +508,10 @@ public class VelocityConfiguration extends AnnotatedConfig {
return queryEnabled; return queryEnabled;
} }
public void setQueryEnabled(boolean queryEnabled) {
this.queryEnabled = queryEnabled;
}
public int getQueryPort() { public int getQueryPort() {
return queryPort; return queryPort;
} }
public void setQueryPort(int queryPort) {
this.queryPort = queryPort;
}
@Override @Override
public String toString() { public String toString() {
return "Query{" return "Query{"

Datei anzeigen

@ -45,7 +45,9 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
private MinecraftSessionHandler sessionHandler; private MinecraftSessionHandler sessionHandler;
private int protocolVersion; private int protocolVersion;
private MinecraftConnectionAssociation association; private MinecraftConnectionAssociation association;
private boolean isLegacyForge;
private final VelocityServer server; private final VelocityServer server;
private boolean canSendLegacyFMLResetPacket = false;
public MinecraftConnection(Channel channel, VelocityServer server) { public MinecraftConnection(Channel channel, VelocityServer server) {
this.channel = channel; 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) { public void write(Object msg) {
if (channel.isActive()) { if (channel.isActive()) {
channel.writeAndFlush(msg, channel.voidPromise()); channel.writeAndFlush(msg, channel.voidPromise());
@ -222,4 +231,20 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
public void setAssociation(MinecraftConnectionAssociation association) { public void setAssociation(MinecraftConnectionAssociation association) {
this.association = 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 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 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.connection.PluginMessageEvent;
import com.velocitypowered.api.event.player.ServerConnectedEvent; import com.velocitypowered.api.event.player.ServerConnectedEvent;
import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.VelocityConstants;
import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler; import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler;
import com.velocitypowered.proxy.connection.util.ConnectionMessages;
import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants; import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.packet.*; import com.velocitypowered.proxy.protocol.packet.*;
@ -30,7 +32,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
if (!connection.getPlayer().isActive()) { if (!connection.getPlayer().isActive()) {
// Connection was left open accidentally. Close it so as to avoid "You logged in from another location" // Connection was left open accidentally. Close it so as to avoid "You logged in from another location"
// errors. // errors.
connection.getMinecraftConnection().close(); connection.disconnect();
return; return;
} }
@ -42,6 +44,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
connection.getPlayer().getConnection().write(packet); connection.getPlayer().getConnection().write(packet);
} else if (packet instanceof Disconnect) { } else if (packet instanceof Disconnect) {
Disconnect original = (Disconnect) packet; Disconnect original = (Disconnect) packet;
connection.disconnect();
connection.getPlayer().handleConnectionException(connection.getServerInfo(), original); connection.getPlayer().handleConnectionException(connection.getServerInfo(), original);
} else if (packet instanceof JoinGame) { } else if (packet instanceof JoinGame) {
playerHandler.handleBackendJoinGame((JoinGame) packet); playerHandler.handleBackendJoinGame((JoinGame) packet);
@ -67,6 +70,20 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
return; 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()), PluginMessageEvent event = new PluginMessageEvent(connection, connection.getPlayer(), server.getChannelRegistrar().getFromId(pm.getChannel()),
pm.getData()); pm.getData());
server.getEventManager().fire(event) server.getEventManager().fire(event)
@ -86,27 +103,43 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
if (!connection.getPlayer().isActive()) { if (!connection.getPlayer().isActive()) {
// Connection was left open accidentally. Close it so as to avoid "You logged in from another location" // Connection was left open accidentally. Close it so as to avoid "You logged in from another location"
// errors. // errors.
connection.getMinecraftConnection().close(); connection.disconnect();
return; return;
} }
if (connection.hasCompletedJoin()) {
connection.getPlayer().getConnection().write(buf.retain()); connection.getPlayer().getConnection().write(buf.retain());
} }
}
@Override @Override
public void exception(Throwable throwable) { public void exception(Throwable throwable) {
connection.getPlayer().handleConnectionException(connection.getServerInfo(), throwable); 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) { private boolean canForwardPluginMessage(PluginMessage message) {
ClientPlaySessionHandler playerHandler = ClientPlaySessionHandler playerHandler =
(ClientPlaySessionHandler) connection.getPlayer().getConnection().getSessionHandler(); (ClientPlaySessionHandler) connection.getPlayer().getConnection().getSessionHandler();
boolean isMCMessage; boolean isMCOrFMLMessage;
if (connection.getMinecraftConnection().getProtocolVersion() <= ProtocolConstants.MINECRAFT_1_12_2) { 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 { } 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()); 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())); connection.getPlayer().getConnection().setSessionHandler(new ClientPlaySessionHandler(server, connection.getPlayer()));
} else { } else {
// The previous server connection should become obsolete. // 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(); 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 dataToForward = Unpooled.buffer();
ByteBuf finalData = Unpooled.buffer(); ByteBuf finalData = Unpooled.buffer();
try { 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_DECODER;
import static com.velocitypowered.proxy.network.Connections.MINECRAFT_ENCODER; 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.READ_TIMEOUT;
import static com.velocitypowered.proxy.network.Connections.SERVER_READ_TIMEOUT_SECONDS;
public class VelocityServerConnection implements MinecraftConnectionAssociation, ServerConnection { public class VelocityServerConnection implements MinecraftConnectionAssociation, ServerConnection {
static final AttributeKey<CompletableFuture<ConnectionRequestBuilder.Result>> CONNECTION_NOTIFIER = static final AttributeKey<CompletableFuture<ConnectionRequestBuilder.Result>> CONNECTION_NOTIFIER =
@ -43,6 +42,9 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
private final ConnectedPlayer proxyPlayer; private final ConnectedPlayer proxyPlayer;
private final VelocityServer server; private final VelocityServer server;
private MinecraftConnection minecraftConnection; private MinecraftConnection minecraftConnection;
private boolean legacyForge = false;
private boolean hasCompletedJoin = false;
private boolean gracefulDisconnect = false;
public VelocityServerConnection(ServerInfo target, ConnectedPlayer proxyPlayer, VelocityServer server) { public VelocityServerConnection(ServerInfo target, ConnectedPlayer proxyPlayer, VelocityServer server) {
this.serverInfo = target; this.serverInfo = target;
@ -57,7 +59,7 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
@Override @Override
protected void initChannel(Channel ch) throws Exception { protected void initChannel(Channel ch) throws Exception {
ch.pipeline() 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_DECODER, new MinecraftVarintFrameDecoder())
.addLast(FRAME_ENCODER, MinecraftVarintLengthEncoder.INSTANCE) .addLast(FRAME_ENCODER, MinecraftVarintLengthEncoder.INSTANCE)
.addLast(MINECRAFT_DECODER, new MinecraftDecoder(ProtocolConstants.Direction.CLIENTBOUND)) .addLast(MINECRAFT_DECODER, new MinecraftDecoder(ProtocolConstants.Direction.CLIENTBOUND))
@ -107,6 +109,8 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
handshake.setProtocolVersion(proxyPlayer.getConnection().getProtocolVersion()); handshake.setProtocolVersion(proxyPlayer.getConnection().getProtocolVersion());
if (forwardingMode == PlayerInfoForwarding.LEGACY) { if (forwardingMode == PlayerInfoForwarding.LEGACY) {
handshake.setServerAddress(createBungeeForwardingAddress()); handshake.setServerAddress(createBungeeForwardingAddress());
} else if (proxyPlayer.getConnection().isLegacyForge()) {
handshake.setServerAddress(handshake.getServerAddress() + "\0FML\0");
} else { } else {
handshake.setServerAddress(serverInfo.getAddress().getHostString()); handshake.setServerAddress(serverInfo.getAddress().getHostString());
} }
@ -122,6 +126,12 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
minecraftConnection.write(login); minecraftConnection.write(login);
} }
public void writeIfJoined(PluginMessage message) {
if (hasCompletedJoin) {
minecraftConnection.write(message);
}
}
public MinecraftConnection getMinecraftConnection() { public MinecraftConnection getMinecraftConnection() {
return minecraftConnection; return minecraftConnection;
} }
@ -136,8 +146,11 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
} }
public void disconnect() { public void disconnect() {
if (minecraftConnection != null) {
minecraftConnection.close(); minecraftConnection.close();
minecraftConnection = null; minecraftConnection = null;
gracefulDisconnect = true;
}
} }
@Override @Override
@ -154,4 +167,24 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
message.setData(data); message.setData(data);
minecraftConnection.write(message); 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.DisconnectEvent;
import com.velocitypowered.api.event.connection.PluginMessageEvent; import com.velocitypowered.api.event.connection.PluginMessageEvent;
import com.velocitypowered.proxy.VelocityServer; 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.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants; import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.packet.*; import com.velocitypowered.proxy.protocol.packet.*;
@ -23,7 +25,7 @@ import java.util.*;
*/ */
public class ClientPlaySessionHandler implements MinecraftSessionHandler { public class ClientPlaySessionHandler implements MinecraftSessionHandler {
private static final Logger logger = LogManager.getLogger(ClientPlaySessionHandler.class); 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 final ConnectedPlayer player;
private long lastPingID = -1; private long lastPingID = -1;
@ -31,6 +33,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
private boolean spawned = false; private boolean spawned = false;
private final List<UUID> serverBossBars = new ArrayList<>(); private final List<UUID> serverBossBars = new ArrayList<>();
private final Set<String> clientPluginMsgChannels = new HashSet<>(); private final Set<String> clientPluginMsgChannels = new HashSet<>();
private final Queue<PluginMessage> loginPluginMessages = new ArrayDeque<>();
private final VelocityServer server; private final VelocityServer server;
public ClientPlaySessionHandler(VelocityServer server, ConnectedPlayer player) { public ClientPlaySessionHandler(VelocityServer server, ConnectedPlayer player) {
@ -51,6 +54,12 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
@Override @Override
public void handle(MinecraftPacket packet) { public void handle(MinecraftPacket packet) {
VelocityServerConnection serverConnection = player.getConnectedServer();
if (serverConnection == null) {
// No server connection yet, probably transitioning.
return;
}
if (packet instanceof KeepAlive) { if (packet instanceof KeepAlive) {
KeepAlive keepAlive = (KeepAlive) packet; KeepAlive keepAlive = (KeepAlive) packet;
if (keepAlive.getRandomId() != lastPingID) { if (keepAlive.getRandomId() != lastPingID) {
@ -60,6 +69,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
} }
player.setPing(System.currentTimeMillis() - lastPingSent); player.setPing(System.currentTimeMillis() - lastPingSent);
resetPingData(); resetPingData();
serverConnection.getMinecraftConnection().write(packet);
return;
} }
if (packet instanceof ClientSettings) { if (packet instanceof ClientSettings) {
@ -106,7 +117,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
player.getConnection().write(response); player.getConnection().write(response);
} else { } else {
player.getConnectedServer().getMinecraftConnection().write(packet); serverConnection.getMinecraftConnection().write(packet);
} }
} catch (Exception e) { } catch (Exception e) {
logger.error("Unable to provide tab list completions for " + player.getUsername() + " for command '" + req.getCommand() + "'", 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. // 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 @Override
public void handleUnknown(ByteBuf buf) { 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 @Override
@ -144,12 +165,31 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
.build()); .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) { public void handleBackendJoinGame(JoinGame joinGame) {
resetPingData(); // reset ping data; resetPingData(); // reset ping data
if (!spawned) { if (!spawned) {
// nothing special to do here // Nothing special to do with regards to spawning the player
spawned = true; spawned = true;
player.getConnection().delayedWrite(joinGame); 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 { } else {
// Ah, this is the meat and potatoes of the whole venture! // Ah, this is the meat and potatoes of the whole venture!
// //
@ -193,17 +233,38 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
channel, toRegister)); 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 // Flush everything
player.getConnection().flush(); player.getConnection().flush();
player.getConnectedServer().getMinecraftConnection().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() { public List<UUID> getServerBossBars() {
return serverBossBars; return serverBossBars;
} }
public void handleClientPluginMessage(PluginMessage packet) { private void handleClientPluginMessage(PluginMessage packet) {
if (packet.getChannel().equals("REGISTER") || packet.getChannel().equals("minecraft:register")) { if (PluginMessageUtil.isMCRegister(packet)) {
List<String> actuallyRegistered = new ArrayList<>(); List<String> actuallyRegistered = new ArrayList<>();
List<String> channels = PluginMessageUtil.getChannels(packet); List<String> channels = PluginMessageUtil.getChannels(packet);
for (String channel : channels) { for (String channel : channels) {
@ -220,20 +281,23 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
PluginMessage newRegisterPacket = PluginMessageUtil.constructChannelsPacket(packet.getChannel(), actuallyRegistered); PluginMessage newRegisterPacket = PluginMessageUtil.constructChannelsPacket(packet.getChannel(), actuallyRegistered);
player.getConnectedServer().getMinecraftConnection().write(newRegisterPacket); player.getConnectedServer().getMinecraftConnection().write(newRegisterPacket);
} }
} else if (PluginMessageUtil.isMCUnregister(packet)) {
return;
}
if (packet.getChannel().equals("UNREGISTER") || packet.getChannel().equals("minecraft:unregister")) {
List<String> channels = PluginMessageUtil.getChannels(packet); List<String> channels = PluginMessageUtil.getChannels(packet);
clientPluginMsgChannels.removeAll(channels); clientPluginMsgChannels.removeAll(channels);
} player.getConnectedServer().getMinecraftConnection().write(packet);
} else if (PluginMessageUtil.isMCBrand(packet)) {
if (PluginMessageUtil.isMCBrand(packet)) {
player.getConnectedServer().getMinecraftConnection().write(PluginMessageUtil.rewriteMCBrand(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(), PluginMessageEvent event = new PluginMessageEvent(player, player.getConnectedServer(),
server.getChannelRegistrar().getFromId(packet.getChannel()), packet.getData()); server.getChannelRegistrar().getFromId(packet.getChannel()), packet.getData());
server.getEventManager().fire(event) server.getEventManager().fire(event)
@ -243,6 +307,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
} }
}, player.getConnectedServer().getMinecraftConnection().getChannel().eventLoop()); }, player.getConnectedServer().getMinecraftConnection().getChannel().eventLoop());
} }
}
public Set<String> getClientPluginMsgChannels() { public Set<String> getClientPluginMsgChannels() {
return clientPluginMsgChannels; return clientPluginMsgChannels;

Datei anzeigen

@ -12,7 +12,7 @@ public class ClientSettingsWrapper implements PlayerSettings {
private final SkinParts parts; private final SkinParts parts;
private Locale locale = null; private Locale locale = null;
public ClientSettingsWrapper(ClientSettings settings) { ClientSettingsWrapper(ClientSettings settings) {
this.settings = settings; this.settings = settings;
this.parts = new SkinParts((byte) settings.getSkinParts()); 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.common.base.Preconditions;
import com.google.gson.JsonObject; 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.PlayerSettingsChangedEvent;
import com.velocitypowered.api.event.player.ServerPreConnectEvent; import com.velocitypowered.api.event.player.ServerPreConnectEvent;
import com.velocitypowered.api.permission.PermissionFunction; 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.api.proxy.Player;
import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation; 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.ConnectionMessages;
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults;
import com.velocitypowered.api.util.GameProfile; 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.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.util.List; 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); logger.error("{}: unable to connect to server {}", this, info.getName(), throwable);
userMessage = "Exception connecting to server " + info.getName(); userMessage = "Exception connecting to server " + info.getName();
} }
handleConnectionException(info, TextComponent.builder() handleConnectionException(info, null, TextComponent.builder()
.content(userMessage + ": ") .content(userMessage + ": ")
.color(TextColor.RED) .color(TextColor.RED)
.append(TextComponent.of(error, TextColor.WHITE)) .append(TextComponent.of(error, TextColor.WHITE))
@ -202,17 +205,23 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
String plainTextReason = PASS_THRU_TRANSLATE.serialize(disconnectReason); String plainTextReason = PASS_THRU_TRANSLATE.serialize(disconnectReason);
if (connectedServer != null && connectedServer.getServerInfo().equals(info)) { if (connectedServer != null && connectedServer.getServerInfo().equals(info)) {
logger.error("{}: kicked from server {}: {}", this, info.getName(), plainTextReason); 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 { } else {
logger.error("{}: disconnected while connecting to {}: {}", this, info.getName(), plainTextReason); logger.error("{}: disconnected while connecting to {}: {}", this, info.getName(), plainTextReason);
} handleConnectionException(info, disconnectReason, TextComponent.builder()
handleConnectionException(info, TextComponent.builder()
.content("Unable to connect to " + info.getName() + ": ") .content("Unable to connect to " + info.getName() + ": ")
.color(TextColor.RED) .color(TextColor.RED)
.append(disconnectReason) .append(disconnectReason)
.build()); .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; connectionInFlight = null;
if (connectedServer == null) { if (connectedServer == null) {
// The player isn't yet connected to a server. // The player isn't yet connected to a server.
@ -220,14 +229,29 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
if (nextServer.isPresent()) { if (nextServer.isPresent()) {
createConnectionRequest(nextServer.get()).fireAndForget(); createConnectionRequest(nextServer.get()).fireAndForget();
} else { } else {
connection.closeWith(Disconnect.create(disconnectReason)); connection.closeWith(Disconnect.create(friendlyReason));
} }
} else if (connectedServer.getServerInfo().equals(info)) { } else if (connectedServer.getServerInfo().equals(info)) {
// Already connected to the server being disconnected from. // Already connected to the server being disconnected from.
// TODO: ServerKickEvent if (kickReason != null) {
connection.closeWith(Disconnect.create(disconnectReason)); 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 { } else {
connection.write(Chat.create(disconnectReason)); // 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(friendlyReason));
} }
} }
@ -256,7 +280,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
} }
// Otherwise, initiate the connection. // 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) return server.getEventManager().fire(event)
.thenCompose((newEvent) -> { .thenCompose((newEvent) -> {
if (!newEvent.getResult().isAllowed()) { 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; 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) { public void close(TextComponent reason) {
connection.closeWith(Disconnect.create(reason)); connection.closeWith(Disconnect.create(reason));
} }

Datei anzeigen

@ -70,9 +70,12 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
return; return;
} }
// Make sure legacy forwarding is not in use on this connection. Make sure that we do _not_ reject Forge, // 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
// although Velocity does not yet support Forge. boolean isForge = handshake.getServerAddress().endsWith("\0FML\0");
if (handshake.getServerAddress().contains("\0") && !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."))); connection.closeWith(Disconnect.create(TextComponent.of("Running Velocity behind Velocity is unsupported.")));
return; 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.Version(ProtocolConstants.MAXIMUM_GENERIC_VERSION, "Velocity " + ProtocolConstants.SUPPORTED_GENERIC_VERSION_STRING),
new ServerPing.Players(server.getPlayerCount(), configuration.getShowMaxPlayers(), ImmutableList.of()), new ServerPing.Players(server.getPlayerCount(), configuration.getShowMaxPlayers(), ImmutableList.of()),
configuration.getMotdComponent(), configuration.getMotdComponent(),
null,
null null
); );
ProxyPingEvent event = new ProxyPingEvent(new LegacyInboundConnection(connection), ping); 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.google.common.base.Preconditions;
import com.velocitypowered.api.event.connection.LoginEvent; 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;
import com.velocitypowered.api.event.connection.PreLoginEvent.PreLoginComponentResult; import com.velocitypowered.api.event.connection.PreLoginEvent.PreLoginComponentResult;
import com.velocitypowered.api.event.permission.PermissionsSetupEvent; import com.velocitypowered.api.event.permission.PermissionsSetupEvent;
import com.velocitypowered.api.event.player.GameProfileRequestEvent; import com.velocitypowered.api.event.player.GameProfileRequestEvent;
import com.velocitypowered.api.proxy.InboundConnection; import com.velocitypowered.api.proxy.InboundConnection;
import com.velocitypowered.api.proxy.server.ServerInfo; import com.velocitypowered.api.proxy.server.ServerInfo;
import com.velocitypowered.proxy.config.PlayerInfoForwarding;
import com.velocitypowered.proxy.connection.VelocityConstants; import com.velocitypowered.proxy.connection.VelocityConstants;
import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.api.util.GameProfile;
import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.MinecraftPacket;
@ -31,11 +33,14 @@ import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.security.KeyPair; import java.security.KeyPair;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.ThreadLocalRandom;
public class LoginSessionHandler implements MinecraftSessionHandler { public class LoginSessionHandler implements MinecraftSessionHandler {
private static final Logger logger = LogManager.getLogger(LoginSessionHandler.class); private static final Logger logger = LogManager.getLogger(LoginSessionHandler.class);
private static final String MOJANG_SERVER_AUTH_URL = private static final String MOJANG_SERVER_AUTH_URL =
"https://sessionserver.mojang.com/session/minecraft/hasJoined?username=%s&serverId=%s&ip=%s"; "https://sessionserver.mojang.com/session/minecraft/hasJoined?username=%s&serverId=%s&ip=%s";
@ -154,7 +159,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
return; return;
} }
if (server.getConfiguration().isOnlineMode() || result.isOnlineModeAllowed()) { if (!result.isForceOfflineMode() && (server.getConfiguration().isOnlineMode() || result.isOnlineModeAllowed())) {
// Request encryption. // Request encryption.
EncryptionRequest request = generateRequest(); EncryptionRequest request = generateRequest();
this.verify = Arrays.copyOf(request.getVerifyToken(), 4); this.verify = Arrays.copyOf(request.getVerifyToken(), 4);
@ -176,6 +181,12 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
} }
private void initializePlayer(GameProfile profile, boolean onlineMode) { 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); GameProfileRequestEvent profileRequestEvent = new GameProfileRequestEvent(apiInbound, profile, onlineMode);
server.getEventManager().fire(profileRequestEvent).thenCompose(profileEvent -> { server.getEventManager().fire(profileRequestEvent).thenCompose(profileEvent -> {
@ -235,7 +246,9 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
logger.info("{} has connected", player); logger.info("{} has connected", player);
inbound.setSessionHandler(new InitialConnectSessionHandler(player)); inbound.setSessionHandler(new InitialConnectSessionHandler(player));
server.getEventManager().fire(new PostLoginEvent(player)).thenRun(() -> {
player.createConnectionRequest(toTry.get()).fireAndForget(); player.createConnectionRequest(toTry.get()).fireAndForget();
});
} }
@Override @Override

Datei anzeigen

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

Datei anzeigen

@ -16,11 +16,7 @@ import com.velocitypowered.proxy.protocol.netty.MinecraftVarintFrameDecoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftVarintLengthEncoder; import com.velocitypowered.proxy.protocol.netty.MinecraftVarintLengthEncoder;
import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap; import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel; import io.netty.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.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.EpollEventLoopGroup;
@ -48,6 +44,8 @@ import java.util.concurrent.TimeUnit;
import static com.velocitypowered.proxy.network.Connections.*; import static com.velocitypowered.proxy.network.Connections.*;
public final class ConnectionManager { 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 static final Logger logger = LogManager.getLogger(ConnectionManager.class);
private final Set<Channel> endpoints = new HashSet<>(); private final Set<Channel> endpoints = new HashSet<>();
@ -72,11 +70,12 @@ public final class ConnectionManager {
final ServerBootstrap bootstrap = new ServerBootstrap() final ServerBootstrap bootstrap = new ServerBootstrap()
.channel(this.transportType.serverSocketChannelClass) .channel(this.transportType.serverSocketChannelClass)
.group(this.bossGroup, this.workerGroup) .group(this.bossGroup, this.workerGroup)
.childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, SERVER_WRITE_MARK)
.childHandler(new ChannelInitializer<Channel>() { .childHandler(new ChannelInitializer<Channel>() {
@Override @Override
protected void initChannel(final Channel ch) { protected void initChannel(final Channel ch) {
ch.pipeline() 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(LEGACY_PING_DECODER, new LegacyPingDecoder())
.addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder()) .addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder())
.addLast(LEGACY_PING_ENCODER, LegacyPingEncoder.INSTANCE) .addLast(LEGACY_PING_ENCODER, LegacyPingEncoder.INSTANCE)
@ -126,7 +125,9 @@ public final class ConnectionManager {
public Bootstrap createWorker() { public Bootstrap createWorker() {
return new Bootstrap() return new Bootstrap()
.channel(this.transportType.socketChannelClass) .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() { public void shutdown() {

Datei anzeigen

@ -13,7 +13,4 @@ public interface Connections {
String MINECRAFT_DECODER = "minecraft-decoder"; String MINECRAFT_DECODER = "minecraft-decoder";
String MINECRAFT_ENCODER = "minecraft-encoder"; String MINECRAFT_ENCODER = "minecraft-encoder";
String READ_TIMEOUT = "read-timeout"; 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; package com.velocitypowered.proxy.protocol;
import com.google.common.base.Strings;
import com.google.common.primitives.ImmutableIntArray; import com.google.common.primitives.ImmutableIntArray;
import com.velocitypowered.proxy.protocol.packet.*; import com.velocitypowered.proxy.protocol.packet.*;
import io.netty.util.collection.IntObjectHashMap; import io.netty.util.collection.IntObjectHashMap;
@ -258,9 +259,18 @@ public enum StateRegistry {
@Override @Override
public String toString() { 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{" + return "ProtocolVersion{" +
"id=" + id + "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); packet.decode(msg, direction, protocolVersion.id);
} catch (Exception e) { } catch (Exception e) {
throw new CorruptedFrameException("Error decoding " + packet.getClass() + " Direction " + direction 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()) { if (msg.isReadable()) {
throw new CorruptedFrameException("Did not read full packet for " + packet.getClass() + " Direction " + direction 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); out.add(packet);
} }

Datei anzeigen

@ -2,8 +2,10 @@ package com.velocitypowered.proxy.protocol.netty;
import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder; import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.CorruptedFrameException;
import java.util.List; import java.util.List;
@ -15,12 +17,31 @@ public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder {
} }
in.markReaderIndex(); in.markReaderIndex();
int packetLength = ProtocolUtils.readVarInt(in);
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) { if (in.readableBytes() < packetLength) {
in.resetReaderIndex(); in.resetReaderIndex();
return; return;
} }
out.add(in.readRetainedSlice(packetLength)); out.add(in.readRetainedSlice(packetLength));
return;
}
}
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"); 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) { public static List<String> getChannels(PluginMessage message) {
Preconditions.checkNotNull(message, "message"); Preconditions.checkNotNull(message, "message");
Preconditions.checkArgument(message.getChannel().equals("REGISTER") || Preconditions.checkArgument(isMCRegister(message) || isMCUnregister(message),"Unknown channel type %s",
message.getChannel().equals("UNREGISTER") || message.getChannel());
message.getChannel().equals("minecraft:register") ||
message.getChannel().equals("minecraft:unregister"),
"Unknown channel type " + message.getChannel());
String channels = new String(message.getData(), StandardCharsets.UTF_8); String channels = new String(message.getData(), StandardCharsets.UTF_8);
return ImmutableList.copyOf(channels.split("\0")); 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.CountDownLatch;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
@ -33,4 +34,36 @@ class RecordingThreadFactoryTest {
Thread.sleep(10); Thread.sleep(10);
assertEquals(0, factory.size()); 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());
}
} }