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:
Commit
5ff36f1ae7
@ -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.
|
|
@ -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
|
||||||
|
+ '}';
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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() {
|
||||||
|
@ -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;
|
||||||
builder.onlinePlayers = players.online;
|
if (players != null) {
|
||||||
builder.maximumPlayers = players.max;
|
builder.onlinePlayers = players.online;
|
||||||
builder.samplePlayers.addAll(players.sample);
|
builder.maximumPlayers = players.max;
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
15
build.gradle
15
build.gradle
@ -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 {
|
||||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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,13 +109,13 @@ 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) {
|
||||||
@ -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{"
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,4 +29,8 @@ public interface MinecraftSessionHandler {
|
|||||||
default void exception(Throwable throwable) {
|
default void exception(Throwable throwable) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default void writabilityChanged() {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 };
|
||||||
}
|
}
|
||||||
|
@ -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,10 +103,13 @@ 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;
|
||||||
}
|
}
|
||||||
connection.getPlayer().getConnection().write(buf.retain());
|
|
||||||
|
if (connection.hasCompletedJoin()) {
|
||||||
|
connection.getPlayer().getConnection().write(buf.retain());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -97,16 +117,29 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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() {
|
||||||
minecraftConnection.close();
|
if (minecraftConnection != null) {
|
||||||
minecraftConnection = null;
|
minecraftConnection.close();
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,28 +281,32 @@ 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(),
|
||||||
|
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() {
|
public Set<String> getClientPluginMsgChannels() {
|
||||||
|
@ -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());
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
.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;
|
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 {
|
||||||
|
// 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 {
|
} 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.
|
// 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));
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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));
|
||||||
player.createConnectionRequest(toTry.get()).fireAndForget();
|
server.getEventManager().fire(new PostLoginEvent(player)).thenRun(() -> {
|
||||||
|
player.createConnectionRequest(toTry.get()).fireAndForget();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -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);
|
||||||
|
@ -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();
|
||||||
|
@ -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() {
|
||||||
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
@ -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() +
|
||||||
'}';
|
'}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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);
|
|
||||||
if (in.readableBytes() < packetLength) {
|
byte[] lenBuf = new byte[3];
|
||||||
in.resetReaderIndex();
|
for (int i = 0; i < lenBuf.length; i++) {
|
||||||
return;
|
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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"));
|
||||||
}
|
}
|
||||||
|
@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
Laden…
In neuem Issue referenzieren
Einen Benutzer sperren