Introduce a fluent connection request API.

This commit is contained in:
Andrew Steinborn 2018-08-04 03:13:17 -04:00
parent fbdaae5ac7
commit 0ba85fe83f
9 changed files with 278 additions and 9 deletions

View File

@ -0,0 +1,84 @@
package com.velocitypowered.api.proxy;
import com.velocitypowered.api.server.ServerInfo;
import net.kyori.text.Component;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
/**
* Represents a connection request. A connection request is created using {@link Player#createConnectionRequest(ServerInfo)}
* and is used to allow a plugin to compose and request a connection to another Minecraft server using a fluent API.
*/
public interface ConnectionRequestBuilder {
/**
* Returns the server that this connection request represents.
* @return the server this request will connect to
*/
ServerInfo getServer();
/**
* Initiates the connection to the remote server and emits a result on the {@link CompletableFuture} after the user
* has logged on. No messages will be communicated to the client: the user is responsible for all error handling.
* @return a {@link CompletableFuture} representing the status of this connection
*/
CompletableFuture<Result> connect();
/**
* Initiates the connection to the remote server without waiting for a result. Velocity will use generic error
* handling code to notify the user.
*/
void fireAndForget();
/**
* Represents the result of a connection request.
*/
interface Result {
/**
* Determines whether or not the connection request was successful.
* @return whether or not the request succeeded
*/
default boolean isSuccessful() {
return getStatus() == Status.SUCCESS;
}
/**
* Returns the status associated with this result.
* @return the status for this result
*/
Status getStatus();
/**
* Returns a reason for the failure to connect to the server. None may be provided.
* @return the reason why the user could not connect to the server
*/
Optional<Component> getReason();
}
/**
* Represents the status of a connection request initiated by a {@link ConnectionRequestBuilder}.
*/
enum Status {
/**
* The player was successfully connected to the server.
*/
SUCCESS,
/**
* The player is already connected to this server.
*/
ALREADY_CONNECTED,
/**
* The connection is already in progress.
*/
CONNECTION_IN_PROGRESS,
/**
* A plugin has cancelled this connection.
*/
CONNECTION_CANCELLED,
/**
* The server disconnected the user. A reason may be provided in the {@link Result} object.
*/
SERVER_DISCONNECTED
}
}

View File

@ -57,4 +57,11 @@ public interface Player {
* @param position the position for the message
*/
void sendMessage(@Nonnull Component component, @Nonnull MessagePosition position);
/**
* Creates a new connection request so that the player can connect to another server.
* @param info the server to connect to
* @return a new connection request
*/
ConnectionRequestBuilder createConnectionRequest(@Nonnull ServerInfo info);
}

View File

@ -4,6 +4,7 @@ import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.config.IPForwardingMode;
import com.velocitypowered.proxy.connection.VelocityConstants;
import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler;
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults;
import com.velocitypowered.proxy.data.GameProfile;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
@ -65,6 +66,14 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
} else if (packet instanceof Disconnect) {
Disconnect disconnect = (Disconnect) packet;
connection.disconnect();
// Do we have an outstanding notification? If so, fulfill it.
ServerConnection.ConnectionNotifier n = connection.getMinecraftConnection().getChannel()
.pipeline().get(ServerConnection.ConnectionNotifier.class);
if (n != null) {
n.getResult().complete(ConnectionRequestResults.forDisconnect(disconnect));
}
connection.getProxyPlayer().handleConnectionException(connection.getServerInfo(), disconnect);
} else if (packet instanceof SetCompression) {
SetCompression sc = (SetCompression) packet;
@ -80,6 +89,15 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
// The previous server connection should become obsolete.
existingConnection.disconnect();
}
// Do we have an outstanding notification? If so, fulfill it.
ServerConnection.ConnectionNotifier n = connection.getMinecraftConnection().getChannel()
.pipeline().get(ServerConnection.ConnectionNotifier.class);
if (n != null) {
n.onComplete();
connection.getMinecraftConnection().getChannel().pipeline().remove(n);
}
connection.getMinecraftConnection().setSessionHandler(new BackendPlaySessionHandler(connection));
connection.getProxyPlayer().setConnectedServer(connection);
}

View File

