1
0
mirror of https://github.com/GeyserMC/Floodgate.git synced 2026-01-06 15:42:03 +00:00

Use Micronaut HTTP client, config loading in boostrap, work on database

This commit is contained in:
Tim203
2023-05-03 00:51:14 +02:00
parent f8fa963904
commit d09c98f9cb
43 changed files with 1119 additions and 859 deletions

View File

@@ -33,20 +33,15 @@ import org.geysermc.floodgate.api.InstanceHolder;
import org.geysermc.floodgate.api.event.FloodgateEventBus;
import org.geysermc.floodgate.api.handshake.HandshakeHandlers;
import org.geysermc.floodgate.api.inject.PlatformInjector;
import org.geysermc.floodgate.api.link.PlayerLink;
import org.geysermc.floodgate.api.logger.FloodgateLogger;
import org.geysermc.floodgate.api.packet.PacketHandlers;
import org.geysermc.floodgate.core.config.Properties;
import org.geysermc.floodgate.core.database.PlayerLinkRepository;
import org.geysermc.floodgate.core.database.entity.LinkedPlayer;
import org.geysermc.floodgate.core.event.EventBus;
import org.geysermc.floodgate.core.event.lifecycle.PostEnableEvent;
import org.geysermc.floodgate.core.event.lifecycle.ShutdownEvent;
import org.geysermc.floodgate.core.util.EagerSingleton;
import org.geysermc.floodgate.isolation.library.Library;
import org.geysermc.floodgate.core.util.GlobalBeanCache;
import org.geysermc.floodgate.isolation.library.LibraryManager;
import org.geysermc.floodgate.isolation.library.Repository;
import org.geysermc.floodgate.isolation.library.info.DependencyInfoLoader;
public abstract class FloodgatePlatform {
private static final UUID KEY = UUID.randomUUID();
@@ -65,32 +60,11 @@ public abstract class FloodgatePlatform {
public void load() {
long startTime = System.currentTimeMillis();
var infoLoader = DependencyInfoLoader.load(
getClass().getClassLoader().getResource("dependencyInfo.txt")
);
manager
// .addLibrary(
// Library.builder(infoLoader)
// .id("guava")
// .repository(Repository.MAVEN_CENTRAL)
// .groupId("com.google.guava")
// .artifactId("guava")
// .build()
// )
.addLibrary(
Library.builder()
.id("local-linking")
.repository(Repository.OPEN_COLLAB)
.groupId("org.geysermc.floodgate")
.artifactId("database")
.version("a-version")
.build()
)
.apply();
GlobalBeanCache.cacheIfAbsent("libraryManager", () -> manager);
//noinspection unchecked
context = ApplicationContext.builder(manager.classLoader())
.singletons(manager)
.properties(Map.of(
"platform.proxy", isProxy()
))
@@ -102,34 +76,11 @@ public abstract class FloodgatePlatform {
onContextCreated(context);
context.start();
LinkedPlayer link = new LinkedPlayer()
.bedrockId(UUID.fromString("00000000-0000-0000-0009-01f64f65c7c3"))
.javaUniqueId(UUID.fromString("d34eb447-6e90-4c78-9281-600df88aef1d"))
.javaUsername("Tim203");
System.out.println(context.getBean(PlayerLinkRepository.class).save(link));
System.out.println(context.getBean(PlayerLinkRepository.class)
.findByBedrockId(UUID.fromString("00000000-0000-0000-0009-01f64f65c7c3")));
System.out.println(context.getBean(PlayerLinkRepository.class)
.findByJavaUniqueId(UUID.fromString("d34eb447-6e90-4c78-9281-600df88aef1d")));
System.out.println(context.getBean(PlayerLinkRepository.class)
.existsByBedrockId(UUID.fromString("00000000-0000-0000-0009-01f64f65c7c3")));
System.out.println(context.getBean(PlayerLinkRepository.class)
.existsByJavaUniqueId(UUID.fromString("d34eb447-6e90-4c78-9281-600df88aef1d")));
// var scopeBuilder = BeanScope.builder()
// .bean("isProxy", boolean.class, isProxy())
// .modules(new CoreModule())
// // temporary fix for https://github.com/avaje/avaje-inject/issues/295
// .modules(makeModule(isProxy() ? PROXY_MODULE : SERVER_MODULE));
// onBuildBeanScope(scopeBuilder);
// scope = scopeBuilder.build();
injector = context.getBean(PlatformInjector.class);
InstanceHolder.set(
context.getBean(FloodgateApi.class),
context.getBean(PlayerLink.class),
null, // todo context.getBean(PlayerLink.class),
context.getBean(FloodgateEventBus.class),
injector,
context.getBean(PacketHandlers.class),

View File

@@ -28,8 +28,6 @@ package org.geysermc.floodgate.core.api;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.ImmutableSet;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import io.micronaut.context.BeanProvider;
import jakarta.inject.Inject;
import java.util.Collection;
@@ -45,12 +43,13 @@ import org.geysermc.floodgate.api.logger.FloodgateLogger;
import org.geysermc.floodgate.api.player.FloodgatePlayer;
import org.geysermc.floodgate.api.unsafe.Unsafe;
import org.geysermc.floodgate.core.config.FloodgateConfig;
import org.geysermc.floodgate.core.http.xbox.GetGamertagResult;
import org.geysermc.floodgate.core.http.xbox.GetXuidResult;
import org.geysermc.floodgate.core.http.xbox.XboxClient;
import org.geysermc.floodgate.core.pluginmessage.PluginMessageManager;
import org.geysermc.floodgate.core.pluginmessage.channel.FormChannel;
import org.geysermc.floodgate.core.pluginmessage.channel.TransferChannel;
import org.geysermc.floodgate.core.scope.ServerOnly;
import org.geysermc.floodgate.core.util.Constants;
import org.geysermc.floodgate.core.util.HttpClient;
import org.geysermc.floodgate.core.util.Utils;
@ServerOnly
@@ -63,8 +62,8 @@ public class SimpleFloodgateApi implements FloodgateApi {
@Inject BeanProvider<PluginMessageManager> pluginMessageManager;
@Inject FloodgateConfig config;
@Inject HttpClient httpClient;
@Inject FloodgateLogger logger;
@Inject XboxClient xboxClient;
@Override
public String getPlayerPrefix() {
@@ -148,36 +147,12 @@ public class SimpleFloodgateApi implements FloodgateApi {
@Override
public CompletableFuture<Long> getXuidFor(String gamertag) {
if (gamertag == null || gamertag.isEmpty() || gamertag.length() > 16) {
return Utils.failedFuture(new IllegalStateException("Received an invalid gamertag"));
}
return httpClient.asyncGet(Constants.GET_XUID_URL + gamertag)
.thenApply(result -> {
JsonObject response = result.getResponse();
if (!result.isCodeOk()) {
throw new IllegalStateException(response.get("message").getAsString());
}
JsonElement xuid = response.get("xuid");
return xuid != null ? xuid.getAsLong() : null;
});
return xboxClient.xuidByGamertag(gamertag).thenApply(GetXuidResult::xuid);
}
@Override
public CompletableFuture<String> getGamertagFor(long xuid) {
return httpClient.asyncGet(Constants.GET_GAMERTAG_URL + xuid)
.thenApply(result -> {
JsonObject response = result.getResponse();
if (!result.isCodeOk()) {
throw new IllegalStateException(response.get("message").getAsString());
}
JsonElement gamertag = response.get("gamertag");
return gamertag != null ? gamertag.getAsString() : null;
});
return xboxClient.gamertagByXuid(xuid).thenApply(GetGamertagResult::gamertag);
}
@Override

View File

@@ -33,14 +33,15 @@ import cloud.commandframework.context.CommandContext;
import io.micronaut.context.annotation.Secondary;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import java.util.concurrent.CompletableFuture;
import lombok.Getter;
import org.geysermc.floodgate.api.FloodgateApi;
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.core.command.util.Permission;
import org.geysermc.floodgate.core.config.FloodgateConfig;
import org.geysermc.floodgate.core.link.CommonPlayerLink;
import org.geysermc.floodgate.core.link.GlobalPlayerLinking;
import org.geysermc.floodgate.core.link.LinkVerificationException;
import org.geysermc.floodgate.core.platform.command.FloodgateCommand;
import org.geysermc.floodgate.core.platform.command.TranslatableMessage;
import org.geysermc.floodgate.core.player.UserAudience;
@@ -48,11 +49,13 @@ import org.geysermc.floodgate.core.player.UserAudience.PlayerAudience;
import org.geysermc.floodgate.core.player.audience.ProfileAudience;
import org.geysermc.floodgate.core.player.audience.ProfileAudienceArgument;
import org.geysermc.floodgate.core.util.Constants;
import org.geysermc.floodgate.core.util.Utils;
@Singleton
@Secondary
public final class LinkAccountCommand implements FloodgateCommand {
@Inject FloodgateApi api;
@Inject CommonPlayerLink link;
@Inject FloodgateLogger logger;
@Override
@@ -71,11 +74,9 @@ public final class LinkAccountCommand implements FloodgateCommand {
public void execute(CommandContext<UserAudience> context) {
UserAudience sender = context.getSender();
PlayerLink link = api.getPlayerLink();
//todo make this less hacky
if (link instanceof GlobalPlayerLinking) {
if (((GlobalPlayerLinking) link).getDatabaseImpl() != null) {
if (((GlobalPlayerLinking) link).getDatabase() != null) {
sender.sendMessage(CommonCommandMessage.LOCAL_LINKING_NOTICE,
Constants.LINK_INFO_URL);
} else {
@@ -85,7 +86,7 @@ public final class LinkAccountCommand implements FloodgateCommand {
}
}
if (!link.isEnabledAndAllowed()) {
if (!link.isActive()) {
sender.sendMessage(CommonCommandMessage.LINKING_DISABLED);
return;
}
@@ -103,33 +104,40 @@ public final class LinkAccountCommand implements FloodgateCommand {
String code = context.get("code");
link.verifyLinkRequest(sender.uuid(), targetName, sender.username(), code)
.whenComplete((result, throwable) -> {
if (throwable != null || result == LinkRequestResult.UNKNOWN_ERROR) {
link.linkRequest(targetName)
.thenApply(request -> {
if (request == null || link.isRequestedPlayer(request, sender.uuid())) {
throw LinkVerificationException.NO_LINK_REQUESTED;
}
if (!request.linkCode().equals(code)) {
throw LinkVerificationException.INVALID_CODE;
}
if (request.isExpired(link.getVerifyLinkTimeout())) {
throw LinkVerificationException.LINK_REQUEST_EXPIRED;
}
return request;
})
.thenCompose(request ->
CompletableFuture.allOf(
link.invalidateLinkRequest(request),
link.addLink(
request.javaUniqueId(), request.javaUsername(), sender.uuid()
)
)
)
.whenComplete(($, throwable) -> {
if (throwable instanceof LinkVerificationException exception) {
sender.sendMessage(exception.message());
return;
}
if (throwable != null) {
sender.sendMessage(Message.LINK_REQUEST_ERROR);
return;
}
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;
default:
sender.disconnect("Invalid account linking result");
break;
}
sender.disconnect(Message.LINK_REQUEST_COMPLETED, targetName);
});
return;
}
@@ -139,21 +147,16 @@ public final class LinkAccountCommand implements FloodgateCommand {
return;
}
link.createLinkRequest(sender.uuid(), sender.username(), targetName)
.whenComplete((result, throwable) -> {
if (throwable != null || result == LinkRequestResult.UNKNOWN_ERROR) {
String username = sender.username();
String code = Utils.generateCode(6);
link.createLinkRequest(sender.uuid(), username, targetName, code)
.whenComplete(($, throwable) -> {
if (throwable != null) {
sender.sendMessage(Message.LINK_REQUEST_ERROR);
return;
}
if (!(result instanceof String)) {
logger.error("Expected string code, got {}", result);
sender.sendMessage(Message.LINK_REQUEST_ERROR);
return;
}
sender.sendMessage(Message.LINK_REQUEST_CREATED,
targetName, sender.username(), result);
sender.sendMessage(Message.LINK_REQUEST_CREATED, targetName, username, code);
});
}

View File

@@ -32,10 +32,9 @@ import cloud.commandframework.context.CommandContext;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import lombok.Getter;
import org.geysermc.floodgate.api.FloodgateApi;
import org.geysermc.floodgate.api.link.PlayerLink;
import org.geysermc.floodgate.core.command.util.Permission;
import org.geysermc.floodgate.core.config.FloodgateConfig;
import org.geysermc.floodgate.core.link.CommonPlayerLink;
import org.geysermc.floodgate.core.link.GlobalPlayerLinking;
import org.geysermc.floodgate.core.platform.command.FloodgateCommand;
import org.geysermc.floodgate.core.platform.command.TranslatableMessage;
@@ -45,7 +44,7 @@ import org.geysermc.floodgate.core.util.Constants;
@Singleton
public final class UnlinkAccountCommand implements FloodgateCommand {
@Inject FloodgateApi api;
@Inject CommonPlayerLink link;
@Override
public Command<UserAudience> buildCommand(CommandManager<UserAudience> commandManager) {
@@ -61,11 +60,9 @@ public final class UnlinkAccountCommand implements FloodgateCommand {
public void execute(CommandContext<UserAudience> context) {
UserAudience sender = context.getSender();
PlayerLink link = api.getPlayerLink();
//todo make this less hacky
if (link instanceof GlobalPlayerLinking) {
if (((GlobalPlayerLinking) link).getDatabaseImpl() != null) {
if (((GlobalPlayerLinking) link).getDatabase() != null) {
sender.sendMessage(CommonCommandMessage.LOCAL_LINKING_NOTICE,
Constants.LINK_INFO_URL);
} else {
@@ -75,12 +72,12 @@ public final class UnlinkAccountCommand implements FloodgateCommand {
}
}
if (!link.isEnabledAndAllowed()) {
if (!link.isActive()) {
sender.sendMessage(CommonCommandMessage.LINKING_DISABLED);
return;
}
link.isLinkedPlayer(sender.uuid())
link.isLinked(sender.uuid())
.whenComplete((linked, error) -> {
if (error != null) {
sender.sendMessage(CommonCommandMessage.IS_LINKED_ERROR);
@@ -92,7 +89,7 @@ public final class UnlinkAccountCommand implements FloodgateCommand {
return;
}
link.unlinkPlayer(sender.uuid())
link.unlink(sender.uuid())
.whenComplete((unused, error1) -> {
if (error1 != null) {
sender.sendMessage(Message.UNLINK_ERROR);

View File

@@ -29,8 +29,7 @@ import cloud.commandframework.ArgumentDescription;
import cloud.commandframework.Command;
import cloud.commandframework.CommandManager;
import cloud.commandframework.context.CommandContext;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import io.micronaut.http.client.exceptions.HttpClientResponseException;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import java.util.UUID;
@@ -40,6 +39,8 @@ import org.geysermc.floodgate.api.logger.FloodgateLogger;
import org.geysermc.floodgate.core.command.util.Permission;
import org.geysermc.floodgate.core.config.FloodgateConfig;
import org.geysermc.floodgate.core.config.ProxyFloodgateConfig;
import org.geysermc.floodgate.core.http.UnsuccessfulResponse;
import org.geysermc.floodgate.core.http.xbox.XboxClient;
import org.geysermc.floodgate.core.platform.command.CommandUtil;
import org.geysermc.floodgate.core.platform.command.FloodgateCommand;
import org.geysermc.floodgate.core.platform.command.TranslatableMessage;
@@ -47,13 +48,12 @@ import org.geysermc.floodgate.core.platform.util.PlayerType;
import org.geysermc.floodgate.core.player.UserAudience;
import org.geysermc.floodgate.core.player.audience.ProfileAudience;
import org.geysermc.floodgate.core.player.audience.ProfileAudienceArgument;
import org.geysermc.floodgate.core.util.Constants;
import org.geysermc.floodgate.core.util.HttpClient;
@Singleton
public class WhitelistCommand implements FloodgateCommand {
@Inject FloodgateApi api;
@Inject FloodgateConfig config;
@Inject HttpClient httpClient;
@Inject XboxClient xboxClient;
@Inject FloodgateLogger logger;
@Override
@@ -86,7 +86,7 @@ public class WhitelistCommand implements FloodgateCommand {
}
if (uuid != null) {
if (!FloodgateApi.getInstance().isFloodgateId(uuid)) {
if (!api.isFloodgateId(uuid)) {
sender.sendMessage(Message.INVALID_USERNAME);
return;
}
@@ -129,32 +129,34 @@ public class WhitelistCommand implements FloodgateCommand {
final String strippedName = name;
// We need to get the UUID of the player if it's not manually specified
httpClient.asyncGet(Constants.GET_XUID_URL + name)
xboxClient.xuidByGamertag(name)
.whenComplete((result, error) -> {
if (error != null) {
sender.sendMessage(Message.API_UNAVAILABLE);
error.printStackTrace();
if (!(error instanceof HttpClientResponseException exception)) {
sender.sendMessage(Message.API_UNAVAILABLE);
error.printStackTrace();
return;
}
sender.sendMessage(Message.UNEXPECTED_ERROR);
var response = exception.getResponse().getBody(UnsuccessfulResponse.class);
var message =
response.isPresent() ?
response.get().message() :
exception.getMessage();
logger.error(
"Got an error from requesting the xuid of a Bedrock player: {}",
message
);
return;
}
JsonObject response = result.getResponse();
if (!result.isCodeOk()) {
sender.sendMessage(Message.UNEXPECTED_ERROR);
logger.error(
"Got an error from requesting the xuid of a Bedrock player: {}",
response.get("message").getAsString()
);
}
JsonElement xuidElement = response.get("xuid");
if (xuidElement == null) {
Long xuid = result.xuid();
if (xuid == null) {
sender.sendMessage(Message.USER_NOT_FOUND);
return;
}
String xuid = xuidElement.getAsString();
CommandUtil commandUtil = context.get("CommandUtil");
try {

View File

@@ -69,7 +69,7 @@ public class VersionSubcommand extends FloodgateSubCommand {
COLOR_CHAR + "7You're currently on " + COLOR_CHAR + "b%s" +
COLOR_CHAR + "7 (branch: " + COLOR_CHAR + "b%s" + COLOR_CHAR + "7)\n" +
COLOR_CHAR + "eFetching latest build info...",
Constants.VERSION, Constants.GIT_BRANCH
Constants.FULL_VERSION, Constants.GIT_BRANCH
));
String baseUrl = String.format(

View File

@@ -0,0 +1,110 @@
/*
* Copyright (c) 2019-2023 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.core.config;
import io.micronaut.context.annotation.BootstrapContextCompatible;
import io.micronaut.context.env.BootstrapPropertySourceLocator;
import io.micronaut.context.env.Environment;
import io.micronaut.context.env.PropertySource;
import io.micronaut.context.exceptions.ConfigurationException;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.inject.Singleton;
import java.lang.reflect.Proxy;
import java.nio.file.Path;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.geysermc.configutils.node.codec.strategy.object.ProxyEmbodimentStrategy;
import org.geysermc.floodgate.core.database.loader.DatabaseType;
import org.geysermc.floodgate.core.util.GlobalBeanCache;
import org.geysermc.floodgate.isolation.library.LibraryManager;
@Singleton
@BootstrapContextCompatible
public class ConfigAsProperties implements BootstrapPropertySourceLocator {
@Inject
@Named("dataDirectory")
Path dataDirectory;
@Inject FloodgateConfig config;
public ConfigAsProperties() {
System.out.println("test1");
}
@Override
public Iterable<PropertySource> findPropertySources(Environment environment)
throws ConfigurationException {
loadDatabase();
var flat = flattern(Map.of("config", config));
flat.forEach((key, value) -> System.out.println(key + ": " + value));
return Collections.singleton(PropertySource.of(flat));
}
private void loadDatabase() {
LibraryManager manager = GlobalBeanCache.get("libraryManager");
var databaseConfig = config.database();
if (!databaseConfig.enabled()) {
return;
}
var type = DatabaseType.byId(databaseConfig.type());
if (type == null) {
throw new IllegalStateException(
"Unable to find database type that matches: " + databaseConfig.type()
);
}
type.libraries().forEach(manager::addLibrary);
manager.apply();
}
private Map<String, Object> flattern(Map<String, Object> map) {
return flattern0(Map.of("", map).get(""));
}
@SuppressWarnings("unchecked")
private Map<String, Object> flattern0(Map<String, Object> map) {
var result = new HashMap<String, Object>();
map.forEach((key, value) -> {
if (Proxy.isProxyClass(value.getClass())) {
value = new ProxyEmbodimentStrategy().disembody(value);
}
if (value instanceof Map<?,?>) {
flattern0((Map<String, Object>) value).forEach(
(key1, value1) -> result.put(key + "." + key1, value1)
);
return;
}
result.put(key, value);
});
return result;
}
}

View File

@@ -25,8 +25,8 @@
package org.geysermc.floodgate.core.config;
import io.micronaut.context.ApplicationContext;
import io.micronaut.context.annotation.Bean;
import io.micronaut.context.annotation.BootstrapContextCompatible;
import io.micronaut.context.annotation.Factory;
import jakarta.inject.Inject;
import jakarta.inject.Named;
@@ -39,49 +39,43 @@ import lombok.Getter;
import org.geysermc.configutils.ConfigUtilities;
import org.geysermc.configutils.file.codec.PathFileCodec;
import org.geysermc.configutils.updater.change.Changes;
import org.geysermc.floodgate.core.event.lifecycle.ConfigLoadedEvent;
import org.geysermc.floodgate.core.scope.ProxyOnly;
import org.geysermc.floodgate.core.scope.ServerOnly;
import org.geysermc.floodgate.core.util.LanguageManager;
import org.geysermc.floodgate.core.util.GlobalBeanCache;
import org.geysermc.floodgate.crypto.FloodgateCipher;
import org.geysermc.floodgate.crypto.KeyProducer;
@Factory
@Getter
@BootstrapContextCompatible
public final class ConfigLoader {
private final Path dataDirectory;
private final KeyProducer keyProducer;
private final FloodgateCipher cipher;
private final LanguageManager languageManager;
private final ApplicationContext context;
@Inject
ConfigLoader(
@Named("dataDirectory") Path dataDirectory,
KeyProducer keyProducer,
FloodgateCipher cipher,
LanguageManager languageManager,
ApplicationContext context
FloodgateCipher cipher
) {
this.dataDirectory = dataDirectory;
this.keyProducer = keyProducer;
this.cipher = cipher;
this.languageManager = languageManager;
this.context = context;
}
@Bean
@ServerOnly
@Singleton
FloodgateConfig config() {
return load(FloodgateConfig.class);
return GlobalBeanCache.cacheIfAbsent("config", () -> load(FloodgateConfig.class));
}
@Bean
@ProxyOnly
@Singleton
ProxyFloodgateConfig proxyConfig() {
return load(ProxyFloodgateConfig.class);
return GlobalBeanCache.cacheIfAbsent("config", () -> load(ProxyFloodgateConfig.class));
}
@SuppressWarnings("unchecked")
@@ -99,25 +93,20 @@ public final class ConfigLoader {
.keyRenamed("playerLink.allowLinking", "playerLink.allowed"))
.version(2, Changes.versionBuilder()
.keyRenamed("playerLink.useGlobalLinking", "playerLink.enableGlobalLinking"))
.version(3, Changes.versionBuilder()
.keyRenamed("playerLink.type", "database.type"))
.build())
.definePlaceholder("metrics.uuid", UUID::randomUUID)
.postInitializeCallbackArgument(this)
.commentTranslator((key) -> languageManager.getLogString("floodgate.config." + key))
.build();
T config;
try {
config = (T) utilities.executeOn(configClass);
return (T) utilities.executeOn(configClass);
} catch (Throwable throwable) {
throw new RuntimeException(
"Failed to load the config! Try to delete the config file if this error persists",
throwable
);
}
context.getEventPublisher(ConfigLoadedEvent.class)
.publishEvent(new ConfigLoadedEvent(config));
return config;
}
public void generateKey(Path keyPath) {

View File

@@ -75,10 +75,6 @@ public interface FloodgateConfig extends GenericPostInitializeCallback<ConfigLoa
usernamePrefix(".");
}
// this happens before the config is serialized,
// so the messages in the config will be translated
loader.getLanguageManager().loadConfiguredDefaultLocale(this);
return CallbackResult.ok();
}
@@ -102,6 +98,8 @@ public interface FloodgateConfig extends GenericPostInitializeCallback<ConfigLoa
DisconnectMessages disconnect();
DatabaseConfig database();
@Comment
PlayerLinkConfig playerLink();
@@ -130,6 +128,17 @@ public interface FloodgateConfig extends GenericPostInitializeCallback<ConfigLoa
String invalidArgumentsLength();
}
@ConfigSection
interface DatabaseConfig {
@Comment
@DefaultBoolean(true)
boolean enabled();
@Comment
@DefaultString("h2")
String type();
}
@ConfigSection
interface PlayerLinkConfig {
@Comment
@@ -152,10 +161,6 @@ public interface FloodgateConfig extends GenericPostInitializeCallback<ConfigLoa
@DefaultNumeric(300)
long linkCodeTimeout();
@Comment
@DefaultString("sqlite")
String type();
@Comment
@DefaultBoolean(true)
boolean enableGlobalLinking();

View File

@@ -31,14 +31,12 @@ public interface Properties {
public static PropertySource defaults() {
return PropertySource.of(
"floodgate-properties",
"micronaut.data.mongodb.create-collections", true,
"mongodb.uri", "mongodb://root:root@localhost:27017/test?authSource=admin",
"mongodb.uuid-representation", "standard",
"datasources.default.url", "jdbc:h2:./test",
"datasources.default.username", "sa",
"datasources.default.password", "",
"datasources.default.schema-generate", "create",
"datasources.default.dialect", "H2"
"datasources.default.driverClassName", "org.h2.Driver",
"jpa.default.properties.hibernate.hbm2ddl.auto", "update",
"jpa.default.properties.hibernate.show_sql", "true"
);
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (c) 2019-2023 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.core.database;
import io.micronaut.context.annotation.Requires;
import io.micronaut.data.annotation.Repository;
import io.micronaut.data.repository.async.AsyncCrudRepository;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import org.geysermc.floodgate.core.database.entity.LinkRequest;
@Repository
@Requires(property = "config.database.enabled", value = "true")
public interface PendingLinkRepository extends AsyncCrudRepository<LinkRequest, UUID> {
CompletableFuture<LinkRequest> findByJavaUsername(String javaUsername);
}

View File

@@ -25,18 +25,31 @@
package org.geysermc.floodgate.core.database;
import io.micronaut.context.annotation.Requires;
import io.micronaut.data.annotation.Repository;
import io.micronaut.data.repository.CrudRepository;
import io.micronaut.data.repository.async.AsyncCrudRepository;
import java.util.Optional;
import java.util.UUID;
import javax.validation.constraints.NotNull;
import org.geysermc.floodgate.core.database.entity.LinkedPlayer;
@Repository
public interface PlayerLinkRepository extends CrudRepository<LinkedPlayer, UUID> {
@Requires(property = "config.database.enabled", value = "true")
public interface PlayerLinkRepository extends AsyncCrudRepository<LinkedPlayer, UUID> {
Optional<LinkedPlayer> findByBedrockId(@NotNull UUID bedrockId);
Optional<LinkedPlayer> findByJavaUniqueId(@NotNull UUID javaUniqueId);
Optional<LinkedPlayer> findByBedrockIdOrJavaUniqueId(
UUID bedrockId,
UUID javaUniqueId
);
Boolean existsByBedrockId(@NotNull UUID bedrockId);
Boolean existsByJavaUniqueId(@NotNull UUID javaUniqueId);
Boolean existsByBedrockIdOrJavaUniqueId(UUID bedrockId, UUID javaUniqueId);
void deleteByBedrockIdOrJavaUniqueId(UUID bedrockId, UUID javaUniqueId);
}

View File

@@ -1,124 +0,0 @@
/*
* Copyright (c) 2019-2023 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.core.database.config;
import com.google.gson.JsonObject;
import io.micronaut.context.annotation.Requires;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import org.yaml.snakeyaml.Yaml;
@Requires(env = "no")
public class DatabaseConfigLoader {
private Yaml yaml;
@Inject
@Named("dataDirectory")
Path dataDirectory;
@Inject
@Named("database.name")
String name;
@Inject
@Named("database.classloader")
ClassLoader classLoader;
@Inject
@Named("database.init.data")
JsonObject initData;
@Inject
public void init() {
// yaml = new Yaml(new CustomClassLoaderConstructor(classLoader));
// yaml.setBeanAccess(BeanAccess.FIELD);
}
/**
* This will load the config if it already exists or will create the config from the default
* config file if it doesn't exist.
*
* @param configType the class to parse the config into
* @param <T> type that extends the base DatabaseConfig class
* @return the config if successful or null if not. It'll return null if the database didn't
* provide a config or if there is no config present nor default config available or if an error
* occurred while executing this method.
*/
public <T extends DatabaseConfig> T loadAs(Class<T> configType) {
if (!initData.has("config")) {
return null;
}
String configFile = initData.get("config").getAsString();
Path configPath = dataDirectory.resolve(name).resolve(configFile);
// return the existing config
if (Files.exists(configPath)) {
try (BufferedReader reader = Files.newBufferedReader(configPath)) {
return yaml.loadAs(reader, configType);
} catch (IOException exception) {
exception.printStackTrace();
return null;
}
}
// make directories
try {
Files.createDirectories(configPath.getParent());
} catch (IOException exception) {
exception.printStackTrace();
return null;
}
// load default config resource
try (InputStream configStream = classLoader.getResourceAsStream(configFile)) {
if (configStream == null) {
return null;
}
// copy resource and load config
if (!configStream.markSupported()) {
Files.copy(configStream, configPath);
try (InputStream configStream1 = classLoader.getResourceAsStream(configFile)) {
return yaml.loadAs(configStream1, configType);
}
}
configStream.mark(Integer.MAX_VALUE);
Files.copy(configStream, configPath);
configStream.reset();
return yaml.loadAs(configStream, configType);
} catch (IOException exception) {
exception.printStackTrace();
}
return null;
}
}

View File

@@ -0,0 +1,122 @@
/*
* Copyright (c) 2019-2023 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.core.database.entity;
import io.micronaut.core.annotation.AccessorsStyle;
import java.time.Instant;
import java.util.Objects;
import java.util.UUID;
import javax.persistence.Entity;
import javax.persistence.Id;
@Entity
@AccessorsStyle(readPrefixes = "", writePrefixes = "")
public class LinkRequest {
@Id
private UUID javaUniqueId;
private String javaUsername;
private String bedrockUsername;
private String linkCode;
private long requestTime = Instant.now().getEpochSecond();
public UUID javaUniqueId() {
return javaUniqueId;
}
public LinkRequest javaUniqueId(UUID javaUniqueId) {
this.javaUniqueId = javaUniqueId;
return this;
}
public String javaUsername() {
return javaUsername;
}
public LinkRequest javaUsername(String javaUsername) {
this.javaUsername = javaUsername;
return this;
}
public String bedrockUsername() {
return bedrockUsername;
}
public LinkRequest bedrockUsername(String bedrockUsername) {
this.bedrockUsername = bedrockUsername;
return this;
}
public String linkCode() {
return linkCode;
}
public LinkRequest linkCode(String linkCode) {
this.linkCode = linkCode;
return this;
}
public long requestTime() {
return requestTime;
}
public LinkRequest requestTime(long requestTime) {
this.requestTime = requestTime;
return this;
}
public boolean isExpired(long linkTimeout) {
long timePassed = Instant.now().getEpochSecond() - requestTime;
return timePassed > linkTimeout;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
LinkRequest that = (LinkRequest) o;
return Objects.equals(javaUniqueId, that.javaUniqueId);
}
@Override
public int hashCode() {
return Objects.hash(javaUniqueId);
}
@Override
public String toString() {
return "LinkRequest{" +
"javaUniqueId=" + javaUniqueId +
", javaUsername='" + javaUsername + '\'' +
", bedrockUsername='" + bedrockUsername + '\'' +
", linkCode='" + linkCode + '\'' +
", requestTime=" + requestTime +
'}';
}
}

View File

@@ -25,19 +25,21 @@
package org.geysermc.floodgate.core.database.entity;
import io.micronaut.core.annotation.AccessorsStyle;
import java.util.Objects;
import java.util.UUID;
import javax.persistence.Entity;
import javax.persistence.Id;
@Entity
@AccessorsStyle(readPrefixes = "", writePrefixes = "")
public class LinkedPlayer {
@Id
private UUID bedrockId;
private UUID javaUniqueId;
private String javaUsername;
public UUID getBedrockId() {
public UUID bedrockId() {
return bedrockId;
}
@@ -46,7 +48,7 @@ public class LinkedPlayer {
return this;
}
public UUID getJavaUniqueId() {
public UUID javaUniqueId() {
return javaUniqueId;
}
@@ -55,7 +57,7 @@ public class LinkedPlayer {
return this;
}
public String getJavaUsername() {
public String javaUsername() {
return javaUsername;
}
@@ -74,13 +76,12 @@ public class LinkedPlayer {
}
var that = (LinkedPlayer) obj;
return Objects.equals(this.bedrockId, that.bedrockId) &&
Objects.equals(this.javaUniqueId, that.javaUniqueId) &&
Objects.equals(this.javaUsername, that.javaUsername);
Objects.equals(this.javaUniqueId, that.javaUniqueId);
}
@Override
public int hashCode() {
return Objects.hash(bedrockId, javaUniqueId, javaUsername);
return Objects.hash(bedrockId, javaUniqueId);
}
@Override

View File

@@ -0,0 +1,95 @@
/*
* Copyright (c) 2019-2023 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.core.database.loader;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.floodgate.core.util.Constants;
import org.geysermc.floodgate.isolation.library.Library;
import org.geysermc.floodgate.isolation.library.Repository;
public enum DatabaseType {
H2(
DriverCategory.HIBERNATE,
databaseModule(),
library("h2", Repository.MAVEN_CENTRAL, "com.h2database", "h2", "1.4.200")
);
private static final DatabaseType[] VALUES = values();
private final DriverCategory category;
private final Set<Library> libraries;
DatabaseType(@NonNull DriverCategory category, @NonNull Library... libraries) {
this.category = Objects.requireNonNull(category);
this.libraries = Set.of(Objects.requireNonNull(libraries));
}
static Library databaseModule() {
return library(
"database",
Repository.OPEN_COLLAB,
"org.geysermc.floodgate",
"database",
Constants.VERSION
);
}
static Library library(
String id,
Repository repo,
String groupId,
String artifactId,
String version
) {
return Library.builder()
.id(id)
.repository(repo)
.groupId(groupId)
.artifactId(artifactId)
.version(version)
.build();
}
public static DatabaseType byId(String id) {
id = id.toUpperCase(Locale.ROOT);
for (DatabaseType value : VALUES) {
if (value.name().equals(id)) {
return value;
}
}
return null;
}
public DriverCategory category() {
return category;
}
public Set<Library> libraries() {
return libraries;
}
}

View File

@@ -23,10 +23,8 @@
* @link https://github.com/GeyserMC/Floodgate
*/
package org.geysermc.floodgate.core.database.config;
package org.geysermc.floodgate.core.database.loader;
/**
* Base class for every database related configuration.
*/
public interface DatabaseConfig {
public enum DriverCategory {
HIBERNATE
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright (c) 2019-2023 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.core.http.link;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.UUID;
import org.geysermc.floodgate.core.util.Utils;
public record LinkedPlayer(
@JsonProperty("bedrock_id")
long xuid,
String gamertag,
@JsonProperty("java_id")
UUID uuid,
@JsonProperty("java_name")
String username
) {
public boolean isLinked() {
// everything will be null when the player is not linked, since we return an empty object.
// but it's sufficient to check if one of them is null
return uuid != null;
}
public org.geysermc.floodgate.core.database.entity.LinkedPlayer toDatabase() {
if (!isLinked()) {
return null;
}
return new org.geysermc.floodgate.core.database.entity.LinkedPlayer()
.bedrockId(Utils.getJavaUuid(xuid))
.javaUniqueId(uuid)
.javaUsername(username);
}
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright (c) 2019-2023 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.core.http.xbox;
public record GetGamertagResult(String gamertag) {
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright (c) 2019-2023 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.core.http.xbox;
public record GetXuidResult(Long xuid) {
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright (c) 2019-2023 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.core.http.xbox;
import static io.micronaut.http.HttpHeaders.ACCEPT;
import static io.micronaut.http.HttpHeaders.USER_AGENT;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Header;
import io.micronaut.http.client.annotation.Client;
import java.util.concurrent.CompletableFuture;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
@Client("${http.baseUrl}/v2/xbox")
@Header(name = USER_AGENT, value = "${http.userAgent}")
@Header(name = ACCEPT, value = "application/json")
public interface XboxClient {
@Get("/xuid/{gamertag}")
CompletableFuture<GetXuidResult> xuidByGamertag(
@NotNull @Size(min = 1, max = 16) String gamertag
);
@Get("/gamertag/{xuid}")
CompletableFuture<GetGamertagResult> gamertagByXuid(long xuid);
}

View File

@@ -25,30 +25,20 @@
package org.geysermc.floodgate.core.link;
import io.micronaut.context.ApplicationContext;
import io.micronaut.context.annotation.Requires;
import jakarta.annotation.PreDestroy;
import jakarta.inject.Inject;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.CompletableFuture;
import lombok.AccessLevel;
import lombok.Getter;
import org.checkerframework.checker.nullness.qual.NonNull;
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.api.player.FloodgatePlayer;
import org.geysermc.floodgate.core.config.FloodgateConfig;
import org.geysermc.floodgate.core.database.config.DatabaseConfig;
import org.geysermc.floodgate.core.database.config.DatabaseConfigLoader;
//@Listener
@Requires(env = "no")
public abstract class CommonPlayerLink implements PlayerLink {
@Getter(AccessLevel.PROTECTED)
private final ExecutorService executorService = Executors.newCachedThreadPool();
import org.geysermc.floodgate.core.database.entity.LinkRequest;
import org.geysermc.floodgate.core.database.entity.LinkedPlayer;
public abstract class CommonPlayerLink {
@Getter private boolean enabled;
@Getter private boolean allowLinking;
@Getter private long verifyLinkTimeout;
@@ -61,9 +51,6 @@ public abstract class CommonPlayerLink implements PlayerLink {
@Getter(AccessLevel.PROTECTED)
FloodgateApi api;
@Inject
ApplicationContext context;
@Inject
void init(FloodgateConfig config) {
FloodgateConfig.PlayerLinkConfig linkConfig = config.playerLink();
@@ -72,37 +59,45 @@ public abstract class CommonPlayerLink implements PlayerLink {
verifyLinkTimeout = linkConfig.linkCodeTimeout();
}
public String createCode() {
return String.format("%04d", new Random().nextInt(10000));
}
public boolean isRequestedPlayer(LinkRequest request, UUID bedrockId) {
return request.isRequestedPlayer(api.getPlayer(bedrockId));
}
/**
* Get the config present in init.json and turn it into the given config class. This method will
* automatically copy and save the default config if the config doesn't exist.
* Checks if the given FloodgatePlayer is the player requested in this LinkRequest. This method
* will check both the real bedrock username {@link FloodgatePlayer#getUsername()} and the
* edited username {@link FloodgatePlayer#getJavaUsername()} and returns true if one of the two
* matches.
*
* @param configClass the class to convert the config to
* @param <T> the config type
* @return the loaded config or null if something went wrong.
* @see DatabaseConfigLoader#loadAs(Class)
* @return true if the given player is the player requested
*/
public <T extends DatabaseConfig> T getConfig(Class<T> configClass) {
// this method is not intended to be used more than once. It'll make a new instance of
// DatabaseConfigLoader and DatabaseConfig every time you run this method.
return context.getBean(DatabaseConfigLoader.class).loadAs(configClass);
public boolean isRequestedPlayer(LinkRequest request, UUID bedrockId) {
// Java starts the process, Bedrock finishes it. So player can't be null
var player = api.getPlayer(bedrockId);
return request.bedrockUsername().equals(player.getUsername()) ||
request.bedrockUsername().equals(player.getJavaUsername());
}
@Override
public String getName() {
return context.get("database.name", String.class).get();
}
public abstract CompletableFuture<LinkedPlayer> addLink(
@NonNull UUID javaUniqueId,
@NonNull String javaUsername,
@NonNull UUID bedrockId
);
@Override
@PreDestroy
public void stop() {
executorService.shutdown();
public abstract CompletableFuture<LinkedPlayer> fetchLink(@NonNull UUID uuid);
public abstract CompletableFuture<Boolean> isLinked(@NonNull UUID uuid);
public abstract CompletableFuture<Void> unlink(@NonNull UUID uuid);
public abstract CompletableFuture<LinkRequest> createLinkRequest(
@NonNull UUID javaUniqueId,
@NonNull String javaUsername,
@NonNull String bedrockUsername,
@NonNull String code
);
public abstract CompletableFuture<LinkRequest> linkRequest(@NonNull String javaUsername);
public abstract CompletableFuture<Void> invalidateLinkRequest(@NonNull LinkRequest request);
public boolean isActive() {
return enabled && allowLinking;
}
}

View File

@@ -25,76 +25,18 @@
package org.geysermc.floodgate.core.link;
import jakarta.inject.Singleton;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.floodgate.api.FloodgateApi;
import org.geysermc.floodgate.api.link.LinkRequestResult;
import org.geysermc.floodgate.api.link.PlayerLink;
import org.geysermc.floodgate.core.util.Utils;
import org.geysermc.floodgate.util.LinkedPlayer;
import org.geysermc.floodgate.core.database.entity.LinkRequest;
import org.geysermc.floodgate.core.database.entity.LinkedPlayer;
/**
* Simple class used when PlayerLinking is disabled. This class has been made because Floodgate
* doesn't have a default PlayerLink implementation anymore and {@link FloodgateApi#getPlayerLink()}
* returning null} is also not an option.
* Simple class used when PlayerLinking is disabled
*/
final class DisabledPlayerLink implements PlayerLink {
@Override
public void load() {
}
@Override
@NonNull
public CompletableFuture<LinkedPlayer> getLinkedPlayer(@NonNull UUID bedrockId) {
return failedFuture();
}
@Override
@NonNull
public CompletableFuture<Boolean> isLinkedPlayer(@NonNull UUID bedrockId) {
return failedFuture();
}
@Override
@NonNull
public CompletableFuture<Void> linkPlayer(
@NonNull UUID bedrockId,
@NonNull UUID javaId,
@NonNull String username) {
return failedFuture();
}
@Override
@NonNull
public CompletableFuture<Void> unlinkPlayer(@NonNull UUID javaId) {
return failedFuture();
}
@Override
@NonNull
public CompletableFuture<?> createLinkRequest(
@NonNull UUID javaId,
@NonNull String javaUsername,
@NonNull String bedrockUsername) {
return failedFuture();
}
@Override
@NonNull
public CompletableFuture<LinkRequestResult> verifyLinkRequest(
@NonNull UUID bedrockId,
@NonNull String javaUsername,
@NonNull String bedrockUsername,
@NonNull String code) {
return failedFuture();
}
@Override
public String getName() {
return null;
}
@Singleton
final class DisabledPlayerLink extends CommonPlayerLink {
@Override
public boolean isEnabled() {
return false;
@@ -111,11 +53,52 @@ final class DisabledPlayerLink implements PlayerLink {
}
@Override
public void stop() {
public CompletableFuture<LinkedPlayer> addLink(
@NonNull UUID javaUniqueId,
@NonNull String javaUsername,
@NonNull UUID bedrockId
) {
return failedFuture();
}
@Override
public CompletableFuture<LinkedPlayer> fetchLink(@NonNull UUID uuid) {
return failedFuture();
}
@Override
public CompletableFuture<Boolean> isLinked(@NonNull UUID uuid) {
return failedFuture();
}
@Override
public CompletableFuture<Void> unlink(@NonNull UUID uuid) {
return failedFuture();
}
@Override
public CompletableFuture<LinkRequest> createLinkRequest(
@NonNull UUID javaUniqueId,
@NonNull String javaUsername,
@NonNull String bedrockUsername,
@NonNull String code
) {
return failedFuture();
}
@Override
public CompletableFuture<LinkRequest> linkRequest(@NonNull String javaUsername) {
return failedFuture();
}
@Override
public CompletableFuture<Void> invalidateLinkRequest(@NonNull LinkRequest request) {
return failedFuture();
}
private <U> CompletableFuture<U> failedFuture() {
return Utils.failedFuture(new IllegalStateException(
"Cannot perform this action when PlayerLinking is disabled"));
return CompletableFuture.failedFuture(new IllegalStateException(
"Cannot perform this action when PlayerLinking is disabled"
));
}
}

View File

@@ -25,66 +25,43 @@
package org.geysermc.floodgate.core.link;
import static org.geysermc.floodgate.core.util.Constants.GET_BEDROCK_LINK;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import io.micronaut.context.BeanProvider;
import io.micronaut.context.annotation.Primary;
import io.micronaut.context.annotation.Requires;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.inject.Singleton;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import lombok.Getter;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.floodgate.api.link.LinkRequestResult;
import org.geysermc.floodgate.api.link.PlayerLink;
import org.geysermc.floodgate.core.util.HttpClient;
import org.geysermc.floodgate.core.util.HttpClient.DefaultHttpResponse;
import org.geysermc.floodgate.core.util.Utils;
import org.geysermc.floodgate.util.LinkedPlayer;
import org.geysermc.floodgate.core.database.entity.LinkRequest;
import org.geysermc.floodgate.core.database.entity.LinkedPlayer;
import org.geysermc.floodgate.core.http.link.GlobalLinkClient;
@Requires(property = "config.link.enableGlobalLinking", value = "true")
@Primary
@Singleton
@Getter
public class GlobalPlayerLinking extends CommonPlayerLink {
@Inject HttpClient httpClient;
@Inject GlobalLinkClient linkClient;
private PlayerLink databaseImpl;
private CommonPlayerLink database;
public void setDatabaseImpl(PlayerLink databaseImpl) {
if (this.databaseImpl == null) {
this.databaseImpl = databaseImpl;
}
}
@Override
public void load() {
if (databaseImpl != null) {
databaseImpl.load();
}
}
@Override
public String getName() {
if (databaseImpl != null) {
return databaseImpl.getName();
}
// Global Linking is integrated
return null;
}
@Override
public void stop() {
super.stop();
if (databaseImpl != null) {
databaseImpl.stop();
}
@Inject
void init(@Named("localLinking") BeanProvider<CommonPlayerLink> databaseImpl) {
this.database = databaseImpl.orElse(null);
}
@Override
@NonNull
public CompletableFuture<LinkedPlayer> getLinkedPlayer(@NonNull UUID bedrockId) {
if (databaseImpl == null) {
public CompletableFuture<LinkedPlayer> fetchLink(@NonNull UUID bedrockId) {
if (database == null) {
return getLinkedPlayer0(bedrockId);
}
return databaseImpl.getLinkedPlayer(bedrockId).thenComposeAsync(result -> {
return database.fetchLink(bedrockId).thenComposeAsync(result -> {
if (result != null) {
return CompletableFuture.completedFuture(result);
}
@@ -94,47 +71,18 @@ public class GlobalPlayerLinking extends CommonPlayerLink {
@NonNull
private CompletableFuture<LinkedPlayer> getLinkedPlayer0(@NonNull UUID bedrockId) {
return CompletableFuture.supplyAsync(
() -> {
DefaultHttpResponse response =
httpClient.get(GET_BEDROCK_LINK + bedrockId.getLeastSignificantBits());
// either the global api is down or it failed to return link
if (!response.isCodeOk()) {
if (response.getResponse() != null) {
getLogger().error(
"Failed to request link for {}: {}",
bedrockId.getLeastSignificantBits(),
response.getResponse().get("message").getAsString()
);
}
return null;
}
JsonObject data = response.getResponse();
JsonElement javaName = data.get("java_name");
// javaName will be null when the player isn't linked
if (javaName == null) {
return null;
}
return LinkedPlayer.of(
javaName.getAsString(),
UUID.fromString(data.get("java_id").getAsString()),
Utils.getJavaUuid(data.get("bedrock_id").getAsLong()));
},
getExecutorService());
return linkClient.bedrockLink(bedrockId.getLeastSignificantBits())
.thenApply(org.geysermc.floodgate.core.http.link.LinkedPlayer::toDatabase);
}
@Override
@NonNull
public CompletableFuture<Boolean> isLinkedPlayer(@NonNull UUID bedrockId) {
if (databaseImpl == null) {
public CompletableFuture<Boolean> isLinked(@NonNull UUID bedrockId) {
if (database == null) {
return isLinkedPlayer0(bedrockId);
}
return databaseImpl.isLinkedPlayer(bedrockId).thenComposeAsync(result -> {
return database.isLinked(bedrockId).thenComposeAsync(result -> {
if (result != null) {
return CompletableFuture.completedFuture(result);
}
@@ -144,23 +92,7 @@ public class GlobalPlayerLinking extends CommonPlayerLink {
@NonNull
private CompletableFuture<Boolean> isLinkedPlayer0(@NonNull UUID bedrockId) {
return CompletableFuture.supplyAsync(
() -> {
DefaultHttpResponse response =
httpClient.get(GET_BEDROCK_LINK + bedrockId.getLeastSignificantBits());
if (!response.isCodeOk()) {
getLogger().error(
"Failed to request link for {}: {}",
bedrockId.getLeastSignificantBits(),
response.getResponse().get("message").getAsString());
return false;
}
// no link if data is empty, otherwise the player is linked
return response.getResponse().entrySet().size() != 0;
},
getExecutorService());
return getLinkedPlayer0(bedrockId).thenApply(Objects::nonNull);
}
// player linking and unlinking now goes through the global player linking server.
@@ -168,57 +100,64 @@ public class GlobalPlayerLinking extends CommonPlayerLink {
@Override
@NonNull
public CompletableFuture<Void> linkPlayer(
@NonNull UUID bedrockId,
@NonNull UUID javaId,
@NonNull String username) {
if (databaseImpl != null) {
return databaseImpl.linkPlayer(bedrockId, javaId, username);
}
return failedFuture();
}
@Override
@NonNull
public CompletableFuture<Void> unlinkPlayer(@NonNull UUID javaId) {
if (databaseImpl != null) {
return databaseImpl.unlinkPlayer(javaId);
}
return failedFuture();
}
@Override
@NonNull
public CompletableFuture<?> createLinkRequest(
@NonNull UUID javaId,
public CompletableFuture<LinkedPlayer> addLink(
@NonNull UUID javaUniqueId,
@NonNull String javaUsername,
@NonNull String bedrockUsername) {
if (databaseImpl != null) {
return databaseImpl.createLinkRequest(javaId, javaUsername, bedrockUsername);
@NonNull UUID bedrockId
) {
if (database != null) {
return database.addLink(javaUniqueId, javaUsername, bedrockId);
}
return failedFuture();
}
@Override
@NonNull
public CompletableFuture<LinkRequestResult> verifyLinkRequest(
@NonNull UUID bedrockId,
public CompletableFuture<Void> unlink(@NonNull UUID javaUniqueId) {
if (database != null) {
return database.unlink(javaUniqueId);
}
return failedFuture();
}
@Override
@NonNull
public CompletableFuture<LinkRequest> createLinkRequest(
@NonNull UUID javaUniqueId,
@NonNull String javaUsername,
@NonNull String bedrockUsername,
@NonNull String code) {
if (databaseImpl != null) {
return databaseImpl.verifyLinkRequest(bedrockId, javaUsername, bedrockUsername, code);
@NonNull String code
) {
if (database != null) {
return database.createLinkRequest(javaUniqueId, javaUsername, bedrockUsername, code);
}
return failedFuture();
}
@Override
public boolean isEnabledAndAllowed() {
return databaseImpl != null && databaseImpl.isEnabledAndAllowed();
public CompletableFuture<LinkRequest> linkRequest(@NonNull String javaUsername) {
if (database != null) {
return database.linkRequest(javaUsername);
}
return failedFuture();
}
@Override
public CompletableFuture<Void> invalidateLinkRequest(@NonNull LinkRequest request) {
if (database != null) {
return database.invalidateLinkRequest(request);
}
return failedFuture();
}
@Override
public boolean isActive() {
return database != null && database.isActive();
}
private <U> CompletableFuture<U> failedFuture() {
return Utils.failedFuture(new IllegalStateException(
"Cannot perform this action when Global Linking is enabled"));
return CompletableFuture.failedFuture(new IllegalStateException(
"Cannot perform this action when Global Linking is enabled"
));
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright (c) 2019-2023 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.core.link;
import org.geysermc.floodgate.core.command.LinkAccountCommand.Message;
public class LinkVerificationException extends RuntimeException {
public static final LinkVerificationException NO_LINK_REQUESTED =
new LinkVerificationException(Message.NO_LINK_REQUESTED);
public static final LinkVerificationException INVALID_CODE =
new LinkVerificationException(Message.INVALID_CODE);
public static final LinkVerificationException LINK_REQUEST_EXPIRED =
new LinkVerificationException(Message.LINK_REQUEST_EXPIRED);
private final Message message;
private LinkVerificationException(Message message) {
super(null, null, true, false);
this.message = message;
}
public Message message() {
return message;
}
}

View File

@@ -0,0 +1,110 @@
/*
* Copyright (c) 2019-2023 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.core.link;
import io.micronaut.context.annotation.Replaces;
import io.micronaut.context.annotation.Requires;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.inject.Singleton;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.floodgate.core.database.PendingLinkRepository;
import org.geysermc.floodgate.core.database.PlayerLinkRepository;
import org.geysermc.floodgate.core.database.entity.LinkRequest;
import org.geysermc.floodgate.core.database.entity.LinkedPlayer;
@Requires(property = "config.playerLink.enableOwnLinking", value = "true")
@Replaces(DisabledPlayerLink.class)
@Named("localLinking")
@Singleton
public class PlayerLinkHibernate extends CommonPlayerLink {
@Inject PlayerLinkRepository linkRepository;
@Inject PendingLinkRepository pendingLinkRepository;
@Override
public CompletableFuture<LinkedPlayer> addLink(
@NonNull UUID javaUniqueId,
@NonNull String javaUsername,
@NonNull UUID bedrockId
) {
return linkRepository.save(
new LinkedPlayer()
.javaUniqueId(javaUniqueId)
.javaUsername(javaUsername)
.bedrockId(bedrockId)
);
}
@Override
public CompletableFuture<LinkedPlayer> fetchLink(@NonNull UUID uuid) {
return CompletableFuture.supplyAsync(() ->
linkRepository.findByBedrockIdOrJavaUniqueId(uuid, uuid).orElse(null)
);
}
@Override
public CompletableFuture<Boolean> isLinked(@NonNull UUID uuid) {
return CompletableFuture.supplyAsync(() ->
linkRepository.existsByBedrockIdOrJavaUniqueId(uuid, uuid)
);
}
@Override
public CompletableFuture<Void> unlink(@NonNull UUID uuid) {
return CompletableFuture.supplyAsync(() -> {
linkRepository.deleteByBedrockIdOrJavaUniqueId(uuid, uuid);
return null;
});
}
@Override
public CompletableFuture<LinkRequest> createLinkRequest(
@NonNull UUID javaUniqueId,
@NonNull String javaUsername,
@NonNull String bedrockUsername,
@NonNull String code
) {
return pendingLinkRepository.save(
new LinkRequest()
.javaUniqueId(javaUniqueId)
.javaUsername(javaUsername)
.bedrockUsername(bedrockUsername)
.linkCode(code)
);
}
@Override
public CompletableFuture<LinkRequest> linkRequest(@NonNull String javaUsername) {
return pendingLinkRepository.findByJavaUsername(javaUsername);
}
@Override
public CompletableFuture<Void> invalidateLinkRequest(@NonNull LinkRequest request) {
return pendingLinkRepository.delete(request);
}
}

View File

@@ -1,229 +0,0 @@
/*
* Copyright (c) 2019-2023 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.core.link;
import static java.util.Objects.requireNonNull;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import io.micronaut.context.ApplicationContext;
import io.micronaut.context.annotation.Bean;
import io.micronaut.context.annotation.Factory;
import jakarta.annotation.PreDestroy;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.inject.Singleton;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.geysermc.event.Listener;
import org.geysermc.floodgate.api.link.PlayerLink;
import org.geysermc.floodgate.api.logger.FloodgateLogger;
import org.geysermc.floodgate.core.config.FloodgateConfig;
import org.geysermc.floodgate.core.config.FloodgateConfig.PlayerLinkConfig;
import org.geysermc.floodgate.core.util.Constants;
import org.geysermc.floodgate.core.util.Utils;
@Listener
@Factory
@SuppressWarnings("unchecked")
public final class PlayerLinkHolder {
private final ApplicationContext currentContext;
private URLClassLoader classLoader;
private ApplicationContext childContext;
private PlayerLink instance;
@Inject
PlayerLinkHolder(ApplicationContext context) {
this.currentContext = context;
}
@Bean
@Singleton
PlayerLink load(
FloodgateConfig config,
FloodgateLogger logger,
@Named("dataDirectory") Path dataDirectory
) {
if (instance != null) {
return instance;
}
if (config == null) {
throw new IllegalStateException("Config cannot be null!");
}
PlayerLinkConfig linkConfig = config.playerLink();
if (!linkConfig.enabled()) {
return new DisabledPlayerLink();
}
List<Path> files;
try (Stream<Path> list = Files.list(dataDirectory)) {
files = list
.filter(path -> Files.isRegularFile(path) && path.toString().endsWith(".jar"))
.collect(Collectors.toList());
} catch (IOException exception) {
logger.error("Failed to list possible database implementations", exception);
return new DisabledPlayerLink();
}
// we can skip the rest if global linking is enabled and no database implementations has
// been found, or when global linking is enabled and own player linking is disabled.
if (linkConfig.enableGlobalLinking() &&
(files.isEmpty() || !linkConfig.enableOwnLinking())) {
return currentContext.getBean(GlobalPlayerLinking.class);
}
if (files.isEmpty()) {
logger.error("Failed to find a database implementation");
return new DisabledPlayerLink();
}
Path implementationPath = files.get(0);
final String databaseName;
// We only want to load one database implementation
if (files.size() > 1) {
boolean found = false;
databaseName = linkConfig.type();
String expectedName = "floodgate-" + databaseName + "-database.jar";
for (Path path : files) {
if (expectedName.equalsIgnoreCase(path.getFileName().toString())) {
implementationPath = path;
found = true;
}
}
if (!found) {
logger.error(
"Failed to find an implementation for type: {}", linkConfig.type()
);
return new DisabledPlayerLink();
}
} else {
String name = implementationPath.getFileName().toString();
if (!Utils.isValidDatabaseName(name)) {
logger.error(
"Found database {} but the name doesn't match {}",
name, Constants.DATABASE_NAME_FORMAT
);
return new DisabledPlayerLink();
}
int firstSplit = name.indexOf('-') + 1;
databaseName = name.substring(firstSplit, name.indexOf('-', firstSplit));
}
boolean init = true;
try {
URL pluginUrl = implementationPath.toUri().toURL();
// we don't have a way to close this properly since we have no stop method and we have
// to be able to load classes on the fly, but that doesn't matter anyway since Floodgate
// doesn't support reloading
classLoader = new URLClassLoader(
new URL[]{pluginUrl},
PlayerLinkHolder.class.getClassLoader()
);
String mainClassName;
JsonObject dbInitConfig;
try (InputStream linkConfigStream = classLoader.getResourceAsStream("init.json")) {
requireNonNull(linkConfigStream, "Implementation should have an init file");
dbInitConfig = new Gson().fromJson(
new InputStreamReader(linkConfigStream), JsonObject.class
);
mainClassName = dbInitConfig.get("mainClass").getAsString();
}
Class<? extends PlayerLink> mainClass =
(Class<? extends PlayerLink>) classLoader.loadClass(mainClassName);
init = false;
// childContext = ApplicationContext.builder()
// .singletons()
// .classLoader(classLoader)
// .packages()
// .properties(Map.of(
// "database.name", databaseName,
// "database.classloader", classLoader,
// "database.init.data", dbInitConfig
// ))
// .build();
//
// childContext.registerBeanDefinition(RuntimeBeanDefinition.builder(null).)
//
// childContext.environment(helo -> helo.);
instance = childContext.getBean(mainClass);
// we use our own internal PlayerLinking when global linking is enabled
if (linkConfig.enableGlobalLinking()) {
GlobalPlayerLinking linking = childContext.getBean(GlobalPlayerLinking.class);
linking.setDatabaseImpl(instance);
linking.load();
return linking;
} else {
instance.load();
return instance;
}
} catch (ClassCastException exception) {
logger.error(
"The database implementation ({}) doesn't extend the PlayerLink class!",
implementationPath.getFileName().toString(), exception
);
return new DisabledPlayerLink();
} catch (Exception exception) {
if (init) {
logger.error("Error while initialising database jar", exception);
} else {
logger.error("Error while loading database jar", exception);
}
return new DisabledPlayerLink();
}
}
@PreDestroy
void close() throws Exception {
instance.stop();
childContext.close();
classLoader.close();
}
}

View File

@@ -26,10 +26,13 @@
package org.geysermc.floodgate.core.module;
import io.micronaut.context.annotation.Bean;
import io.micronaut.context.annotation.BootstrapContextCompatible;
import io.micronaut.context.annotation.Factory;
import io.netty.util.AttributeKey;
import jakarta.inject.Named;
import jakarta.inject.Singleton;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
@@ -39,6 +42,7 @@ import java.util.concurrent.TimeUnit;
import org.geysermc.event.Listener;
import org.geysermc.floodgate.api.player.FloodgatePlayer;
import org.geysermc.floodgate.core.util.Constants;
import org.geysermc.floodgate.core.util.GlobalBeanCache;
import org.geysermc.floodgate.crypto.AesCipher;
import org.geysermc.floodgate.crypto.AesKeyProducer;
import org.geysermc.floodgate.crypto.Base64Topping;
@@ -46,6 +50,7 @@ import org.geysermc.floodgate.crypto.FloodgateCipher;
import org.geysermc.floodgate.crypto.KeyProducer;
@Factory
@BootstrapContextCompatible
@Listener
public class CommonModule {
@Bean(preDestroy = "shutdown")
@@ -63,15 +68,26 @@ public class CommonModule {
}
@Bean
@BootstrapContextCompatible
@Singleton
public KeyProducer keyProducer() {
return new AesKeyProducer();
@Named("dataDirectory")
public Path dataDirectory() {
// todo open discussion asking how you can register bootstrap context beans
return Paths.get("plugins", "floodgate");
}
@Bean
@BootstrapContextCompatible
@Singleton
public KeyProducer keyProducer() {
return GlobalBeanCache.cacheIfAbsent("keyProducer", AesKeyProducer::new);
}
@Bean
@BootstrapContextCompatible
@Singleton
public FloodgateCipher cipher() {
return new AesCipher(new Base64Topping());
return GlobalBeanCache.cacheIfAbsent("cipher", () -> new AesCipher(new Base64Topping()));
}
@Bean

View File

@@ -192,7 +192,7 @@ public abstract class CommandUtil {
* @return true if the player has been whitelisted, false if the player was already whitelisted.
* Defaults to false when this platform doesn't support whitelisting.
*/
public boolean whitelistPlayer(String xuid, String username) {
public boolean whitelistPlayer(long xuid, String username) {
UUID uuid = Utils.getJavaUuid(xuid);
return whitelistPlayer(uuid, username);
}
@@ -217,7 +217,7 @@ public abstract class CommandUtil {
* @return true if the player has been removed from the whitelist, false if the player wasn't
* whitelisted. Defaults to false when this platform doesn't support whitelisting.
*/
public boolean removePlayerFromWhitelist(String xuid, String username) {
public boolean removePlayerFromWhitelist(long xuid, String username) {
UUID uuid = Utils.getJavaUuid(xuid);
return removePlayerFromWhitelist(uuid, username);
}

View File

@@ -51,6 +51,7 @@ import org.geysermc.floodgate.core.addon.data.HandshakeDataImpl;
import org.geysermc.floodgate.core.addon.data.HandshakeHandlersImpl;
import org.geysermc.floodgate.core.api.SimpleFloodgateApi;
import org.geysermc.floodgate.core.config.FloodgateConfig;
import org.geysermc.floodgate.core.link.CommonPlayerLink;
import org.geysermc.floodgate.core.skin.SkinUploadManager;
import org.geysermc.floodgate.core.util.Constants;
import org.geysermc.floodgate.core.util.LanguageManager;
@@ -63,6 +64,7 @@ import org.geysermc.floodgate.util.LinkedPlayer;
public final class FloodgateHandshakeHandler {
@Inject HandshakeHandlersImpl handshakeHandlers;
@Inject SimpleFloodgateApi api;
@Inject CommonPlayerLink link;
@Inject FloodgateCipher cipher;
@Inject FloodgateConfig config;
@Inject SkinUploadManager skinUploadManager;
@@ -246,10 +248,16 @@ public final class FloodgateHandshakeHandler {
}
private CompletableFuture<Pair<BedrockData, LinkedPlayer>> fetchLinkedPlayer(BedrockData data) {
if (!api.getPlayerLink().isEnabled()) {
if (!link.isEnabled()) {
return CompletableFuture.completedFuture(new ObjectObjectImmutablePair<>(data, null));
}
return api.getPlayerLink().getLinkedPlayer(Utils.getJavaUuid(data.getXuid()))
return link.fetchLink(Utils.getJavaUuid(data.getXuid()))
.thenApply(link -> {
if (link == null) {
return null;
}
return LinkedPlayer.of(link.javaUsername(), link.javaUniqueId(), link.bedrockId());
})
.thenApply(link -> new ObjectObjectImmutablePair<>(data, link))
.handle((result, error) -> {
if (error != null) {

View File

@@ -0,0 +1,44 @@
/*
* Copyright (c) 2019-2023 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.core.util;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
public class GlobalBeanCache {
private static final Map<String, Object> cache = new HashMap<>();
@SuppressWarnings("unchecked")
public static <T> T cacheIfAbsent(String id, Supplier<T> object) {
return (T) cache.computeIfAbsent(id, $ -> object.get());
}
@SuppressWarnings("unchecked")
public static <T> T get(String id) {
return (T) cache.get(id);
}
}

View File

@@ -26,8 +26,6 @@
package org.geysermc.floodgate.core.util;
import com.google.common.base.Joiner;
import io.micronaut.context.BeanProvider;
import jakarta.annotation.PostConstruct;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import java.net.URL;
@@ -47,7 +45,7 @@ import org.geysermc.floodgate.core.config.FloodgateConfig;
public final class LanguageManager {
private final Map<String, Properties> localeMappings = new HashMap<>();
@Inject BeanProvider<FloodgateLogger> logger;
@Inject FloodgateLogger logger;
/**
* The locale used in console and as a fallback
@@ -72,16 +70,11 @@ public final class LanguageManager {
/**
* Tries to load the log's locale file once a string has been requested
*/
@PostConstruct
public void init() {
@Inject
public void init(FloodgateConfig config) {
if (!loadLocale("en_US")) {
logger.get().error("Failed to load the fallback language. This will likely cause errors!");
logger.error("Failed to load the fallback language. This will likely cause errors!");
}
defaultLocale = "en_US";
}
public void loadConfiguredDefaultLocale(FloodgateConfig config) {
FloodgateLogger logger = this.logger.get();
defaultLocale = formatLocale(config.defaultLocale());
if (!"system".equals(defaultLocale) && isValidLanguage(defaultLocale)) {
@@ -126,7 +119,7 @@ public final class LanguageManager {
return true;
}
logger.get().warn("Missing locale file: " + formatLocale);
logger.warn("Missing locale file: " + formatLocale);
return false;
}
@@ -187,7 +180,7 @@ public final class LanguageManager {
.getResource("/languages/texts/" + locale + ".properties");
if (languageFile == null) {
logger.get().warn(locale + " is not a supported Floodgate language.");
logger.warn(locale + " is not a supported Floodgate language.");
return false;
}
return true;

View File

@@ -99,7 +99,7 @@ public final class Metrics {
);
metricsBase.addCustomChart(
new SimplePie("floodgate_version", () -> Constants.VERSION)
new SimplePie("floodgate_version", () -> Constants.FULL_VERSION)
);
metricsBase.addCustomChart(

View File

@@ -34,15 +34,17 @@ import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Locale;
import java.util.Properties;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.regex.Pattern;
public class Utils {
private static final Pattern NON_UNIQUE_PREFIX = Pattern.compile("^\\w{0,16}$");
private static final Pattern DATABASE_NAME = Pattern.compile(Constants.DATABASE_NAME_FORMAT);
private static final Random random = new SecureRandom();
/**
* This method is used in Addons.<br> Most addons can be removed once the player associated to
@@ -99,6 +101,22 @@ public class Utils {
return DATABASE_NAME.matcher(databaseName).matches();
}
public static String generateCode(int length) {
StringBuilder code = new StringBuilder(length);
for (int i = 0; i < length; i++) {
code.append(generateCodeChar());
}
return code.toString();
}
public static char generateCodeChar() {
var codeChar = random.nextInt() % (10 + 26);
if (codeChar < 10) {
return (char) ('0' + codeChar);
}
return (char) ('A' + codeChar);
}
public static int readVarInt(ByteBuf buffer) {
int out = 0;
int count = 0;
@@ -119,18 +137,4 @@ public class Utils {
throwable.printStackTrace(new PrintWriter(writer));
return writer.toString();
}
/**
* Returns a new CompletableFuture that is already completed exceptionally with the given
* exception.
*
* @param ex the exception
* @param <U> the type of the value
* @return the exceptionally completed CompletableFuture
*/
public static <U> CompletableFuture<U> failedFuture(Throwable ex) {
CompletableFuture<U> future = new CompletableFuture<>();
future.completeExceptionally(ex);
return future;
}
}

View File

@@ -0,0 +1,3 @@
http:
baseUrl: https://api.geysermc.org
userAgent: GeyserMC/Floodgate

View File

@@ -26,7 +26,8 @@
package org.geysermc.floodgate.core.util;
public final class Constants {
public static final String VERSION = "@floodgateVersion@";
public static final String FULL_VERSION = "@fullVersion@";
public static final String VERSION = "@version@";
public static final int BUILD_NUMBER = Integer.parseInt("@buildNumber@");
public static final String GIT_BRANCH = "@branch@";
public static final int METRICS_ID = 14649;