diff --git a/api/src/main/java/org/geysermc/floodgate/api/link/LinkRequestResult.java b/api/src/main/java/org/geysermc/floodgate/api/link/LinkRequestResult.java new file mode 100644 index 00000000..8df7305e --- /dev/null +++ b/api/src/main/java/org/geysermc/floodgate/api/link/LinkRequestResult.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2019-2021 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Floodgate + */ + +package org.geysermc.floodgate.api.link; + +/** + * This enum has all the available result types of both creating a player link request and + * validating it. + */ +public enum LinkRequestResult { + /** + * An unknown error encountered while creating / verifying the link request. + */ + UNKNOWN_ERROR, + /** + * @deprecated this result isn't used. Instead the link code is returned + */ + REQUEST_CREATED, + /** + * The specified bedrock username is already linked to a Java account. + */ + ALREADY_LINKED, + /** + * The Bedrock player verified the request too late. The request has been expired. + */ + REQUEST_EXPIRED, + /** + * The Java player hasn't requested a link to this Bedrock account. + */ + NO_LINK_REQUESTED, + /** + * The entered code is invalid. + */ + INVALID_CODE, + /** + * The link request has been verified successfully! + */ + LINK_COMPLETED +} diff --git a/api/src/main/java/org/geysermc/floodgate/api/link/PlayerLink.java b/api/src/main/java/org/geysermc/floodgate/api/link/PlayerLink.java index 3a6fd8ae..1fad9e14 100644 --- a/api/src/main/java/org/geysermc/floodgate/api/link/PlayerLink.java +++ b/api/src/main/java/org/geysermc/floodgate/api/link/PlayerLink.java @@ -54,10 +54,10 @@ public interface PlayerLink { /** * Tells if the given player is a linked player * - * @param bedrockId the bedrock uuid of the linked player + * @param playerId the uuid of the player to check, can be both a Java or a Bedrock uuid * @return true if the player is a linked player */ - CompletableFuture isLinkedPlayer(UUID bedrockId); + CompletableFuture isLinkedPlayer(UUID playerId); /** * Links a Java account to a Bedrock account. @@ -77,6 +77,37 @@ public interface PlayerLink { */ CompletableFuture unlinkPlayer(UUID javaId); + /** + * Creates a link request for the given Java player. + * + * @param javaId the uuid of the Java player + * @param javaUsername the username of the Java player + * @param bedrockUsername the username of the Bedrock player receiving the link request + * @return a future holding the result of the link request which will be a {@link + * LinkRequestResult} on failure and the link code (string) on success + */ + CompletableFuture createLinkRequest( + UUID javaId, + String javaUsername, + String bedrockUsername + ); + + /** + * Verifies a link request for the given Bedrock player. + * + * @param bedrockId the uuid of the Bedrock player + * @param javaUsername the username of the Java players who requested the link + * @param bedrockUsername the username of the Bedrock player + * @param code the code created in {@link #createLinkRequest(UUID, String, String)} + * @return a future holding the result of the link verification + */ + CompletableFuture verifyLinkRequest( + UUID bedrockId, + String javaUsername, + String bedrockUsername, + String code + ); + /** * Return if account linking is enabled. The difference between enabled and allowed is that * 'enabled' still allows already linked people to join with their linked account while 'allow diff --git a/bungee/src/main/java/org/geysermc/floodgate/inject/bungee/BungeeInjector.java b/bungee/src/main/java/org/geysermc/floodgate/inject/bungee/BungeeInjector.java index 072a339f..7cf8c9cd 100644 --- a/bungee/src/main/java/org/geysermc/floodgate/inject/bungee/BungeeInjector.java +++ b/bungee/src/main/java/org/geysermc/floodgate/inject/bungee/BungeeInjector.java @@ -94,7 +94,6 @@ public final class BungeeInjector extends CommonPlatformInjector { } public void injectClient(Channel channel, boolean clientToProxy) { - logger.info("Client to proxy? " + clientToProxy); injectAddonsCall(channel, !clientToProxy); addInjectedClient(channel); } diff --git a/common/src/main/java/org/geysermc/floodgate/command/LinkAccountCommand.java b/common/src/main/java/org/geysermc/floodgate/command/LinkAccountCommand.java index 06ca8994..1b30712d 100644 --- a/common/src/main/java/org/geysermc/floodgate/command/LinkAccountCommand.java +++ b/common/src/main/java/org/geysermc/floodgate/command/LinkAccountCommand.java @@ -33,16 +33,13 @@ import cloud.commandframework.Description; import cloud.commandframework.arguments.standard.StringArgument; import cloud.commandframework.context.CommandContext; import com.google.inject.Inject; -import java.util.HashMap; -import java.util.Map; -import java.util.Random; import lombok.Getter; import lombok.NoArgsConstructor; import org.geysermc.floodgate.api.FloodgateApi; -import org.geysermc.floodgate.api.link.LinkRequest; +import org.geysermc.floodgate.api.link.LinkRequestResult; import org.geysermc.floodgate.api.link.PlayerLink; +import org.geysermc.floodgate.api.logger.FloodgateLogger; import org.geysermc.floodgate.config.FloodgateConfig; -import org.geysermc.floodgate.link.LinkRequestImpl; import org.geysermc.floodgate.platform.command.CommandMessage; import org.geysermc.floodgate.platform.command.FloodgateCommand; import org.geysermc.floodgate.player.UserAudience; @@ -51,9 +48,8 @@ import org.geysermc.floodgate.player.UserAudienceArgument; @NoArgsConstructor public final class LinkAccountCommand implements FloodgateCommand { - private final Map activeLinkRequests = new HashMap<>(); - @Inject private FloodgateApi api; + @Inject private FloodgateLogger logger; @Override public Command buildCommand(CommandManager commandManager) { @@ -77,81 +73,68 @@ public final class LinkAccountCommand implements FloodgateCommand { return; } - link.isLinkedPlayer(sender.uuid()) - .whenComplete((linked, throwable) -> { - if (throwable != null) { - sender.sendMessage(CommonCommandMessage.IS_LINKED_ERROR); - return; - } + // when the player is a Bedrock player + if (api.isBedrockPlayer(sender.uuid())) { + if (!context.contains("code")) { + sender.sendMessage(Message.BEDROCK_USAGE); + return; + } - if (linked) { - sender.sendMessage(Message.ALREADY_LINKED); - return; - } + UserAudience targetUser = context.get("player"); + String targetName = targetUser.username(); + String code = context.get("code"); - // when the player is a Java player - if (!api.isBedrockPlayer(sender.uuid())) { - if (context.contains("code")) { - sender.sendMessage(Message.JAVA_USAGE); + link.verifyLinkRequest(sender.uuid(), targetName, sender.username(), code) + .whenComplete((result, throwable) -> { + if (throwable != null || result == LinkRequestResult.UNKNOWN_ERROR) { + sender.sendMessage(Message.LINK_REQUEST_ERROR); return; } - String code = String.format("%04d", new Random().nextInt(10000)); - UserAudience targetUser = context.get("player"); - String targetName = targetUser.username(); + switch (result) { + case ALREADY_LINKED: + sender.sendMessage(Message.ALREADY_LINKED); + break; + case NO_LINK_REQUESTED: + sender.sendMessage(Message.NO_LINK_REQUESTED); + break; + case INVALID_CODE: + sender.sendMessage(Message.INVALID_CODE); + break; + case REQUEST_EXPIRED: + sender.sendMessage(Message.LINK_REQUEST_EXPIRED); + break; + case LINK_COMPLETED: + sender.disconnect(Message.LINK_REQUEST_COMPLETED, targetName); + break; + } + }); + return; + } - LinkRequest linkRequest = new LinkRequestImpl( - sender.username(), sender.uuid(), code, targetName); + if (context.contains("code")) { + sender.sendMessage(Message.JAVA_USAGE); + return; + } - activeLinkRequests.put(sender.username(), linkRequest); - sender.sendMessage( - Message.LINK_REQUEST_CREATED, - targetName, sender.username(), code); + UserAudience targetUser = context.get("player"); + String targetName = targetUser.username(); + + link.createLinkRequest(sender.uuid(), sender.username(), targetName) + .whenComplete((result, throwable) -> { + if (throwable != null || result == LinkRequestResult.UNKNOWN_ERROR) { + sender.sendMessage(Message.LINK_REQUEST_ERROR); return; } - // when the player is a Bedrock player - - if (!context.contains("code")) { - sender.sendMessage(Message.BEDROCK_USAGE); + if (!(result instanceof String)) { + logger.error("Expected string code, got {}", result); + sender.sendMessage(Message.LINK_REQUEST_ERROR); return; } - UserAudience targetUser = context.get("player"); - String targetName = targetUser.username(); - String code = context.get("code"); - - LinkRequest request = activeLinkRequests.getOrDefault(targetName, null); - if (request == null || - !request.isRequestedPlayer(api.getPlayer(sender.uuid()))) { - sender.sendMessage(Message.NO_LINK_REQUESTED); - return; - } - - if (!request.getLinkCode().equals(code)) { - sender.sendMessage(Message.INVALID_CODE); - return; - } - - // Delete the request, whether it has expired or is successful - activeLinkRequests.remove(targetName); - if (request.isExpired(link.getVerifyLinkTimeout())) { - sender.sendMessage(Message.LINK_REQUEST_EXPIRED); - return; - } - - link.linkPlayer(sender.uuid(), request.getJavaUniqueId(), - request.getJavaUsername()) - .whenComplete((unused, error) -> { - if (error != null) { - sender.sendMessage(Message.LINK_REQUEST_ERROR); - return; - } - sender.disconnect( - Message.LINK_REQUEST_COMPLETED, - request.getJavaUsername() - ); - }); + sender.sendMessage(Message.LINK_REQUEST_CREATED, + targetName, sender.username(), result); }); } diff --git a/common/src/main/java/org/geysermc/floodgate/config/updater/ConfigFileUpdater.java b/common/src/main/java/org/geysermc/floodgate/config/updater/ConfigFileUpdater.java index b742568b..85cce88a 100644 --- a/common/src/main/java/org/geysermc/floodgate/config/updater/ConfigFileUpdater.java +++ b/common/src/main/java/org/geysermc/floodgate/config/updater/ConfigFileUpdater.java @@ -93,7 +93,6 @@ public final class ConfigFileUpdater { spaces = " "; //todo allow rename of subcategory? - System.out.println("subcategory: " + line.substring(0, splitIndex)); map = (Map) currentVersion.get(line.substring(0, splitIndex)); map.entrySet().forEach(System.out::println); continue; diff --git a/common/src/main/java/org/geysermc/floodgate/config/updater/ConfigUpdater.java b/common/src/main/java/org/geysermc/floodgate/config/updater/ConfigUpdater.java index 5b3eec7f..4571a69f 100644 --- a/common/src/main/java/org/geysermc/floodgate/config/updater/ConfigUpdater.java +++ b/common/src/main/java/org/geysermc/floodgate/config/updater/ConfigUpdater.java @@ -58,8 +58,6 @@ public final class ConfigUpdater { Map config = new Yaml().load(configReader); - config.forEach((key, value) -> System.out.println(key + ":" + value + " (" + value.getClass().getName() + ")")); - // new name -> old name Map renames = new HashMap<>(); diff --git a/common/src/main/java/org/geysermc/floodgate/link/CommonPlayerLink.java b/common/src/main/java/org/geysermc/floodgate/link/CommonPlayerLink.java index 03e93e36..eee6f18d 100644 --- a/common/src/main/java/org/geysermc/floodgate/link/CommonPlayerLink.java +++ b/common/src/main/java/org/geysermc/floodgate/link/CommonPlayerLink.java @@ -26,10 +26,14 @@ package org.geysermc.floodgate.link; import com.google.inject.Inject; +import java.util.Random; +import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import lombok.AccessLevel; import lombok.Getter; +import org.geysermc.floodgate.api.FloodgateApi; +import org.geysermc.floodgate.api.link.LinkRequest; import org.geysermc.floodgate.api.link.PlayerLink; import org.geysermc.floodgate.api.logger.FloodgateLogger; import org.geysermc.floodgate.config.FloodgateConfig; @@ -46,6 +50,10 @@ public abstract class CommonPlayerLink implements PlayerLink { @Getter(AccessLevel.PROTECTED) private FloodgateLogger logger; + @Inject + @Getter(AccessLevel.PROTECTED) + private FloodgateApi api; + @Inject private void init(FloodgateConfig config) { FloodgateConfig.PlayerLinkConfig linkConfig = config.getPlayerLink(); @@ -54,6 +62,14 @@ public abstract class CommonPlayerLink implements PlayerLink { verifyLinkTimeout = linkConfig.getLinkCodeTimeout(); } + public String createCode() { + return String.format("%04d", new Random().nextInt(10000)); + } + + public boolean isRequestedPlayer(LinkRequest request, UUID bedrockId) { + return request.isRequestedPlayer(api.getPlayer(bedrockId)); + } + @Override public void stop() { executorService.shutdown(); diff --git a/common/src/main/java/org/geysermc/floodgate/link/DisabledPlayerLink.java b/common/src/main/java/org/geysermc/floodgate/link/DisabledPlayerLink.java index 0c4fa825..a8dd38f2 100644 --- a/common/src/main/java/org/geysermc/floodgate/link/DisabledPlayerLink.java +++ b/common/src/main/java/org/geysermc/floodgate/link/DisabledPlayerLink.java @@ -28,6 +28,7 @@ package org.geysermc.floodgate.link; import java.util.UUID; import java.util.concurrent.CompletableFuture; import org.geysermc.floodgate.api.FloodgateApi; +import org.geysermc.floodgate.api.link.LinkRequestResult; import org.geysermc.floodgate.api.link.PlayerLink; import org.geysermc.floodgate.util.LinkedPlayer; @@ -61,6 +62,25 @@ final class DisabledPlayerLink implements PlayerLink { return null; } + @Override + public CompletableFuture createLinkRequest( + UUID javaId, + String javaUsername, + String bedrockUsername + ) { + return null; + } + + @Override + public CompletableFuture verifyLinkRequest( + UUID bedrockId, + String javaUsername, + String bedrockUsername, + String code + ) { + return null; + } + @Override public boolean isEnabled() { return false; diff --git a/common/src/main/java/org/geysermc/floodgate/link/GlobalPlayerLinking.java b/common/src/main/java/org/geysermc/floodgate/link/GlobalPlayerLinking.java index e8ce54d6..545d7fb4 100644 --- a/common/src/main/java/org/geysermc/floodgate/link/GlobalPlayerLinking.java +++ b/common/src/main/java/org/geysermc/floodgate/link/GlobalPlayerLinking.java @@ -30,6 +30,7 @@ import com.google.inject.Inject; import java.util.UUID; import java.util.concurrent.CompletableFuture; import org.geysermc.floodgate.api.FloodgateApi; +import org.geysermc.floodgate.api.link.LinkRequestResult; import org.geysermc.floodgate.util.HttpUtils; import org.geysermc.floodgate.util.HttpUtils.HttpResponse; import org.geysermc.floodgate.util.LinkedPlayer; @@ -100,6 +101,8 @@ public class GlobalPlayerLinking extends CommonPlayerLink { // player linking and unlinking now goes through the global player linking server. // so individual servers can't register nor unlink players. + //todo probably return a failed future instead of returning null? + @Override public CompletableFuture linkPlayer(UUID bedrockId, UUID javaId, String username) { return null; @@ -109,4 +112,23 @@ public class GlobalPlayerLinking extends CommonPlayerLink { public CompletableFuture unlinkPlayer(UUID javaId) { return null; } + + @Override + public CompletableFuture createLinkRequest( + UUID javaId, + String javaUsername, + String bedrockUsername + ) { + return null; + } + + @Override + public CompletableFuture verifyLinkRequest( + UUID bedrockId, + String javaUsername, + String bedrockUsername, + String code + ) { + return null; + } } diff --git a/database/sqlite/src/main/java/org/geysermc/floodgate/database/SqliteDatabase.java b/database/sqlite/src/main/java/org/geysermc/floodgate/database/SqliteDatabase.java index 41dab4f0..8b9272fa 100644 --- a/database/sqlite/src/main/java/org/geysermc/floodgate/database/SqliteDatabase.java +++ b/database/sqlite/src/main/java/org/geysermc/floodgate/database/SqliteDatabase.java @@ -34,13 +34,19 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; +import java.util.HashMap; +import java.util.Map; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; +import org.geysermc.floodgate.api.link.LinkRequest; +import org.geysermc.floodgate.api.link.LinkRequestResult; import org.geysermc.floodgate.link.CommonPlayerLink; +import org.geysermc.floodgate.link.LinkRequestImpl; import org.geysermc.floodgate.util.LinkedPlayer; public class SqliteDatabase extends CommonPlayerLink { + private final Map activeLinkRequests = new HashMap<>(); private Connection connection; @Inject @@ -99,14 +105,14 @@ public class SqliteDatabase extends CommonPlayerLink { } @Override - public CompletableFuture isLinkedPlayer(UUID bedrockId) { + public CompletableFuture isLinkedPlayer(UUID playerId) { return CompletableFuture.supplyAsync(() -> { try { PreparedStatement query = connection.prepareStatement( "select javaUniqueId from LinkedPlayers where bedrockId = ? or javaUniqueId = ?" ); - query.setString(1, bedrockId.toString()); - query.setString(2, bedrockId.toString()); + query.setString(1, playerId.toString()); + query.setString(2, playerId.toString()); ResultSet result = query.executeQuery(); return result.next(); } catch (SQLException | NullPointerException exception) { @@ -120,20 +126,24 @@ public class SqliteDatabase extends CommonPlayerLink { @Override public CompletableFuture linkPlayer(UUID bedrockId, UUID javaId, String username) { - return CompletableFuture.runAsync(() -> { - try { - PreparedStatement query = connection.prepareStatement( - "insert into LinkedPlayers values(?, ?, ?)" - ); - query.setString(1, bedrockId.toString()); - query.setString(2, javaId.toString()); - query.setString(3, username); - query.executeUpdate(); - } catch (SQLException | NullPointerException exception) { - getLogger().error("Error while linking player", exception); - throw new CompletionException("Error while linking player", exception); - } - }, getExecutorService()); + return CompletableFuture.runAsync( + () -> linkPlayer0(bedrockId, javaId, username), + getExecutorService()); + } + + private void linkPlayer0(UUID bedrockId, UUID javaId, String username) { + try { + PreparedStatement query = connection.prepareStatement( + "insert into LinkedPlayers values(?, ?, ?)" + ); + query.setString(1, bedrockId.toString()); + query.setString(2, javaId.toString()); + query.setString(3, username); + query.executeUpdate(); + } catch (SQLException | NullPointerException exception) { + getLogger().error("Error while linking player", exception); + throw new CompletionException("Error while linking player", exception); + } } @Override @@ -152,4 +162,50 @@ public class SqliteDatabase extends CommonPlayerLink { } }, getExecutorService()); } + + @Override + public CompletableFuture createLinkRequest( + UUID javaId, + String javaUsername, + String bedrockUsername + ) { + return CompletableFuture.supplyAsync(() -> { + LinkRequest request = + new LinkRequestImpl(javaUsername, javaId, createCode(), bedrockUsername); + + activeLinkRequests.put(javaUsername, request); + + return request.getLinkCode(); + }, getExecutorService()); + } + + @Override + public CompletableFuture verifyLinkRequest( + UUID bedrockId, + String javaUsername, + String bedrockUsername, + String code + ) { + return CompletableFuture.supplyAsync(() -> { + LinkRequest request = activeLinkRequests.get(javaUsername); + + if (request == null || !isRequestedPlayer(request, bedrockId)) { + return LinkRequestResult.NO_LINK_REQUESTED; + } + + if (!request.getLinkCode().equals(code)) { + return LinkRequestResult.INVALID_CODE; + } + + // link request can be removed. Doesn't matter if the request is expired or not + activeLinkRequests.remove(javaUsername); + + if (request.isExpired(getVerifyLinkTimeout())) { + return LinkRequestResult.REQUEST_EXPIRED; + } + + linkPlayer0(bedrockId, request.getJavaUniqueId(), javaUsername); + return LinkRequestResult.LINK_COMPLETED; + }, getExecutorService()); + } } diff --git a/velocity/src/main/java/org/geysermc/floodgate/addon/data/VelocityServerDataHandler.java b/velocity/src/main/java/org/geysermc/floodgate/addon/data/VelocityServerDataHandler.java index f16a0aad..dc4d0225 100644 --- a/velocity/src/main/java/org/geysermc/floodgate/addon/data/VelocityServerDataHandler.java +++ b/velocity/src/main/java/org/geysermc/floodgate/addon/data/VelocityServerDataHandler.java @@ -85,8 +85,6 @@ public final class VelocityServerDataHandler extends MessageToMessageEncoder