@ -1,7 +1,9 @@
package com.velocitypowered.proxy.connection.backend;
import com.velocitypowered.api.proxy.ConnectionRequestBuilder;
import com.velocitypowered.proxy.config.IPForwardingMode;
import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation;
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder;
@ -17,6 +19,7 @@ import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import io.netty.channel.*;
import io.netty.handler.timeout.ReadTimeoutHandler;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import static com.velocitypowered.network.Connections.FRAME_DECODER;
@ -28,6 +31,8 @@ import static com.velocitypowered.network.Connections.READ_TIMEOUT;
import static com.velocitypowered.network.Connections.SERVER_READ_TIMEOUT_SECONDS;
public class ServerConnection implements MinecraftConnectionAssociation {
static final String CONNECTION_NOTIFIER = "connection-notifier";
private final ServerInfo serverInfo;
private final ConnectedPlayer proxyPlayer;
private final VelocityServer server;
@ -39,7 +44,8 @@ public class ServerConnection implements MinecraftConnectionAssociation {
this.server = server;
}
public void connect() {
public CompletableFuture<ConnectionRequestBuilder.Result> connect() {
CompletableFuture<ConnectionRequestBuilder.Result> result = new CompletableFuture<>();
server.initializeGenericBootstrap()
.handler(new ChannelInitializer<Channel>() {
@Override
@ -49,7 +55,8 @@ public class ServerConnection implements MinecraftConnectionAssociation {
.addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder())
.addLast(FRAME_ENCODER, MinecraftVarintLengthEncoder.INSTANCE)
.addLast(MINECRAFT_DECODER, new MinecraftDecoder(ProtocolConstants.Direction.CLIENTBOUND))
.addLast(MINECRAFT_ENCODER, new MinecraftEncoder(ProtocolConstants.Direction.SERVERBOUND));
.addLast(MINECRAFT_ENCODER, new MinecraftEncoder(ProtocolConstants.Direction.SERVERBOUND))
.addLast(CONNECTION_NOTIFIER, new ConnectionNotifier(result));
MinecraftConnection connection = new MinecraftConnection(ch);
connection.setState(StateRegistry.HANDSHAKE);
@ -68,10 +75,11 @@ public class ServerConnection implements MinecraftConnectionAssociation {
minecraftConnection.setSessionHandler(new LoginSessionHandler(ServerConnection.this));
startHandshake();
} else {
proxyPlayer.handleConnectionException(serverInfo, future.cause());
result.completeExceptionally(future.cause());
}
}
});
return result;
}
private String createBungeeForwardingAddress() {
@ -131,4 +139,25 @@ public class ServerConnection implements MinecraftConnectionAssociation {
public String toString() {
return "[server connection] " + proxyPlayer.getProfile().getName() + " -> " + serverInfo.getName();
}
static class ConnectionNotifier extends ChannelInboundHandlerAdapter {
private final CompletableFuture<ConnectionRequestBuilder.Result> result;
public ConnectionNotifier(CompletableFuture<ConnectionRequestBuilder.Result> result) {
this.result = result;
}
public CompletableFuture<ConnectionRequestBuilder.Result> getResult() {
return result;
}
public void onComplete() {
result.complete(ConnectionRequestResults.SUCCESSFUL);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
result.completeExceptionally(cause);
}
}
}

View File

@ -83,8 +83,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
Chat chat = (Chat) packet;
if (chat.getMessage().equals("/connect")) {
ServerInfo info = new ServerInfo("test", new InetSocketAddress("localhost", 25566));
ServerConnection connection = new ServerConnection(info, player, VelocityServer.getServer());
connection.connect();
player.createConnectionRequest(info).fireAndForget();
return;
}
}

View File

@ -2,10 +2,13 @@ package com.velocitypowered.proxy.connection.client;
import com.google.common.base.Preconditions;
import com.google.gson.JsonObject;
import com.velocitypowered.api.proxy.ConnectionRequestBuilder;
import com.velocitypowered.api.util.MessagePosition;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation;
import com.velocitypowered.proxy.connection.util.ConnectionMessages;
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults;
import com.velocitypowered.proxy.data.GameProfile;
import com.velocitypowered.proxy.protocol.packet.Chat;
import com.velocitypowered.proxy.connection.MinecraftConnection;
@ -28,6 +31,7 @@ import java.net.InetSocketAddress;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
private static final PlainComponentSerializer PASS_THRU_TRANSLATE = new PlainComponentSerializer((c) -> "", TranslatableComponent::key);
@ -58,7 +62,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
@Override
public Optional<ServerInfo> getCurrentServer() {
return Optional.empty();
return connectedServer != null ? Optional.of(connectedServer.getServerInfo()) : Optional.empty();
}
public GameProfile getProfile() {
@ -102,6 +106,11 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
connection.write(chat);
}
@Override
public ConnectionRequestBuilder createConnectionRequest(@Nonnull ServerInfo info) {
return new ConnectionRequestBuilderImpl(info);
}
public ServerConnection getConnectedServer() {
return connectedServer;
}
@ -118,8 +127,10 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
String error = ThrowableUtils.briefDescription(throwable);
String userMessage;
if (connectedServer != null && connectedServer.getServerInfo().equals(info)) {
logger.error("{}: exception occurred in connection to {}", this, info.getName(), throwable);
userMessage = "Exception in server " + info.getName();
} else {
logger.error("{}: unable to connect to server {}", this, info.getName(), throwable);
userMessage = "Exception connecting to server " + info.getName();
}
handleConnectionException(info, TextComponent.builder()
@ -151,7 +162,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
}
}
public Optional<ServerInfo> getNextServerToTry() {
Optional<ServerInfo> getNextServerToTry() {
List<String> serversToTry = VelocityServer.getServer().getConfiguration().getAttemptConnectionOrder();
if (tryIndex >= serversToTry.size()) {
return Optional.empty();
@ -162,7 +173,25 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
return VelocityServer.getServer().getServers().getServer(toTryName);
}
public void connect(ServerInfo info) {
private CompletableFuture<ConnectionRequestBuilder.Result> connect(ConnectionRequestBuilderImpl request) {
if (connectionInFlight != null) {
return CompletableFuture.completedFuture(
ConnectionRequestResults.plainResult(ConnectionRequestBuilder.Status.CONNECTION_IN_PROGRESS)
);
}
if (connectedServer != null && connectedServer.getServerInfo().equals(request.getServer())) {
return CompletableFuture.completedFuture(
ConnectionRequestResults.plainResult(ConnectionRequestBuilder.Status.ALREADY_CONNECTED)
);
}
// Otherwise, initiate the connection.
ServerConnection connection = new ServerConnection(request.getServer(), this, VelocityServer.getServer());
return connection.connect();
}
void connect(ServerInfo info) {
Preconditions.checkNotNull(info, "info");
Preconditions.checkState(connectionInFlight == null, "A connection is already active!");
ServerConnection connection = new ServerConnection(info, this, VelocityServer.getServer());
@ -194,4 +223,48 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
public String toString() {
return "[connected player] " + getProfile().getName() + " (" + getRemoteAddress() + ")";
}
private class ConnectionRequestBuilderImpl implements ConnectionRequestBuilder {
private final ServerInfo info;
public ConnectionRequestBuilderImpl(ServerInfo info) {
this.info = Preconditions.checkNotNull(info, "info");
}
@Override
public ServerInfo getServer() {
return info;
}
@Override
public CompletableFuture<Result> connect() {
return ConnectedPlayer.this.connect(this);
}
@Override
public void fireAndForget() {
connect()
.whenComplete((status, throwable) -> {
if (throwable != null) {
handleConnectionException(info, throwable);
return;
}
switch (status.getStatus()) {
case ALREADY_CONNECTED:
sendMessage(ConnectionMessages.ALREADY_CONNECTED);
break;
case CONNECTION_IN_PROGRESS:
sendMessage(ConnectionMessages.IN_PROGRESS);
break;
case CONNECTION_CANCELLED:
// Ignored; the plugin probably already handled this.
break;
case SERVER_DISCONNECTED:
handleConnectionException(info, Disconnect.create(status.getReason().orElse(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR)));
break;
}
});
}
}
}

View File

@ -140,6 +140,6 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
inbound.setAssociation(player);
inbound.setState(StateRegistry.PLAY);
inbound.setSessionHandler(new InitialConnectSessionHandler(player));
player.connect(toTry.get());
player.createConnectionRequest(toTry.get()).fireAndForget();
}
}

View File

@ -0,0 +1,14 @@
package com.velocitypowered.proxy.connection.util;
import net.kyori.text.TextComponent;
import net.kyori.text.format.TextColor;
public class ConnectionMessages {
public static final TextComponent ALREADY_CONNECTED = TextComponent.of("You are already connected to this server!", TextColor.RED);
public static final TextComponent IN_PROGRESS = TextComponent.of("You are already connecting to a server!", TextColor.RED);
public static final TextComponent INTERNAL_SERVER_CONNECTION_ERROR = TextComponent.of("Internal server connection error");
private ConnectionMessages() {
throw new AssertionError();
}
}

View File

@ -0,0 +1,45 @@
package com.velocitypowered.proxy.connection.util;
import com.velocitypowered.api.proxy.ConnectionRequestBuilder;
import com.velocitypowered.proxy.protocol.packet.Disconnect;
import net.kyori.text.Component;
import net.kyori.text.serializer.ComponentSerializers;
import java.util.Optional;
public class ConnectionRequestResults {
public static final ConnectionRequestBuilder.Result SUCCESSFUL = plainResult(ConnectionRequestBuilder.Status.SUCCESS);
private ConnectionRequestResults() {
throw new AssertionError();
}
public static ConnectionRequestBuilder.Result plainResult(ConnectionRequestBuilder.Status status) {
return new ConnectionRequestBuilder.Result() {
@Override
public ConnectionRequestBuilder.Status getStatus() {
return status;
}
@Override
public Optional<Component> getReason() {
return Optional.empty();
}
};
}
public static ConnectionRequestBuilder.Result forDisconnect(Disconnect disconnect) {
Component deserialized = ComponentSerializers.JSON.deserialize(disconnect.getReason());
return new ConnectionRequestBuilder.Result() {
@Override
public ConnectionRequestBuilder.Status getStatus() {
return ConnectionRequestBuilder.Status.SERVER_DISCONNECTED;
}
@Override
public Optional<Component> getReason() {
return Optional.of(deserialized);
}
};
}
}