diff --git a/api/src/main/java/com/bivashy/auth/api/AuthPlugin.java b/api/src/main/java/com/bivashy/auth/api/AuthPlugin.java index dc318548..2dcbb530 100644 --- a/api/src/main/java/com/bivashy/auth/api/AuthPlugin.java +++ b/api/src/main/java/com/bivashy/auth/api/AuthPlugin.java @@ -3,23 +3,19 @@ import java.io.File; import com.bivashy.auth.api.account.AccountFactory; -import com.bivashy.auth.api.bucket.AuthenticatingAccountBucket; -import com.bivashy.auth.api.bucket.AuthenticationStepContextFactoryBucket; -import com.bivashy.auth.api.bucket.AuthenticationStepFactoryBucket; -import com.bivashy.auth.api.bucket.AuthenticationTaskBucket; -import com.bivashy.auth.api.bucket.CryptoProviderBucket; -import com.bivashy.auth.api.bucket.LinkAuthenticationBucket; -import com.bivashy.auth.api.bucket.LinkConfirmationBucket; +import com.bivashy.auth.api.bucket.*; import com.bivashy.auth.api.config.PluginConfig; import com.bivashy.auth.api.database.AccountDatabase; import com.bivashy.auth.api.hook.PluginHook; import com.bivashy.auth.api.link.user.entry.LinkEntryUser; import com.bivashy.auth.api.management.LibraryManagement; import com.bivashy.auth.api.management.LoginManagement; +import com.bivashy.auth.api.premium.PremiumProvider; import com.bivashy.auth.api.provider.LinkTypeProvider; import com.bivashy.auth.api.server.ServerCore; import com.bivashy.auth.api.util.Castable; import com.bivashy.configuration.ConfigurationProcessor; +import com.google.gson.Gson; import com.warrenstrange.googleauth.GoogleAuthenticator; import io.github.revxrsal.eventbus.EventBus; @@ -46,10 +42,14 @@ static AuthPlugin instance() { AuthenticationStepContextFactoryBucket getAuthenticationContextFactoryBucket(); + AuthenticationStepContextFactoryBucket getPremiumAuthenticationContextFactoryBucket(); + ConfigurationProcessor getConfigurationProcessor(); LoginManagement getLoginManagement(); + PremiumProvider getPremiumProvider(); + AuthPlugin setLoginManagement(LoginManagement loginManagement); LinkTypeProvider getLinkTypeProvider(); @@ -58,10 +58,14 @@ static AuthPlugin instance() { AuthPlugin setEventBus(EventBus eventBus); + Gson getGson(); + AuthenticationTaskBucket getAuthenticationTaskBucket(); AuthenticatingAccountBucket getAuthenticatingAccountBucket(); + PendingLoginBucket getPendingLoginBucket(); + LinkConfirmationBucket getLinkConfirmationBucket(); LinkAuthenticationBucket getLinkEntryBucket(); diff --git a/api/src/main/java/com/bivashy/auth/api/account/Account.java b/api/src/main/java/com/bivashy/auth/api/account/Account.java index cb85511a..ae8eda19 100644 --- a/api/src/main/java/com/bivashy/auth/api/account/Account.java +++ b/api/src/main/java/com/bivashy/auth/api/account/Account.java @@ -47,6 +47,8 @@ default void setHashType(CryptoProvider cryptoProvider) { String getName(); + void setName(String newName); + HashedPassword getPasswordHash(); void setPasswordHash(HashedPassword passwordHash); @@ -136,6 +138,10 @@ default void logout(long sessionDurability) { boolean isSessionActive(long sessionDurability); + boolean isPremium(); + + void setPremium(boolean newPremium); + default KickResultType kick(String reason) { Optional serverPlayer = getPlayer(); if (!serverPlayer.isPresent()) diff --git a/api/src/main/java/com/bivashy/auth/api/account/AccountFactory.java b/api/src/main/java/com/bivashy/auth/api/account/AccountFactory.java index 7eda5233..a27284be 100644 --- a/api/src/main/java/com/bivashy/auth/api/account/AccountFactory.java +++ b/api/src/main/java/com/bivashy/auth/api/account/AccountFactory.java @@ -9,5 +9,11 @@ public interface AccountFactory { long DEFAULT_TELEGRAM_ID = -1; int DEFAULT_VK_ID = -1; + /** + * @deprecated use {@link #createAccount(String, IdentifierType, UUID, String, CryptoProvider, String, String, boolean)} instead. + */ + @Deprecated Account createAccount(String id, IdentifierType identifierType, UUID uuid, String name, CryptoProvider cryptoProvider, String passwordHash, String lastIp); + + Account createAccount(String id, IdentifierType identifierType, UUID uuid, String name, CryptoProvider cryptoProvider, String passwordHash, String lastIp, boolean isPremium); } \ No newline at end of file diff --git a/api/src/main/java/com/bivashy/auth/api/bucket/PendingLoginBucket.java b/api/src/main/java/com/bivashy/auth/api/bucket/PendingLoginBucket.java new file mode 100644 index 00000000..99cfb939 --- /dev/null +++ b/api/src/main/java/com/bivashy/auth/api/bucket/PendingLoginBucket.java @@ -0,0 +1,28 @@ +package com.bivashy.auth.api.bucket; + +import com.bivashy.auth.api.account.Account; +import com.bivashy.auth.api.bucket.AuthenticatingAccountBucket.AuthenticatingAccountState; +import com.bivashy.auth.api.model.PlayerIdSupplier; + +import java.util.Collection; +import java.util.Optional; +import java.util.stream.Collectors; + +public interface PendingLoginBucket extends Bucket { + default boolean hasFailedLogin(String ip, String username) { + return hasByValue(PendingLoginState::getPendingLoginId, ip + username); + } + + void addPendingLogin(String ip, String username); + + void removePendingLogin(String ip, String username); + + interface PendingLoginState { + default String getPendingLoginId() { + return getAddress() + getUsername(); + } + + String getAddress(); + String getUsername(); + } +} diff --git a/api/src/main/java/com/bivashy/auth/api/config/PluginConfig.java b/api/src/main/java/com/bivashy/auth/api/config/PluginConfig.java index 63bd8fb9..207815e4 100644 --- a/api/src/main/java/com/bivashy/auth/api/config/PluginConfig.java +++ b/api/src/main/java/com/bivashy/auth/api/config/PluginConfig.java @@ -12,6 +12,7 @@ import com.bivashy.auth.api.config.link.TelegramSettings; import com.bivashy.auth.api.config.link.VKSettings; import com.bivashy.auth.api.config.message.server.ServerMessages; +import com.bivashy.auth.api.config.premium.PremiumSettings; import com.bivashy.auth.api.config.server.ConfigurationServer; import com.bivashy.auth.api.crypto.CryptoProvider; import com.bivashy.auth.api.database.DatabaseConnectionProvider; @@ -33,6 +34,10 @@ public interface PluginConfig { DatabaseConnectionProvider getStorageType(); + int getNameMinLength(); + + int getNameMaxLength(); + Pattern getNamePattern(); List getAuthServers(); @@ -49,6 +54,8 @@ public interface PluginConfig { String getAuthenticationStepName(int index); + String getPremiumAuthenticationStepName(int index); + boolean isPasswordConfirmationEnabled(); boolean isPasswordInChatEnabled(); @@ -81,6 +88,8 @@ public interface PluginConfig { GoogleAuthenticatorSettings getGoogleAuthenticatorSettings(); + PremiumSettings getPremiumSettings(); + TelegramSettings getTelegramSettings(); VKSettings getVKSettings(); diff --git a/api/src/main/java/com/bivashy/auth/api/config/premium/PremiumSettings.java b/api/src/main/java/com/bivashy/auth/api/config/premium/PremiumSettings.java new file mode 100644 index 00000000..f5281a31 --- /dev/null +++ b/api/src/main/java/com/bivashy/auth/api/config/premium/PremiumSettings.java @@ -0,0 +1,13 @@ +package com.bivashy.auth.api.config.premium; + +import java.util.List; + +public interface PremiumSettings { + boolean isEnabled(); + + boolean getBlockOfflinePlayersWithPremiumName(); + + ProfileConflictResolutionStrategy getProfileConflictResolutionStrategy(); + + List getAuthenticationSteps(); +} diff --git a/api/src/main/java/com/bivashy/auth/api/config/premium/ProfileConflictResolutionStrategy.java b/api/src/main/java/com/bivashy/auth/api/config/premium/ProfileConflictResolutionStrategy.java new file mode 100644 index 00000000..1ec74883 --- /dev/null +++ b/api/src/main/java/com/bivashy/auth/api/config/premium/ProfileConflictResolutionStrategy.java @@ -0,0 +1,5 @@ +package com.bivashy.auth.api.config.premium; + +public enum ProfileConflictResolutionStrategy { + BLOCK, USE_OFFLINE, OVERWRITE +} diff --git a/api/src/main/java/com/bivashy/auth/api/database/AccountDatabase.java b/api/src/main/java/com/bivashy/auth/api/database/AccountDatabase.java index bdfd82d2..b4705c84 100644 --- a/api/src/main/java/com/bivashy/auth/api/database/AccountDatabase.java +++ b/api/src/main/java/com/bivashy/auth/api/database/AccountDatabase.java @@ -1,6 +1,7 @@ package com.bivashy.auth.api.database; import java.util.Collection; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import com.bivashy.auth.api.account.Account; @@ -12,6 +13,8 @@ public interface AccountDatabase { CompletableFuture getAccountFromName(String playerName); + CompletableFuture getAccountFromUUID(UUID uuid); + @Deprecated CompletableFuture> getAccountsByVKID(Integer id); diff --git a/api/src/main/java/com/bivashy/auth/api/event/result/PreLoginResult.java b/api/src/main/java/com/bivashy/auth/api/event/result/PreLoginResult.java new file mode 100644 index 00000000..1d941f47 --- /dev/null +++ b/api/src/main/java/com/bivashy/auth/api/event/result/PreLoginResult.java @@ -0,0 +1,31 @@ +package com.bivashy.auth.api.event.result; + +import com.bivashy.auth.api.server.message.ServerComponent; + +public class PreLoginResult { + private final PreLoginState state; + private ServerComponent disconnectMessage; + + public PreLoginResult(PreLoginState state, ServerComponent disconnectMessage) { + this.state = state; + this.disconnectMessage = disconnectMessage; + } + + public PreLoginResult(PreLoginState state) { + this.state = state; + } + + public PreLoginState getState() { + return state; + } + + public ServerComponent getDisconnectMessage() { + return disconnectMessage; + } + + public enum PreLoginState { + FORCE_ONLINE, + FORCE_OFFLINE, + DENIED + } +} diff --git a/api/src/main/java/com/bivashy/auth/api/management/LoginManagement.java b/api/src/main/java/com/bivashy/auth/api/management/LoginManagement.java index e84b5e7f..247601e8 100644 --- a/api/src/main/java/com/bivashy/auth/api/management/LoginManagement.java +++ b/api/src/main/java/com/bivashy/auth/api/management/LoginManagement.java @@ -1,8 +1,10 @@ package com.bivashy.auth.api.management; import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; import com.bivashy.auth.api.account.Account; +import com.bivashy.auth.api.event.result.PreLoginResult; import com.bivashy.auth.api.server.player.ServerPlayer; /** @@ -10,6 +12,12 @@ * same ip. Or remove */ public interface LoginManagement { + /** + * Handle player pre login. Check for premium UUID and + * force online/offline mode. + */ + void onPreLogin(String ip, String username, Consumer continuation); + /** * Handle player join. Start authentication/registration/session process. * On BungeeCord this will use LoginEvent and player from "connection". diff --git a/api/src/main/java/com/bivashy/auth/api/premium/PremiumException.java b/api/src/main/java/com/bivashy/auth/api/premium/PremiumException.java new file mode 100644 index 00000000..c99957c4 --- /dev/null +++ b/api/src/main/java/com/bivashy/auth/api/premium/PremiumException.java @@ -0,0 +1,63 @@ +package com.bivashy.auth.api.premium; + +/** + * An exception that is thrown when fetching premium data fails. + * + * @author kyngs + */ +public class PremiumException extends Exception { + + /** + * The issue that caused this exception. + */ + private final Issue issue; + + /** + * Construct a new PremiumException with the given Issue and Exception. + * + * @param issue The issue related to the exception. + * @param exception The exception that caused the issue. + */ + public PremiumException(Issue issue, Exception exception) { + super(exception); + this.issue = issue; + } + + /** + * Construct a new PremiumException with the given Issue and message. + * + * @param issue The issue related to the exception. + * @param message The message describing the exception. + */ + public PremiumException(Issue issue, String message) { + super(message); + this.issue = issue; + } + + /** + * Gets the issue that caused this exception. + * + * @return the issue that caused this exception + */ + public Issue getIssue() { + return issue; + } + + /** + * Possible issues that can cause this exception. + */ + public enum Issue { + /** + * The API throttled the request. + */ + THROTTLED, + /** + * The API returned an invalid response. + */ + SERVER_EXCEPTION, + /** + * Other issues. + */ + UNDEFINED + } +} diff --git a/api/src/main/java/com/bivashy/auth/api/premium/PremiumProvider.java b/api/src/main/java/com/bivashy/auth/api/premium/PremiumProvider.java new file mode 100644 index 00000000..16cb54a9 --- /dev/null +++ b/api/src/main/java/com/bivashy/auth/api/premium/PremiumProvider.java @@ -0,0 +1,15 @@ +package com.bivashy.auth.api.premium; + +import java.util.UUID; + +public interface PremiumProvider { + + /** + * This method fetches a user by their username. + * + * @param name The username of the user. + * @return The user, or null if the user does not exist. + * @throws PremiumException If the user could not be fetched. + */ + UUID getPremiumUUIDForName(String name) throws PremiumException; +} diff --git a/api/src/main/java/com/bivashy/auth/api/server/player/ServerPlayer.java b/api/src/main/java/com/bivashy/auth/api/server/player/ServerPlayer.java index 097e4be9..59291cc5 100644 --- a/api/src/main/java/com/bivashy/auth/api/server/player/ServerPlayer.java +++ b/api/src/main/java/com/bivashy/auth/api/server/player/ServerPlayer.java @@ -39,5 +39,7 @@ default void sendMessage(String message) { Optional getCurrentServer(); + boolean isOnlineMode(); + T getRealPlayer(); } diff --git a/api/src/main/java/com/bivashy/auth/api/util/PasswordGenerator.java b/api/src/main/java/com/bivashy/auth/api/util/PasswordGenerator.java new file mode 100644 index 00000000..e0da09b2 --- /dev/null +++ b/api/src/main/java/com/bivashy/auth/api/util/PasswordGenerator.java @@ -0,0 +1,149 @@ +package com.bivashy.auth.api.util; + +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +public final class PasswordGenerator { + + private static final String LOWER = "abcdefghijklmnopqrstuvwxyz"; + private static final String UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + private static final String DIGITS = "0123456789"; + private static final String PUNCTUATION = "!@#$%&*()_+-=[]|,./?><"; + private boolean useLower; + private boolean useUpper; + private boolean useDigits; + private boolean usePunctuation; + + private PasswordGenerator() { + throw new UnsupportedOperationException("Empty constructor is not supported."); + } + + private PasswordGenerator(PasswordGeneratorBuilder builder) { + this.useLower = builder.useLower; + this.useUpper = builder.useUpper; + this.useDigits = builder.useDigits; + this.usePunctuation = builder.usePunctuation; + } + + public static class PasswordGeneratorBuilder { + + private boolean useLower; + private boolean useUpper; + private boolean useDigits; + private boolean usePunctuation; + + public PasswordGeneratorBuilder() { + this.useLower = false; + this.useUpper = false; + this.useDigits = false; + this.usePunctuation = false; + } + + /** + * Set true in case you would like to include lower characters + * (abc...xyz). Default false. + * + * @param useLower true in case you would like to include lower + * characters (abc...xyz). Default false. + * @return the builder for chaining. + */ + public PasswordGeneratorBuilder useLower(boolean useLower) { + this.useLower = useLower; + return this; + } + + /** + * Set true in case you would like to include upper characters + * (ABC...XYZ). Default false. + * + * @param useUpper true in case you would like to include upper + * characters (ABC...XYZ). Default false. + * @return the builder for chaining. + */ + public PasswordGeneratorBuilder useUpper(boolean useUpper) { + this.useUpper = useUpper; + return this; + } + + /** + * Set true in case you would like to include digit characters (123..). + * Default false. + * + * @param useDigits true in case you would like to include digit + * characters (123..). Default false. + * @return the builder for chaining. + */ + public PasswordGeneratorBuilder useDigits(boolean useDigits) { + this.useDigits = useDigits; + return this; + } + + /** + * Set true in case you would like to include punctuation characters + * (!@#..). Default false. + * + * @param usePunctuation true in case you would like to include + * punctuation characters (!@#..). Default false. + * @return the builder for chaining. + */ + public PasswordGeneratorBuilder usePunctuation(boolean usePunctuation) { + this.usePunctuation = usePunctuation; + return this; + } + + /** + * Get an object to use. + * + * @return the {@link PasswordGenerator} + * object. + */ + public PasswordGenerator build() { + return new PasswordGenerator(this); + } + } + + /** + * This method will generate a password depending the use* properties you + * define. It will use the categories with a probability. It is not sure + * that all of the defined categories will be used. + * + * @param length the length of the password you would like to generate. + * @return a password that uses the categories you define when constructing + * the object with a probability. + */ + public String generate(int length) { + // Argument Validation. + if (length <= 0) { + return ""; + } + + // Variables. + StringBuilder password = new StringBuilder(length); + Random random = new SecureRandom(); + + // Collect the categories to use. + List charCategories = new ArrayList<>(4); + if (useLower) { + charCategories.add(LOWER); + } + if (useUpper) { + charCategories.add(UPPER); + } + if (useDigits) { + charCategories.add(DIGITS); + } + if (usePunctuation) { + charCategories.add(PUNCTUATION); + } + + // Build the password. + for (int i = 0; i < length; i++) { + String charCategory = charCategories.get(random.nextInt(charCategories.size())); + int position = random.nextInt(charCategory.length()); + password.append(charCategory.charAt(position)); + } + return new String(password); + } +} \ No newline at end of file diff --git a/api/src/main/java/com/bivashy/auth/api/util/ThrowableFunction.java b/api/src/main/java/com/bivashy/auth/api/util/ThrowableFunction.java new file mode 100644 index 00000000..f8d5249e --- /dev/null +++ b/api/src/main/java/com/bivashy/auth/api/util/ThrowableFunction.java @@ -0,0 +1,23 @@ +package com.bivashy.auth.api.util; + +/** + * A functional interface that represents a function that takes in a parameter of type T and returns a result of type V, + * and throws an exception of type {@link E}. + * + * @param the type of the input to the function + * @param the type of the result of the function + * @param the type of the exception that can be thrown + * @see java.util.function.Function + */ +public interface ThrowableFunction { + + /** + * Applies the given function to the specified argument. + * + * @param t the argument to apply the function to + * @return the result of applying the function to the argument + * @throws E if an error occurs during the application of the function + */ + V apply(T t) throws E; + +} diff --git a/bungee/src/main/java/me/mastercapexd/auth/bungee/listener/AuthenticationListener.java b/bungee/src/main/java/me/mastercapexd/auth/bungee/listener/AuthenticationListener.java index 7764f8be..e1200aad 100644 --- a/bungee/src/main/java/me/mastercapexd/auth/bungee/listener/AuthenticationListener.java +++ b/bungee/src/main/java/me/mastercapexd/auth/bungee/listener/AuthenticationListener.java @@ -11,14 +11,14 @@ import com.bivashy.auth.api.server.player.ServerPlayer; import me.mastercapexd.auth.bungee.BungeeAuthPluginBootstrap; +import me.mastercapexd.auth.bungee.message.BungeeComponent; import me.mastercapexd.auth.bungee.player.BungeeConnectionProxyPlayer; import me.mastercapexd.auth.bungee.player.BungeeServerPlayer; import me.mastercapexd.auth.bungee.server.BungeeServer; import me.mastercapexd.auth.config.message.server.ServerMessageContext; -import net.md_5.bungee.api.event.ChatEvent; -import net.md_5.bungee.api.event.LoginEvent; -import net.md_5.bungee.api.event.PlayerDisconnectEvent; -import net.md_5.bungee.api.event.ServerConnectEvent; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.ComponentBuilder; +import net.md_5.bungee.api.event.*; import net.md_5.bungee.api.plugin.Listener; import net.md_5.bungee.event.EventHandler; @@ -31,6 +31,28 @@ public AuthenticationListener(AuthPlugin plugin) { this.plugin = plugin; } + @EventHandler + public void onPreLogin(PreLoginEvent event) { + event.registerIntent(bungeePlugin); + plugin.getLoginManagement().onPreLogin(event.getConnection().getAddress().getAddress().getHostAddress(), event.getConnection().getName(), result -> { + switch (result.getState()) { + case DENIED: + ServerPlayer p = new BungeeConnectionProxyPlayer(event.getConnection()); + p.disconnect(result.getDisconnectMessage()); + event.setCancelled(true); + break; + case FORCE_OFFLINE: + event.getConnection().setOnlineMode(false); + break; + case FORCE_ONLINE: + event.getConnection().setOnlineMode(true); + break; + } + + event.completeIntent(bungeePlugin); + }); + } + @EventHandler public void onLogin(LoginEvent event) { event.registerIntent(bungeePlugin); diff --git a/bungee/src/main/java/me/mastercapexd/auth/bungee/player/BungeeConnectionProxyPlayer.java b/bungee/src/main/java/me/mastercapexd/auth/bungee/player/BungeeConnectionProxyPlayer.java index 65703c23..fc1a031a 100644 --- a/bungee/src/main/java/me/mastercapexd/auth/bungee/player/BungeeConnectionProxyPlayer.java +++ b/bungee/src/main/java/me/mastercapexd/auth/bungee/player/BungeeConnectionProxyPlayer.java @@ -57,6 +57,11 @@ public Optional getCurrentServer() { return Optional.empty(); } + @Override + public boolean isOnlineMode() { + return pendingConnection.isOnlineMode(); + } + @Override public T getRealPlayer() { return (T) pendingConnection; diff --git a/bungee/src/main/java/me/mastercapexd/auth/bungee/player/BungeeServerPlayer.java b/bungee/src/main/java/me/mastercapexd/auth/bungee/player/BungeeServerPlayer.java index a24931ae..7106c757 100644 --- a/bungee/src/main/java/me/mastercapexd/auth/bungee/player/BungeeServerPlayer.java +++ b/bungee/src/main/java/me/mastercapexd/auth/bungee/player/BungeeServerPlayer.java @@ -65,6 +65,11 @@ public Optional getCurrentServer() { return Optional.of(new BungeeServer(player.getServer().getInfo())); } + @Override + public boolean isOnlineMode() { + return player.getPendingConnection().isOnlineMode(); + } + @SuppressWarnings("unchecked") @Override public T getRealPlayer() { diff --git a/bungee/src/main/resources/bungee.yml b/bungee/src/main/resources/bungee.yml index 17998a56..2ef81d5f 100644 --- a/bungee/src/main/resources/bungee.yml +++ b/bungee/src/main/resources/bungee.yml @@ -5,4 +5,4 @@ softDepends: - VK-API - JavaTelegramBotApi - NanoLimboBungee -author: bivashy, MasterCapeXD +author: bivashy, MasterCapeXD \ No newline at end of file diff --git a/core/pom.xml b/core/pom.xml index 449be3d6..98a945d2 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -119,5 +119,12 @@ api provided + + + + com.github.ben-manes.caffeine + caffeine + provided + \ No newline at end of file diff --git a/core/src/main/java/me/mastercapexd/auth/BaseAuthPlugin.java b/core/src/main/java/me/mastercapexd/auth/BaseAuthPlugin.java index 5e5d7d68..10c71c33 100644 --- a/core/src/main/java/me/mastercapexd/auth/BaseAuthPlugin.java +++ b/core/src/main/java/me/mastercapexd/auth/BaseAuthPlugin.java @@ -16,13 +16,7 @@ import com.bivashy.auth.api.asset.resource.Resource; import com.bivashy.auth.api.asset.resource.impl.FolderResource; import com.bivashy.auth.api.asset.resource.impl.FolderResourceReader; -import com.bivashy.auth.api.bucket.AuthenticatingAccountBucket; -import com.bivashy.auth.api.bucket.AuthenticationStepContextFactoryBucket; -import com.bivashy.auth.api.bucket.AuthenticationStepFactoryBucket; -import com.bivashy.auth.api.bucket.AuthenticationTaskBucket; -import com.bivashy.auth.api.bucket.CryptoProviderBucket; -import com.bivashy.auth.api.bucket.LinkAuthenticationBucket; -import com.bivashy.auth.api.bucket.LinkConfirmationBucket; +import com.bivashy.auth.api.bucket.*; import com.bivashy.auth.api.config.PluginConfig; import com.bivashy.auth.api.config.duration.ConfigurationDuration; import com.bivashy.auth.api.config.server.ConfigurationServer; @@ -32,6 +26,7 @@ import com.bivashy.auth.api.link.user.entry.LinkEntryUser; import com.bivashy.auth.api.management.LibraryManagement; import com.bivashy.auth.api.management.LoginManagement; +import com.bivashy.auth.api.premium.PremiumProvider; import com.bivashy.auth.api.provider.LinkTypeProvider; import com.bivashy.auth.api.server.ServerCore; import com.bivashy.auth.api.server.message.ServerComponent; @@ -41,18 +36,13 @@ import com.bivashy.messenger.discord.provider.DiscordApiProvider; import com.bivashy.messenger.telegram.message.TelegramMessage; import com.bivashy.messenger.telegram.providers.TelegramApiProvider; +import com.google.gson.Gson; import com.warrenstrange.googleauth.GoogleAuthenticator; import io.github.revxrsal.eventbus.EventBus; import io.github.revxrsal.eventbus.EventBusBuilder; import me.mastercapexd.auth.account.factory.AuthAccountFactory; -import me.mastercapexd.auth.bucket.BaseAuthenticatingAccountBucket; -import me.mastercapexd.auth.bucket.BaseAuthenticationStepContextFactoryBucket; -import me.mastercapexd.auth.bucket.BaseAuthenticationStepFactoryBucket; -import me.mastercapexd.auth.bucket.BaseAuthenticationTaskBucket; -import me.mastercapexd.auth.bucket.BaseCryptoProviderBucket; -import me.mastercapexd.auth.bucket.BaseLinkAuthenticationBucket; -import me.mastercapexd.auth.bucket.BaseLinkConfirmationBucket; +import me.mastercapexd.auth.bucket.*; import me.mastercapexd.auth.config.BasePluginConfig; import me.mastercapexd.auth.config.factory.ConfigurationHolderMapResolverFactory; import me.mastercapexd.auth.config.resolver.RawURLProviderFieldResolverFactory; @@ -78,6 +68,7 @@ import me.mastercapexd.auth.listener.AuthenticationChatPasswordListener; import me.mastercapexd.auth.management.BaseLibraryManagement; import me.mastercapexd.auth.management.BaseLoginManagement; +import me.mastercapexd.auth.premium.BasePremiumProvider; import me.mastercapexd.auth.step.impl.EnterAuthServerAuthenticationStep.EnterAuthServerAuthenticationStepFactory; import me.mastercapexd.auth.step.impl.EnterServerAuthenticationStep.EnterServerAuthenticationStepFactory; import me.mastercapexd.auth.step.impl.LoginAuthenticationStep.LoginAuthenticationStepFactory; @@ -108,11 +99,13 @@ public class BaseAuthPlugin implements AuthPlugin { private final String version; private final File pluginFolder; private AuthenticationStepContextFactoryBucket authenticationStepContextFactoryBucket; + private AuthenticationStepContextFactoryBucket premiumAuthenticationStepContextFactoryBucket; private AudienceProvider audienceProvider; private LibraryManagement libraryManagement; private ServerCore core; private File dataFolder; private AuthenticatingAccountBucket accountBucket; + private PendingLoginBucket pendingLoginBucket; private EventBus eventBus = EventBusBuilder.asm().executor(Executors.newFixedThreadPool(4)).build(); private GoogleAuthenticator googleAuthenticator; private PluginConfig config; @@ -120,6 +113,8 @@ public class BaseAuthPlugin implements AuthPlugin { private LinkTypeProvider linkTypeProvider; private AccountDatabase accountDatabase; private LoginManagement loginManagement; + private PremiumProvider premiumProvider; + private Gson gson; public BaseAuthPlugin(AudienceProvider audienceProvider, String version, File pluginFolder, ServerCore core, LibraryManagement libraryManagement) { AuthPluginProvider.setPluginInstance(this); @@ -128,6 +123,7 @@ public BaseAuthPlugin(AudienceProvider audienceProvider, String version, File pl this.version = version; this.pluginFolder = pluginFolder; this.libraryManagement = libraryManagement; + this.gson = new Gson(); libraryManagement.loadLibraries(); initializeBasic(); @@ -141,6 +137,7 @@ public BaseAuthPlugin(AudienceProvider audienceProvider, String version, File pl private void initializeBasic() { this.accountBucket = new BaseAuthenticatingAccountBucket(this); + this.pendingLoginBucket = new BasePendingLoginBucket(); this.registerCryptoProviders(); this.registerConfigurationProcessor(); @@ -154,11 +151,13 @@ private void initializeBasic() { } this.authenticationStepContextFactoryBucket = new BaseAuthenticationStepContextFactoryBucket(config.getAuthenticationSteps()); + this.premiumAuthenticationStepContextFactoryBucket = new BaseAuthenticationStepContextFactoryBucket(config.getPremiumSettings().getAuthenticationSteps()); this.accountFactory = new AuthAccountFactory(); this.linkTypeProvider = BaseLinkTypeProvider.allLinks(); // TODO: Replace this with IsolatedDatabaseHelperFactory this.accountDatabase = new AuthAccountDatabaseProxy(new DatabaseHelper(this, new IsolatedClassLoader())); this.loginManagement = new BaseLoginManagement(this); + this.premiumProvider = new BasePremiumProvider(this); this.registerAuthenticationSteps(); @@ -307,6 +306,11 @@ public AuthenticationStepContextFactoryBucket getAuthenticationContextFactoryBuc return authenticationStepContextFactoryBucket; } + @Override + public AuthenticationStepContextFactoryBucket getPremiumAuthenticationContextFactoryBucket() { + return premiumAuthenticationStepContextFactoryBucket; + } + @Override public ConfigurationProcessor getConfigurationProcessor() { return configurationProcessor; @@ -317,6 +321,11 @@ public LoginManagement getLoginManagement() { return loginManagement; } + @Override + public PremiumProvider getPremiumProvider() { + return premiumProvider; + } + @Override public AuthPlugin setLoginManagement(LoginManagement loginManagement) { this.loginManagement = loginManagement; @@ -339,6 +348,11 @@ public AuthPlugin setEventBus(EventBus eventBus) { return this; } + @Override + public Gson getGson() { + return gson; + } + @Override public AuthenticationTaskBucket getAuthenticationTaskBucket() { return taskBucket; @@ -349,6 +363,11 @@ public AuthenticatingAccountBucket getAuthenticatingAccountBucket() { return accountBucket; } + @Override + public PendingLoginBucket getPendingLoginBucket() { + return pendingLoginBucket; + } + @Override public LinkConfirmationBucket getLinkConfirmationBucket() { return linkConfirmationBucket; diff --git a/core/src/main/java/me/mastercapexd/auth/account/AccountTemplate.java b/core/src/main/java/me/mastercapexd/auth/account/AccountTemplate.java index 6473ca09..51aaf31e 100644 --- a/core/src/main/java/me/mastercapexd/auth/account/AccountTemplate.java +++ b/core/src/main/java/me/mastercapexd/auth/account/AccountTemplate.java @@ -32,7 +32,10 @@ public CompletableFuture nextAuthenticationStep(AuthenticationStepContext currentConfigurationAuthenticationStepCreatorIndex = 0; return; } - String stepCreatorName = PLUGIN.getConfig().getAuthenticationStepName(currentConfigurationAuthenticationStepCreatorIndex); + String stepCreatorName = isPremium() ? + PLUGIN.getConfig().getPremiumAuthenticationStepName(currentConfigurationAuthenticationStepCreatorIndex) : + PLUGIN.getConfig().getAuthenticationStepName(currentConfigurationAuthenticationStepCreatorIndex); + AuthenticationStepFactory authenticationStepFactory = PLUGIN.getAuthenticationStepFactoryBucket() .findFirst(stepCreator -> stepCreator.getAuthenticationStepName().equals(stepCreatorName)) .orElse(new NullAuthenticationStepFactory()); @@ -46,7 +49,10 @@ public CompletableFuture nextAuthenticationStep(AuthenticationStepContext currentConfigurationAuthenticationStepCreatorIndex += 1; if (currentAuthenticationStep.shouldSkip()) { currentAuthenticationStep = new NullAuthenticationStep(); - nextAuthenticationStep(PLUGIN.getAuthenticationContextFactoryBucket().createContext(this)).join(); + nextAuthenticationStep(isPremium() ? + PLUGIN.getPremiumAuthenticationContextFactoryBucket().createContext(this) : + PLUGIN.getAuthenticationContextFactoryBucket().createContext(this) + ).join(); } }); } @@ -70,7 +76,9 @@ public void setCurrentAuthenticationStepCreatorIndex(int index) { .findFirst(stepCreator -> stepCreator.getAuthenticationStepName().equals(stepName)) .orElse(new NullAuthenticationStepFactory()); - AuthenticationStepContext stepContext = PLUGIN.getAuthenticationContextFactoryBucket().createContext(stepName, this); + AuthenticationStepContext stepContext = isPremium() ? + PLUGIN.getPremiumAuthenticationContextFactoryBucket().createContext(stepName, this) : + PLUGIN.getAuthenticationContextFactoryBucket().createContext(stepName, this); currentConfigurationAuthenticationStepCreatorIndex = index; currentAuthenticationStep = authenticationStepFactory.createNewAuthenticationStep(stepContext); } diff --git a/core/src/main/java/me/mastercapexd/auth/account/AuthAccountAdapter.java b/core/src/main/java/me/mastercapexd/auth/account/AuthAccountAdapter.java index 3c976bfb..e017718a 100644 --- a/core/src/main/java/me/mastercapexd/auth/account/AuthAccountAdapter.java +++ b/core/src/main/java/me/mastercapexd/auth/account/AuthAccountAdapter.java @@ -81,6 +81,11 @@ public String getName() { return authAccount.getPlayerName(); } + @Override + public void setName(String newName) { + authAccount.setPlayerName(newName); + } + @Override public HashedPassword getPasswordHash() { return HashedPassword.of(authAccount.getPasswordHash(), null, authAccount.getHashType()); @@ -140,6 +145,16 @@ public void setLastSessionStartTimestamp(long currentTimeMillis) { authAccount.setLastSessionStartTimestamp(currentTimeMillis); } + @Override + public boolean isPremium() { + return authAccount.isPremium(); + } + + @Override + public void setPremium(boolean newPremium) { + authAccount.setPremium(newPremium); + } + @Override public int compareTo(AccountTemplate accountTemplate) { return accountTemplate.getName().compareTo(getName()); diff --git a/core/src/main/java/me/mastercapexd/auth/account/factory/AccountFactoryTemplate.java b/core/src/main/java/me/mastercapexd/auth/account/factory/AccountFactoryTemplate.java index 2b9486be..d7a1ea76 100644 --- a/core/src/main/java/me/mastercapexd/auth/account/factory/AccountFactoryTemplate.java +++ b/core/src/main/java/me/mastercapexd/auth/account/factory/AccountFactoryTemplate.java @@ -15,14 +15,20 @@ import me.mastercapexd.auth.link.user.LinkUserTemplate; public abstract class AccountFactoryTemplate implements AccountFactory { + @Override + public Account createAccount(String id, IdentifierType identifierType, UUID uuid, String name, CryptoProvider cryptoProvider, String passwordHash, String lastIp) { + return createAccount(id, identifierType, uuid, name, cryptoProvider, passwordHash, lastIp, false); + } + @Override public Account createAccount(String id, IdentifierType identifierType, UUID uuid, String name, CryptoProvider cryptoProvider, String passwordHash, - String lastIp) { + String lastIp, boolean isPremium) { Account account = newAccount(identifierType.fromRawString(id), identifierType, uuid, name); account.setCryptoProvider(cryptoProvider); account.setPasswordHash(HashedPassword.of(passwordHash, cryptoProvider)); account.setLastIpAddress(lastIp); + account.setPremium(isPremium); for (LinkType linkType : AuthPlugin.instance().getLinkTypeProvider().getLinkTypes()) account.addLinkUser(createUser(linkType, account)); diff --git a/core/src/main/java/me/mastercapexd/auth/bucket/BasePendingLoginBucket.java b/core/src/main/java/me/mastercapexd/auth/bucket/BasePendingLoginBucket.java new file mode 100644 index 00000000..dc3e9d10 --- /dev/null +++ b/core/src/main/java/me/mastercapexd/auth/bucket/BasePendingLoginBucket.java @@ -0,0 +1,38 @@ +package me.mastercapexd.auth.bucket; + +import com.bivashy.auth.api.bucket.PendingLoginBucket; + +public class BasePendingLoginBucket extends BaseListBucket implements PendingLoginBucket { + @Override + public void addPendingLogin(String ip, String username) { + if (hasByValue(PendingLoginState::getPendingLoginId, ip + username)) + return; + modifiable().add(new BasePendingLoginState(ip, username)); + } + + @Override + public void removePendingLogin(String ip, String username) { + modifiable().removeIf(pendingLoginState -> pendingLoginState.getPendingLoginId().equals(ip + username)); + } + + public static class BasePendingLoginState implements PendingLoginState { + private final String ip; + private final String username; + + public BasePendingLoginState(String ip, String username) { + this.ip = ip; + this.username = username; + } + + @Override + public String getAddress() { + return ip; + } + + @Override + public String getUsername() { + return username; + } + } + +} diff --git a/core/src/main/java/me/mastercapexd/auth/config/PluginConfigTemplate.java b/core/src/main/java/me/mastercapexd/auth/config/PluginConfigTemplate.java index 158b6dc9..9ce9208e 100644 --- a/core/src/main/java/me/mastercapexd/auth/config/PluginConfigTemplate.java +++ b/core/src/main/java/me/mastercapexd/auth/config/PluginConfigTemplate.java @@ -17,6 +17,7 @@ import com.bivashy.auth.api.config.link.TelegramSettings; import com.bivashy.auth.api.config.link.VKSettings; import com.bivashy.auth.api.config.message.server.ServerMessages; +import com.bivashy.auth.api.config.premium.PremiumSettings; import com.bivashy.auth.api.config.server.ConfigurationServer; import com.bivashy.auth.api.crypto.CryptoProvider; import com.bivashy.auth.api.database.DatabaseConnectionProvider; @@ -31,6 +32,7 @@ import me.mastercapexd.auth.config.discord.BaseDiscordSettings; import me.mastercapexd.auth.config.google.BaseGoogleAuthenticatorSettings; import me.mastercapexd.auth.config.message.server.BaseServerMessages; +import me.mastercapexd.auth.config.premium.BasePremiumSettings; import me.mastercapexd.auth.config.resolver.RawURLProviderFieldResolverFactory.RawURLProvider; import me.mastercapexd.auth.config.storage.BaseDatabaseConfiguration; import me.mastercapexd.auth.config.storage.BaseLegacyStorageDataSettings; @@ -56,6 +58,10 @@ public abstract class PluginConfigTemplate implements PluginConfig { private CryptoProvider activeCryptoProvider; @ConfigField("storage-type") private DatabaseConnectionProvider databaseConnectionProvider = DatabaseConnectionProvider.SQLITE; + @ConfigField("name-min-length") + private int nameMinLength = 3; + @ConfigField("name-max-length") + private int nameMaxLength = 16; @ConfigField("name-regex-pattern") private Pattern namePattern = Pattern.compile("[a-zA-Z0-9_]*"); @ConfigField("password-min-length") @@ -92,6 +98,8 @@ public abstract class PluginConfigTemplate implements PluginConfig { private BaseDiscordSettings discordSettings = new BaseDiscordSettings(); @ConfigField("google-authenticator") private BaseGoogleAuthenticatorSettings googleAuthenticatorSettings = new BaseGoogleAuthenticatorSettings(); + @ConfigField("premium-support") + private BasePremiumSettings premiumSettings = new BasePremiumSettings(); @ImportantField @ConfigField("messages") private BaseServerMessages serverMessages = null; @@ -179,11 +187,22 @@ public DatabaseConnectionProvider getStorageType() { return databaseConnectionProvider; } + @Override + public int getNameMinLength() { + return nameMinLength; + } + + @Override + public int getNameMaxLength() { + return nameMaxLength; + } + @Override public Pattern getNamePattern() { return namePattern; } + @Override public int getPasswordMinLength() { return passwordMinLength; } @@ -238,6 +257,11 @@ public LegacyStorageDataSettings getStorageDataSettings() { return legacyStorageDataSettings; } + @Override + public PremiumSettings getPremiumSettings() { + return premiumSettings; + } + @Override public BaseDatabaseConfiguration getDatabaseConfiguration() { return databaseConfiguration; @@ -298,6 +322,11 @@ public String getAuthenticationStepName(int index) { return index >= 0 && index < authenticationSteps.size() ? authenticationSteps.get(index) : "NULL"; } + @Override + public String getPremiumAuthenticationStepName(int index) { + return index >= 0 && index < premiumSettings.getAuthenticationSteps().size() ? premiumSettings.getAuthenticationSteps().get(index) : "NULL"; + } + @Override public IntStream getLimboPortRange() { return limboPortRange; diff --git a/core/src/main/java/me/mastercapexd/auth/config/premium/BasePremiumSettings.java b/core/src/main/java/me/mastercapexd/auth/config/premium/BasePremiumSettings.java new file mode 100644 index 00000000..f2086a63 --- /dev/null +++ b/core/src/main/java/me/mastercapexd/auth/config/premium/BasePremiumSettings.java @@ -0,0 +1,50 @@ +package me.mastercapexd.auth.config.premium; + +import com.bivashy.auth.api.AuthPlugin; +import com.bivashy.auth.api.config.premium.PremiumSettings; +import com.bivashy.auth.api.config.premium.ProfileConflictResolutionStrategy; +import com.bivashy.configuration.ConfigurationHolder; +import com.bivashy.configuration.annotation.ConfigField; +import com.bivashy.configuration.holder.ConfigurationSectionHolder; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class BasePremiumSettings implements ConfigurationHolder, PremiumSettings { + @ConfigField("enabled") + private boolean enabled = true; + @ConfigField("block-offline-players-with-premium-name") + private boolean blockOfflinePlayersWithPremiumName = false; + @ConfigField("profile-conflict-resolution-strategy") + private ProfileConflictResolutionStrategy profileConflictResolutionStrategy = ProfileConflictResolutionStrategy.BLOCK; + @ConfigField("authentication-steps") + private List authenticationSteps = Collections.singletonList("ENTER_SERVER"); + + public BasePremiumSettings(ConfigurationSectionHolder sectionHolder) { + AuthPlugin.instance().getConfigurationProcessor().resolve(sectionHolder, this); + } + + public BasePremiumSettings() { + } + + @Override + public boolean isEnabled() { + return enabled; + } + + @Override + public boolean getBlockOfflinePlayersWithPremiumName() { + return blockOfflinePlayersWithPremiumName; + } + + @Override + public ProfileConflictResolutionStrategy getProfileConflictResolutionStrategy() { + return profileConflictResolutionStrategy; + } + + @Override + public List getAuthenticationSteps() { + return Collections.unmodifiableList(authenticationSteps); + } +} diff --git a/core/src/main/java/me/mastercapexd/auth/database/AuthAccountDatabaseProxy.java b/core/src/main/java/me/mastercapexd/auth/database/AuthAccountDatabaseProxy.java index 61372f41..7081721c 100644 --- a/core/src/main/java/me/mastercapexd/auth/database/AuthAccountDatabaseProxy.java +++ b/core/src/main/java/me/mastercapexd/auth/database/AuthAccountDatabaseProxy.java @@ -1,6 +1,7 @@ package me.mastercapexd.auth.database; import java.util.Collection; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; @@ -30,6 +31,12 @@ public CompletableFuture getAccountFromName(String playerName) { () -> databaseHelper.getAuthAccountDao().queryFirstAccountPlayerName(playerName).map(AuthAccountAdapter::new).orElse(null)); } + @Override + public CompletableFuture getAccountFromUUID(UUID uuid) { + return CompletableFuture.supplyAsync( + () -> databaseHelper.getAuthAccountDao().queryFirstAccountPlayerUUID(uuid).map(AuthAccountAdapter::new).orElse(null)); + } + @Override public CompletableFuture> getAccountsByVKID(Integer id) { return getAccountsFromLinkIdentificator(new UserStringIdentificator(Integer.toString(id))); diff --git a/core/src/main/java/me/mastercapexd/auth/database/adapter/AccountAdapter.java b/core/src/main/java/me/mastercapexd/auth/database/adapter/AccountAdapter.java index 2aa9463b..af5d2f8b 100644 --- a/core/src/main/java/me/mastercapexd/auth/database/adapter/AccountAdapter.java +++ b/core/src/main/java/me/mastercapexd/auth/database/adapter/AccountAdapter.java @@ -9,7 +9,7 @@ public class AccountAdapter extends AuthAccount { public AccountAdapter(Account account) { super(account.getDatabaseId(), account.getPlayerId(), account.getIdentifierType(), account.getCryptoProvider(), account.getLastIpAddress(), account.getUniqueId(), account.getName(), account.getPasswordHash() - .getHash(), account.getLastQuitTimestamp(), account.getLastSessionStartTimestamp()); + .getHash(), account.getLastQuitTimestamp(), account.getLastSessionStartTimestamp(), account.isPremium()); } } diff --git a/core/src/main/java/me/mastercapexd/auth/database/dao/AuthAccountDao.java b/core/src/main/java/me/mastercapexd/auth/database/dao/AuthAccountDao.java index 26be9311..1d24cd74 100644 --- a/core/src/main/java/me/mastercapexd/auth/database/dao/AuthAccountDao.java +++ b/core/src/main/java/me/mastercapexd/auth/database/dao/AuthAccountDao.java @@ -1,17 +1,14 @@ package me.mastercapexd.auth.database.dao; import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Optional; +import java.util.*; import com.bivashy.auth.api.account.Account; import com.bivashy.auth.api.account.AccountFactory; import com.bivashy.auth.api.config.database.schema.TableSettings; import com.bivashy.auth.api.link.LinkType; import com.bivashy.auth.api.link.user.info.LinkUserIdentificator; +import com.bivashy.auth.api.type.IdentifierType; import com.j256.ormlite.dao.BaseDaoImpl; import com.j256.ormlite.field.DataType; import com.j256.ormlite.field.DatabaseFieldConfig; @@ -36,6 +33,7 @@ public class AuthAccountDao extends BaseDaoImpl { private static final String PASSWORD_HASH_CONFIGURATION_KEY = "passwordHash"; private static final String LAST_QUIT_TIMESTAMP_CONFIGURATION_KEY = "lastQuitTimestamp"; private static final String LAST_SESSION_TIMESTAMP_START_CONFIGURATION_KEY = "lastSessionStartTimestamp"; + private static final String IS_PREMIUM_CONFIGURATION_KEY = "isPremium"; private static final String LINKS_CONFIGURATION_KEY = "links"; private static final SupplierExceptionCatcher DEFAULT_EXCEPTION_CATCHER = new SupplierExceptionCatcher(); private final DatabaseHelper databaseHelper; @@ -91,6 +89,10 @@ private static DatabaseTableConfig createTableConfig(TableSettings lastSessionStartTimestampFieldConfig.setDataType(DataType.LONG); fields.add(lastSessionStartTimestampFieldConfig); + DatabaseFieldConfig isPremiumFieldConfig = createFieldConfig(settings, IS_PREMIUM_CONFIGURATION_KEY, + AuthAccount.IS_PREMIUM_FIELD_KEY); + fields.add(isPremiumFieldConfig); + DatabaseFieldConfig linksFieldConfig = new DatabaseFieldConfig(LINKS_CONFIGURATION_KEY); linksFieldConfig.setForeignCollection(true); fields.add(linksFieldConfig); @@ -113,6 +115,11 @@ public Optional queryFirstAccountPlayerName(String playerName) { DEFAULT_EXCEPTION_CATCHER.execute(() -> queryBuilder().where().eq(AuthAccount.PLAYER_NAME_FIELD_KEY, playerName).queryForFirst())); } + public Optional queryFirstAccountPlayerUUID(UUID uuid) { + return Optional.ofNullable( + DEFAULT_EXCEPTION_CATCHER.execute(() -> queryBuilder().where().eq(AuthAccount.UNIQUE_ID_FIELD_KEY, uuid).queryForFirst())); + } + public Collection queryAccounts(LinkUserIdentificator linkUserIdentificator, String linkType) { return DEFAULT_EXCEPTION_CATCHER.execute(() -> queryBuilder().where() .in(DatabaseHelper.ID_FIELD_KEY, databaseHelper.getAccountLinkDao().queryBuilder(linkUserIdentificator, linkType)) @@ -159,6 +166,8 @@ public AuthAccount createOrUpdateAccount(Account account) { Optional foundAccount = queryFirstAccountPlayerId(authAccount.getPlayerId()); if (foundAccount.isPresent()) { authAccount.setId(foundAccount.get().getId()); + if (authAccount.getPlayerIdType() == IdentifierType.NAME && !authAccount.getPlayerId().equals(authAccount.getPlayerName())) + authAccount.setPlayerId(authAccount.getPlayerName()); update(authAccount); } else create(authAccount); diff --git a/core/src/main/java/me/mastercapexd/auth/database/model/AuthAccount.java b/core/src/main/java/me/mastercapexd/auth/database/model/AuthAccount.java index 493bffea..81fdc343 100644 --- a/core/src/main/java/me/mastercapexd/auth/database/model/AuthAccount.java +++ b/core/src/main/java/me/mastercapexd/auth/database/model/AuthAccount.java @@ -23,6 +23,7 @@ public class AuthAccount { public static final String LAST_SESSION_TIMESTAMP_START_FIELD_KEY = "last_session_start"; public static final String PLAYER_ID_TYPE_FIELD_KEY = "player_id_type"; public static final String HASH_TYPE_FIELD_KEY = "hash_type"; + public static final String IS_PREMIUM_FIELD_KEY = "is_premium"; @DatabaseField(generatedId = true) private long id; @DatabaseField(columnName = PLAYER_ID_FIELD_KEY, unique = true, canBeNull = false) @@ -43,6 +44,8 @@ public class AuthAccount { private long lastQuitTimestamp; @DatabaseField(columnName = LAST_SESSION_TIMESTAMP_START_FIELD_KEY, dataType = DataType.LONG) private long lastSessionStartTimestamp; + @DatabaseField(columnName = IS_PREMIUM_FIELD_KEY) + private boolean isPremium; @ForeignCollectionField private ForeignCollection links; @@ -58,7 +61,8 @@ public AuthAccount(String playerId, IdentifierType playerIdType, String playerNa public AuthAccount(String playerId, IdentifierType playerIdType, CryptoProvider cryptoProvider, String lastIp, UUID uniqueId, String playerName, String passwordHash, - long lastQuitTimestamp, long lastSessionStartTimestamp) { + long lastQuitTimestamp, long lastSessionStartTimestamp, + boolean isPremium) { this.playerId = playerId; this.playerIdType = playerIdType; this.cryptoProvider = cryptoProvider; @@ -68,10 +72,11 @@ public AuthAccount(String playerId, IdentifierType playerIdType, CryptoProvider this.passwordHash = passwordHash; this.lastQuitTimestamp = lastQuitTimestamp; this.lastSessionStartTimestamp = lastSessionStartTimestamp; + this.isPremium = isPremium; } public AuthAccount(long id, String playerId, IdentifierType playerIdType, CryptoProvider cryptoProvider, String lastIp, UUID uniqueId, String playerName, - String passwordHash, long lastQuitTimestamp, long lastSessionStartTimestamp) { + String passwordHash, long lastQuitTimestamp, long lastSessionStartTimestamp, boolean isPremium) { this.id = id; this.playerId = playerId; this.playerIdType = playerIdType; @@ -82,6 +87,7 @@ public AuthAccount(long id, String playerId, IdentifierType playerIdType, Crypto this.passwordHash = passwordHash; this.lastQuitTimestamp = lastQuitTimestamp; this.lastSessionStartTimestamp = lastSessionStartTimestamp; + this.isPremium = isPremium; } public long getId() { @@ -96,6 +102,10 @@ public String getPlayerId() { return playerId; } + public void setPlayerId(String playerId) { + this.playerId = playerId; + } + public IdentifierType getPlayerIdType() { return playerIdType; } @@ -124,6 +134,10 @@ public String getPlayerName() { return playerName; } + public void setPlayerName(String playerName) { + this.playerName = playerName; + } + public String getPasswordHash() { return passwordHash; } @@ -148,6 +162,14 @@ public void setLastSessionStartTimestamp(long lastSessionStartTimestamp) { this.lastSessionStartTimestamp = lastSessionStartTimestamp; } + public boolean isPremium() { + return isPremium; + } + + public void setPremium(boolean newPremium) { + isPremium = newPremium; + } + public ForeignCollection getLinks() { return links; } diff --git a/core/src/main/java/me/mastercapexd/auth/management/BaseLibraryManagement.java b/core/src/main/java/me/mastercapexd/auth/management/BaseLibraryManagement.java index 2e1cb01d..a8e76276 100644 --- a/core/src/main/java/me/mastercapexd/auth/management/BaseLibraryManagement.java +++ b/core/src/main/java/me/mastercapexd/auth/management/BaseLibraryManagement.java @@ -1,7 +1,7 @@ package me.mastercapexd.auth.management; import java.util.ArrayList; -import java.util.Collection; +import java.util.Collections; import java.util.List; import com.alessiodp.libby.Library; @@ -11,6 +11,7 @@ public class BaseLibraryManagement implements LibraryManagement { private static final String JDA_VERSION = "5.0.0-beta.20"; + private static final String CAFFEINE_VERSION = "3.1.8"; public static final Library JDA_LIBRARY = Library.builder() .groupId("net{}dv8tion") .artifactId("JDA") @@ -27,8 +28,17 @@ public class BaseLibraryManagement implements LibraryManagement { .resolveTransitiveDependencies(true) .excludeTransitiveDependency("club{}minnced", "opus-java") .build(); + public static final Library CAFFEINE_LIBRARY = Library.builder() + .groupId("com{}github{}ben-manes{}caffeine") + .artifactId("caffeine") + .relocate("com{}github{}benmanes{}caffeine", "com{}bivashy{}auth{}lib{}com{}github{}benmanes{}caffeine") + .version(CAFFEINE_VERSION) + .resolveTransitiveDependencies(true) + .build(); private final List customRepositories = new ArrayList<>(); - private final List customLibraries = new ArrayList<>(); + private final List customLibraries = new ArrayList<>(Collections.singletonList( + CAFFEINE_LIBRARY + )); private final LibraryManager libraryManager; public BaseLibraryManagement(LibraryManager libraryManager) { @@ -42,9 +52,7 @@ public void loadLibraries() { libraryManager.addMavenCentral(); libraryManager.addJitPack(); - Collection libraries = new ArrayList<>(customLibraries); - - libraries.forEach(libraryManager::loadLibrary); + customLibraries.forEach(libraryManager::loadLibrary); } @Override diff --git a/core/src/main/java/me/mastercapexd/auth/management/BaseLoginManagement.java b/core/src/main/java/me/mastercapexd/auth/management/BaseLoginManagement.java index 43ffd6fa..a1a2869c 100644 --- a/core/src/main/java/me/mastercapexd/auth/management/BaseLoginManagement.java +++ b/core/src/main/java/me/mastercapexd/auth/management/BaseLoginManagement.java @@ -1,25 +1,34 @@ package me.mastercapexd.auth.management; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; import com.bivashy.auth.api.AuthPlugin; import com.bivashy.auth.api.account.Account; import com.bivashy.auth.api.account.AccountFactory; import com.bivashy.auth.api.config.PluginConfig; import com.bivashy.auth.api.config.message.MessageContext; +import com.bivashy.auth.api.config.premium.PremiumSettings; import com.bivashy.auth.api.database.AccountDatabase; import com.bivashy.auth.api.event.AccountJoinEvent; import com.bivashy.auth.api.event.AccountSessionEnterEvent; +import com.bivashy.auth.api.event.result.PreLoginResult; import com.bivashy.auth.api.factory.AuthenticationStepFactory; import com.bivashy.auth.api.management.LoginManagement; +import com.bivashy.auth.api.premium.PremiumException; import com.bivashy.auth.api.server.ServerCore; +import com.bivashy.auth.api.server.message.ServerComponent; import com.bivashy.auth.api.server.player.ServerPlayer; import com.bivashy.auth.api.step.AuthenticationStepContext; +import com.bivashy.auth.api.util.PasswordGenerator; import io.github.revxrsal.eventbus.PostResult; import me.mastercapexd.auth.config.message.server.ServerMessageContext; +import me.mastercapexd.auth.server.commands.impl.RegisterCommandImplementation; import me.mastercapexd.auth.step.impl.NullAuthenticationStep.NullAuthenticationStepFactory; +import org.jetbrains.annotations.Nullable; public class BaseLoginManagement implements LoginManagement { private final AuthPlugin plugin; @@ -37,18 +46,137 @@ public BaseLoginManagement(AuthPlugin plugin) { } @Override - public CompletableFuture onLogin(ServerPlayer player) { - String nickname = player.getNickname(); - if (!config.getNamePattern().matcher(nickname).matches()) { - player.disconnect(config.getServerMessages().getMessage("illegal-name-chars")); - return CompletableFuture.completedFuture(null); + public void onPreLogin(String ip, String username, Consumer continuation) { + ServerComponent invalidNicknameDisconnectMessage = validateNickname(username); + if (invalidNicknameDisconnectMessage != null) { + continuation.accept(new PreLoginResult(PreLoginResult.PreLoginState.DENIED, invalidNicknameDisconnectMessage)); + } + + PremiumSettings premiumSettings = config.getPremiumSettings(); + + if (!premiumSettings.isEnabled()) { + continuation.accept(new PreLoginResult(PreLoginResult.PreLoginState.FORCE_OFFLINE)); + return; } + + if (username.length() > 16) { + onCrackedPreLogin(username, continuation); + return; + } + + if (plugin.getPendingLoginBucket().hasFailedLogin(ip, username)) { + plugin.getPendingLoginBucket().removePendingLogin(ip, username); + + if (premiumSettings.getBlockOfflinePlayersWithPremiumName()) { + continuation.accept(new PreLoginResult( + PreLoginResult.PreLoginState.DENIED, + config.getServerMessages().getMessage("offline-player-with-premium-name-blocked") + )); + return; + } + + onCrackedPreLogin(username, continuation); + return; + } + + UUID premiumUuid = null; + + try { + premiumUuid = plugin.getPremiumProvider().getPremiumUUIDForName(username); + } catch (PremiumException e) { + ServerComponent disconnectMessage; + if (e.getIssue() == PremiumException.Issue.THROTTLED) { + disconnectMessage = config.getServerMessages().getMessage("premium-server-throttled"); + } else { + e.printStackTrace(); + disconnectMessage = config.getServerMessages().getMessage("premium-server-unknown-error"); + } + continuation.accept(new PreLoginResult(PreLoginResult.PreLoginState.DENIED, disconnectMessage)); + } + + if (premiumUuid == null) { + onCrackedPreLogin(username, continuation); + return; + } + + final UUID finalPremiumUuid = premiumUuid; + accountDatabase.getAccountFromName(username).thenAccept(account -> { + if (account == null) { + plugin.getPendingLoginBucket().addPendingLogin(ip, username); + continuation.accept(new PreLoginResult(PreLoginResult.PreLoginState.FORCE_ONLINE)); + return; + } + + if (account.isPremium()) { + continuation.accept(new PreLoginResult(PreLoginResult.PreLoginState.FORCE_ONLINE)); + return; + } + + accountDatabase.getAccountFromUUID(finalPremiumUuid).thenAccept(premiumAccount -> { + if (premiumAccount == null || !premiumAccount.isPremium()) { + continuation.accept(new PreLoginResult(PreLoginResult.PreLoginState.FORCE_OFFLINE)); + return; + } + + plugin.getPendingLoginBucket().addPendingLogin(ip, username); + continuation.accept(new PreLoginResult(PreLoginResult.PreLoginState.FORCE_ONLINE)); + }); + }); + } + + private void onCrackedPreLogin(String username, Consumer continuation) { + accountDatabase.getAccountFromName(username).thenAccept(account -> { + if (account == null || !account.isPremium()) { + continuation.accept(new PreLoginResult(PreLoginResult.PreLoginState.FORCE_OFFLINE)); + } else { + continuation.accept(new PreLoginResult(PreLoginResult.PreLoginState.FORCE_ONLINE)); + } + }); + } + + @Override + public CompletableFuture onLogin(ServerPlayer player) { if (config.getMaxLoginPerIP() != 0 && core.getPlayers().stream().filter(onlinePlayer -> onlinePlayer.getPlayerIp().equals(player.getPlayerIp())).count() > config.getMaxLoginPerIP()) { player.disconnect(config.getServerMessages().getMessage("limit-ip-reached")); return CompletableFuture.completedFuture(null); } + + if (player.isOnlineMode()) { + return accountDatabase.getAccountFromUUID(player.getUniqueId()).thenCompose(premiumAccount -> { + String id = config.getActiveIdentifierType().getId(player); + return accountDatabase.getAccount(id).thenCompose(account -> { + if (premiumAccount == null) { + if (account == null && !config.getPremiumSettings().getAuthenticationSteps().contains("REGISTER")) { + return createAccountWithRandomPassword(id, player).thenCompose(password -> { + player.sendMessage(config.getServerMessages().getMessage("autogenerated-password", + MessageContext.of("%password%", password))); + return handleLogin(player); + }); + } + return handleLogin(player); + } + + if (account == null) { + handlePremiumNickChange(player, premiumAccount); + return handleLogin(player); + } + + if (!premiumAccount.getName().equals(account.getName())) { + return handleProfileConflict(player, account, premiumAccount).thenCompose(a -> handleLogin(player)); + } + + return handleLogin(player); + }); + }); + } + + return handleLogin(player); + } + + private CompletableFuture handleLogin(ServerPlayer player) { + String nickname = player.getNickname(); String id = config.getActiveIdentifierType().getId(player); return accountDatabase.getAccount(id).thenCompose(account -> { if (config.isNameCaseCheckEnabled() && account != null && !account.getName().equals(nickname)) { @@ -59,7 +187,7 @@ public CompletableFuture onLogin(ServerPlayer player) { if (account == null) { Account newAccount = accountFactory.createAccount(id, config.getActiveIdentifierType(), player.getUniqueId(), nickname, - config.getActiveHashType(), null, player.getPlayerIp()); + config.getActiveHashType(), null, player.getPlayerIp(), player.isOnlineMode()); AuthenticationStepContext context = plugin.getAuthenticationContextFactoryBucket().createContext(newAccount); plugin.getAuthenticatingAccountBucket().addAuthenticatingAccount(newAccount); @@ -103,6 +231,59 @@ public CompletableFuture onLogin(ServerPlayer player) { }); } + private CompletableFuture createAccountWithRandomPassword(String id, ServerPlayer player) { + Account account = accountFactory.createAccount(id, config.getActiveIdentifierType(), player.getUniqueId(), + player.getNickname(), config.getActiveHashType(), null, player.getPlayerIp(), player.isOnlineMode()); + + PasswordGenerator passwordGenerator = new PasswordGenerator.PasswordGeneratorBuilder() + .useDigits(true) + .useLower(true) + .useUpper(true) + .build(); + String password = passwordGenerator.generate(12); + + return RegisterCommandImplementation.setAccountPassword(account, password).thenApply(a -> password); + } + + private void handlePremiumNickChange(ServerPlayer player, Account account) { + account.setName(player.getNickname()); + accountDatabase.saveOrUpdateAccount(account); + } + + private CompletableFuture handleProfileConflict(ServerPlayer player, Account crackedAccount, Account premiumAccount) { + switch (config.getPremiumSettings().getProfileConflictResolutionStrategy()) { + case BLOCK: + player.disconnect(config.getServerMessages().getMessage("profile-conflict-blocked")); + break; + case USE_OFFLINE: + break; + case OVERWRITE: + return accountDatabase.deleteAccount(crackedAccount.getPlayerId()).thenCompose(v -> { + premiumAccount.setName(player.getNickname()); + return accountDatabase.saveOrUpdateAccount(premiumAccount); + }); + } + + return CompletableFuture.completedFuture(crackedAccount); + } + + @Nullable + private ServerComponent validateNickname(String nickname) { + if (nickname.length() < config.getNameMinLength()) { + return config.getServerMessages().getMessage("name-too-short"); + } + + if (nickname.length() > config.getNameMaxLength()) { + return config.getServerMessages().getMessage("name-too-long"); + } + + if (!config.getNamePattern().matcher(nickname).matches()) { + return config.getServerMessages().getMessage("illegal-name-chars"); + } + + return null; + } + @Override public void onDisconnect(ServerPlayer player) { String id = config.getActiveIdentifierType().getId(player); diff --git a/core/src/main/java/me/mastercapexd/auth/messenger/commands/AccountEnterAcceptCommand.java b/core/src/main/java/me/mastercapexd/auth/messenger/commands/AccountEnterAcceptCommand.java index ff13a9a1..26ba69a2 100644 --- a/core/src/main/java/me/mastercapexd/auth/messenger/commands/AccountEnterAcceptCommand.java +++ b/core/src/main/java/me/mastercapexd/auth/messenger/commands/AccountEnterAcceptCommand.java @@ -56,7 +56,10 @@ public void onAccept(LinkCommandActorWrapper actorWrapper, LinkType linkType, @D Account account = entryUser.getAccount(); account.getPlayer().ifPresent(player -> player.sendMessage(linkType.getServerMessages().getStringMessage("enter-confirmed", linkType.newMessageContext(account)))); - account.nextAuthenticationStep(plugin.getAuthenticationContextFactoryBucket().createContext(account)); + account.nextAuthenticationStep(account.isPremium() ? + plugin.getPremiumAuthenticationContextFactoryBucket().createContext(account) : + plugin.getAuthenticationContextFactoryBucket().createContext(account) + ); plugin.getLinkEntryBucket().modifiable().remove(entryUser); actorWrapper.reply(linkType.getLinkMessages().getMessage("enter-accepted", linkType.newMessageContext(account))); diff --git a/core/src/main/java/me/mastercapexd/auth/messenger/commands/GoogleCodeCommand.java b/core/src/main/java/me/mastercapexd/auth/messenger/commands/GoogleCodeCommand.java index 51e2d6f0..469984d8 100644 --- a/core/src/main/java/me/mastercapexd/auth/messenger/commands/GoogleCodeCommand.java +++ b/core/src/main/java/me/mastercapexd/auth/messenger/commands/GoogleCodeCommand.java @@ -46,7 +46,10 @@ public void googleCode(LinkCommandActorWrapper actorWrapper, LinkType linkType, if (plugin.getGoogleAuthenticator().authorize(linkUser.getLinkUserInfo().getIdentificator().asString(), code)) { actorWrapper.reply(linkType.getLinkMessages().getStringMessage("google-code-valid", linkType.newMessageContext(account))); account.getCurrentAuthenticationStep().getAuthenticationStepContext().setCanPassToNextStep(true); - account.nextAuthenticationStep(plugin.getAuthenticationContextFactoryBucket().createContext(account)); + account.nextAuthenticationStep(account.isPremium() ? + plugin.getPremiumAuthenticationContextFactoryBucket().createContext(account) : + plugin.getAuthenticationContextFactoryBucket().createContext(account) + ); return; } actorWrapper.reply(linkType.getLinkMessages().getStringMessage("google-code-not-valid", linkType.newMessageContext(account))); diff --git a/core/src/main/java/me/mastercapexd/auth/premium/BasePremiumProvider.java b/core/src/main/java/me/mastercapexd/auth/premium/BasePremiumProvider.java new file mode 100644 index 00000000..1fc94095 --- /dev/null +++ b/core/src/main/java/me/mastercapexd/auth/premium/BasePremiumProvider.java @@ -0,0 +1,151 @@ +package me.mastercapexd.auth.premium; + +import com.bivashy.auth.api.AuthPlugin; +import com.bivashy.auth.api.premium.PremiumException; +import com.bivashy.auth.api.premium.PremiumProvider; +import com.bivashy.auth.api.util.ThrowableFunction; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.google.gson.JsonObject; +import org.jetbrains.annotations.Nullable; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.math.BigInteger; +import java.net.HttpURLConnection; +import java.net.SocketTimeoutException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +public class BasePremiumProvider implements PremiumProvider { + private final Cache userCache; + private final List> fetchers; + private final AuthPlugin plugin; + + public BasePremiumProvider(AuthPlugin plugin) { + this.plugin = plugin; + userCache = Caffeine.newBuilder() + .expireAfterWrite(10, TimeUnit.MINUTES) + .build(); + + fetchers = new ArrayList<>(2); + + fetchers.add(this::getPremiumUUIDFromMojang); + fetchers.add(this::getPremiumUUIDFromPlayerDB); + } + + @Override + public UUID getPremiumUUIDForName(String name) throws PremiumException { + name = name.toLowerCase(); + + PremiumException[] ex = new PremiumException[1]; + + UUID result = userCache.get(name, x -> { + for (int i = 0; i < fetchers.size(); i++) { + ThrowableFunction fetcher = fetchers.get(i); + + try { + return fetcher.apply(x); + } catch (PremiumException e) { + if (i == 0 && e.getIssue() == PremiumException.Issue.UNDEFINED) { + ex[0] = e; + break; + } + + if (i == fetchers.size() - 1) { + ex[0] = e; + } + } catch (RuntimeException e) { + e.printStackTrace(); + if (i == fetchers.size() - 1) { + ex[0] = new PremiumException(PremiumException.Issue.UNDEFINED, e); + } + } + } + return null; + }); + + if (ex[0] != null) { + throw ex[0]; + } + + return result; + } + + @Nullable + private UUID getPremiumUUIDFromMojang(String name) throws PremiumException { + try { + HttpURLConnection connection = (HttpURLConnection) new URL("https://api.mojang.com/users/profiles/minecraft/" + name).openConnection(); + connection.setConnectTimeout(5000); + connection.setReadTimeout(5000); + + switch (connection.getResponseCode()) { + case 429: + throw new PremiumException(PremiumException.Issue.THROTTLED, readInput(connection.getErrorStream())); + case 204: + case 404: + return null; + default: + throw new PremiumException(PremiumException.Issue.UNDEFINED, readInput(connection.getErrorStream())); + case 200: + JsonObject data = plugin.getGson().fromJson(new InputStreamReader(connection.getInputStream()), JsonObject.class); + + String id = data.get("id").getAsString(); + Object demo = data.get("demo"); + + return demo != null ? null : fromUnDashedUUID(id); + case 500: + throw new PremiumException(PremiumException.Issue.SERVER_EXCEPTION, readInput(connection.getErrorStream())); + } + } catch (SocketTimeoutException te) { + throw new PremiumException(PremiumException.Issue.THROTTLED, "Mojang API timed out"); + } catch (IOException e) { + throw new PremiumException(PremiumException.Issue.UNDEFINED, e); + } + } + + @Nullable + private UUID getPremiumUUIDFromPlayerDB(String name) throws PremiumException { + try { + HttpURLConnection connection = (HttpURLConnection) new URL("https://playerdb.co/api/player/minecraft/" + name).openConnection(); + connection.setConnectTimeout(5000); + connection.setReadTimeout(5000); + + switch (connection.getResponseCode()) { + case 200: + JsonObject data = plugin.getGson().fromJson(new InputStreamReader(connection.getInputStream()), JsonObject.class); + + String id = data.get("data").getAsJsonObject().get("player").getAsJsonObject().get("id").getAsString(); + + return UUID.fromString(id); + case 400: + return null; + default: + throw new PremiumException(PremiumException.Issue.UNDEFINED, readInput(connection.getErrorStream())); + } + } catch (IOException e) { + throw new PremiumException(PremiumException.Issue.SERVER_EXCEPTION, e); + } + } + + private static String readInput(InputStream inputStream) throws IOException { + return new BufferedReader( + new InputStreamReader(inputStream, StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining("\n")); + } + + private static UUID fromUnDashedUUID(String uuid) { + return uuid == null ? null : new UUID( + new BigInteger(uuid.substring(0, 16), 16).longValue(), + new BigInteger(uuid.substring(16, 32), 16).longValue() + ); + } +} diff --git a/core/src/main/java/me/mastercapexd/auth/server/commands/GoogleCodeCommand.java b/core/src/main/java/me/mastercapexd/auth/server/commands/GoogleCodeCommand.java index bf9459a3..ae9ad8da 100644 --- a/core/src/main/java/me/mastercapexd/auth/server/commands/GoogleCodeCommand.java +++ b/core/src/main/java/me/mastercapexd/auth/server/commands/GoogleCodeCommand.java @@ -49,7 +49,10 @@ public void googleCode(ServerPlayer player, Account account, Integer code) { if (plugin.getGoogleAuthenticator().authorize(linkUser.getLinkUserInfo().getIdentificator().asString(), code)) { player.sendMessage(GOOGLE_MESSAGES.getMessage("code-entered")); account.getCurrentAuthenticationStep().getAuthenticationStepContext().setCanPassToNextStep(true); - account.nextAuthenticationStep(plugin.getAuthenticationContextFactoryBucket().createContext(account)); + account.nextAuthenticationStep(account.isPremium() ? + plugin.getPremiumAuthenticationContextFactoryBucket().createContext(account) : + plugin.getAuthenticationContextFactoryBucket().createContext(account) + ); return; } player.sendMessage(GOOGLE_MESSAGES.getMessage("code-wrong-code")); diff --git a/core/src/main/java/me/mastercapexd/auth/server/commands/LogoutCommand.java b/core/src/main/java/me/mastercapexd/auth/server/commands/LogoutCommand.java index e9bec238..a4ee9fa7 100644 --- a/core/src/main/java/me/mastercapexd/auth/server/commands/LogoutCommand.java +++ b/core/src/main/java/me/mastercapexd/auth/server/commands/LogoutCommand.java @@ -31,13 +31,16 @@ public void logout(ServerPlayer player) { return; } eventBus.publish(PlayerLogoutEvent.class, player, false).thenAccept(result -> { - if(result.getEvent().isCancelled()) + if (result.getEvent().isCancelled()) return; accountStorage.getAccount(id).thenAccept(account -> { account.logout(config.getSessionDurability()); accountStorage.saveOrUpdateAccount(account); authenticatingAccountBucket.addAuthenticatingAccount(account); - account.nextAuthenticationStep(AuthPlugin.instance().getAuthenticationContextFactoryBucket().createContext(account)); + account.nextAuthenticationStep(account.isPremium() ? + AuthPlugin.instance().getPremiumAuthenticationContextFactoryBucket().createContext(account) : + AuthPlugin.instance().getAuthenticationContextFactoryBucket().createContext(account) + ); player.sendMessage(config.getServerMessages().getMessage("logout-success")); config.findServerInfo(config.getAuthServers()).asProxyServer().sendPlayer(player); }); diff --git a/core/src/main/java/me/mastercapexd/auth/server/commands/impl/LoginCommandImplementation.java b/core/src/main/java/me/mastercapexd/auth/server/commands/impl/LoginCommandImplementation.java index 6d49bf68..958fece6 100644 --- a/core/src/main/java/me/mastercapexd/auth/server/commands/impl/LoginCommandImplementation.java +++ b/core/src/main/java/me/mastercapexd/auth/server/commands/impl/LoginCommandImplementation.java @@ -32,7 +32,10 @@ public void performLogin(ServerPlayer player, Account account, String password) } currentAuthenticationStep.getAuthenticationStepContext().setCanPassToNextStep(true); - account.nextAuthenticationStep(plugin.getAuthenticationContextFactoryBucket().createContext(account)); + account.nextAuthenticationStep(account.isPremium() ? + plugin.getPremiumAuthenticationContextFactoryBucket().createContext(account) : + plugin.getAuthenticationContextFactoryBucket().createContext(account) + ); player.sendMessage(config.getServerMessages().getMessage("login-success")); }); } diff --git a/core/src/main/java/me/mastercapexd/auth/server/commands/impl/RegisterCommandImplementation.java b/core/src/main/java/me/mastercapexd/auth/server/commands/impl/RegisterCommandImplementation.java index 6ab6805e..2c11054b 100644 --- a/core/src/main/java/me/mastercapexd/auth/server/commands/impl/RegisterCommandImplementation.java +++ b/core/src/main/java/me/mastercapexd/auth/server/commands/impl/RegisterCommandImplementation.java @@ -10,6 +10,9 @@ import com.bivashy.auth.api.step.AuthenticationStep; import me.mastercapexd.auth.server.commands.annotations.AuthenticationAccount; import me.mastercapexd.auth.server.commands.parameters.RegisterPassword; +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.CompletableFuture; public class RegisterCommandImplementation { private final AuthPlugin plugin; @@ -29,15 +32,25 @@ public void performRegister(ServerPlayer player, @AuthenticationAccount Account return; currentAuthenticationStep.getAuthenticationStepContext().setCanPassToNextStep(true); - if (!account.getCryptoProvider().getIdentifier().equals(config.getActiveHashType().getIdentifier())) - account.setCryptoProvider(config.getActiveHashType()); - account.setPasswordHash(account.getCryptoProvider().hash(HashInput.of(password.getPassword()))); - - accountStorage.saveOrUpdateAccount(account); + setAccountPassword(account, password.getPassword()); - account.nextAuthenticationStep(plugin.getAuthenticationContextFactoryBucket().createContext(account)); + account.nextAuthenticationStep(account.isPremium() ? + plugin.getPremiumAuthenticationContextFactoryBucket().createContext(account) : + plugin.getAuthenticationContextFactoryBucket().createContext(account) + ); player.sendMessage(config.getServerMessages().getMessage("register-success")); }); } + + public static CompletableFuture setAccountPassword(Account account, String password) { + PluginConfig config = AuthPlugin.instance().getConfig(); + AccountDatabase accountStorage = AuthPlugin.instance().getAccountDatabase(); + + if (!account.getCryptoProvider().getIdentifier().equals(config.getActiveHashType().getIdentifier())) + account.setCryptoProvider(config.getActiveHashType()); + account.setPasswordHash(account.getCryptoProvider().hash(HashInput.of(password))); + + return accountStorage.saveOrUpdateAccount(account); + } } diff --git a/core/src/main/java/me/mastercapexd/auth/server/commands/parameters/ArgumentServerPlayer.java b/core/src/main/java/me/mastercapexd/auth/server/commands/parameters/ArgumentServerPlayer.java index 4bb70661..f0d06e33 100644 --- a/core/src/main/java/me/mastercapexd/auth/server/commands/parameters/ArgumentServerPlayer.java +++ b/core/src/main/java/me/mastercapexd/auth/server/commands/parameters/ArgumentServerPlayer.java @@ -59,6 +59,11 @@ public Optional getCurrentServer() { return player.getCurrentServer(); } + @Override + public boolean isOnlineMode() { + return player.isOnlineMode(); + } + @Override public T getRealPlayer() { return player.getRealPlayer(); diff --git a/core/src/main/resources/configurations/config.yml b/core/src/main/resources/configurations/config.yml index d07b1d1c..ffc614bb 100644 --- a/core/src/main/resources/configurations/config.yml +++ b/core/src/main/resources/configurations/config.yml @@ -135,7 +135,7 @@ fill-type: GRADUALLY # В секундах messages-delay: 5 -# Боссбар при регистрации/входе/подтвердения о входе +# Боссбар при регистрации/входе/подтвердения о входе boss-bar: use: false # Цвет боссбара @@ -163,6 +163,12 @@ id-type: NAME # Вы можете отключить эту опцию если используете id-type: UUID. check-name-case: true +# Минимальная длина ника +name-min-length: 3 +# Максимальная длина ника +# Учтите, что аккаунты Mojang могут иметь ники длиной максимум в 16 символов +# Установите на -1, чтобы отключить +name-max-length: 16 # RegEx паттерн разрешенных символом в нике игрока. name-regex-pattern: '[a-zA-Z0-9_]*' @@ -232,6 +238,32 @@ limbo-port: 49152-65535 google-authenticator: # Включена ли привязка к Google Authenticator, если вы отключите привязку игроки не смогут подтверждать вход, привязывать аккаунты. enabled: false + +# Поддержка игроков с премиум аккаунтом (т.е. игроков, которые купили Minecraft) +# Если включено, игроки с премиум аккаунтом будут быстрее проходить авторизацию. +premium-support: + # Включена ли поддержка премиум аккаунтов + enabled: true + # Не даёт войти на сервер оффлайн игрокам с никами, которые есть в базе данных Mojang. Помогает предотвратить конфликт аккаунтов(см ниже). + block-offline-players-with-premium-name: false + # Устанавливает стратегию разрешения конфликта премиум и оффлайн аккаунта. Не имеет смысла, если включена block-offline-players-with-premium-names + # Пример: Премиум игрок с ником krasni решил сменить свой ник на krasny, а на сервере уже есть оффлайн игрок с таким ником. + # BLOCK - Кикает премиум игрока. Админ должен разрешить эту ситуацию сам. + # USE_OFFLINE - Использует аккаунт оффлайн игрока. Премиум игрок не сможет войти, если только он не знает пароль оффлайн игрока. + # OVERWRITE - Удаляет оффлайн аккаунт, меняет ник онлайн аккаунта на новый. + profile-conflict-resolution-strategy: BLOCK + # Пути авторизации премиум игрока. + # REGISTER - Регистрация игрока. Если отсутствует, игроку будет выдан случайный пароль. Если игрок зарегистрирован, то данный шаг пропускается + # LOGIN - Шаг авторизации + # VK_LINK - Подтверждение входа в VK. Если в конфиге вк выключен или у игрока не привязан вк или он отключил привязку, данный шаг пропускается + # TELEGRAM_LINK - Подтверждение входа в Telegram. Если в конфиге вк выключен или у игрока не привязан вк или он отключил привязку, данный шаг пропускается + # DISCORD_LINK - Подтверждение входа в Discord. Если в конфиге вк выключен или у игрока не привязан вк или он отключил привязку, данный шаг пропускается + # GOOGLE_LINK - Ввод кода гугла. Если в конфиге гугл выключен или у игрока нету гугла данный шаг пропускается + # ENTER_SERVER - Вход в сервер + # ENTER_AUTH_SERVER - Вход в сервер авторизации. Требуется для лимбо. ВНИМАНИЕ, ЕСЛИ ИГРОКА ПЕРЕКИДЫВАЕТ НА ДРУГОЙ СЕРВЕР, ИСПОЛЬЗУЙТЕ blocked-servers + authentication-steps: + - ENTER_SERVER + messages: # PLAIN - Обычный текст без цветовых кодов # GSON - Текст в новом формате JSON @@ -251,6 +283,8 @@ messages: # Ошибки time-left: '&cВремя вышло! Пожалуйста, попробуйте снова.' limit-ip-reached: '&cВы находитесь онлайн с большим количеством аккаунтов!' + name-too-short: '&cВаш никнейм слишком короткий!' + name-too-long: '&cВаш никнейм слишком длинный!' illegal-name-chars: '&cВ вашем никнейме содержатся недопустимые символы!' attempts-limit: '&cВы достигли лимита ввода паролей!' disabled-command: '&cЭта команда отключена до тех пор пока вы не авторизуетесь!' @@ -261,6 +295,10 @@ messages: already-logged-out: '&cВы не авторизованы.' account-exists: '&cТакой аккаунт уже существует. Пожалуйста, войдите.' account-not-found: '&cАккаунт не найден! Пожалуйста, зарегистрируйтесь.' + premium-server-throttled: '&cСоединение с серверами Mojang сброшено. Пожалуйста, перезайдите позже.' + premium-server-unknown-error: '&cОшибка подключения к серверам Mojang. Сообщите администраторам.' + offline-player-with-premium-name-blocked: '&cВаш ник уже занят лицензионным аккаунтом. Попробуйте сменить его.' + profile-conflict-blocked: '&cПохоже, с последнего захода на сервер вы сменили свой ник, и к сожалению, этот ник уже занят. Сообщите администрации' # Авторизация force-connect-success: 'Игрок успешно подключен на сервер!' enter-password: '&cПожалуйста введите ваш пароль.' @@ -276,6 +314,7 @@ messages: register-success: '&aРегистрация прошла успешно.' login-success: '&aВы успешно авторизовались.' logout-success: '&aВы вышли из аккаунта' + autogenerated-password: '&aРегистрация была пропущена, так как вы зашли с лицензионного аккаунта. Ваш резервный пароль: %password%' # ВК vk: # Подтверждение входа через ВК diff --git a/pom.xml b/pom.xml index 152c4990..cde6f1b5 100644 --- a/pom.xml +++ b/pom.xml @@ -65,6 +65,7 @@ 1.3 2.0.0-SNAPSHOT 1.0.8 + 3.1.8 3.12.1 @@ -327,6 +328,11 @@ api ${nanolimbo.version} + + com.github.ben-manes.caffeine + caffeine + ${caffeine.version} + @@ -467,6 +473,10 @@ com.grack.nanojson ${dependencies.relocation.package}.com.grack.nanojson + + com.github.benmanes.caffeine + ${dependencies.relocation.package}.com.github.benmanes.caffeine + diff --git a/velocity/src/main/java/me/mastercapexd/auth/velocity/listener/AuthenticationListener.java b/velocity/src/main/java/me/mastercapexd/auth/velocity/listener/AuthenticationListener.java index 90b50532..78980138 100644 --- a/velocity/src/main/java/me/mastercapexd/auth/velocity/listener/AuthenticationListener.java +++ b/velocity/src/main/java/me/mastercapexd/auth/velocity/listener/AuthenticationListener.java @@ -4,17 +4,23 @@ import com.bivashy.auth.api.AuthPlugin; import com.bivashy.auth.api.event.PlayerChatPasswordEvent; +import com.bivashy.auth.api.server.message.AdventureServerComponent; +import com.bivashy.auth.api.server.message.SelfHandledServerComponent; +import com.bivashy.auth.api.server.message.ServerComponent; import com.bivashy.auth.api.server.player.ServerPlayer; +import com.velocitypowered.api.event.Continuation; import com.velocitypowered.api.event.Subscribe; import com.velocitypowered.api.event.command.CommandExecuteEvent; import com.velocitypowered.api.event.connection.DisconnectEvent; import com.velocitypowered.api.event.connection.PostLoginEvent; +import com.velocitypowered.api.event.connection.PreLoginEvent; import com.velocitypowered.api.event.player.PlayerChatEvent; import com.velocitypowered.api.event.player.ServerPreConnectEvent; import com.velocitypowered.api.event.player.ServerPreConnectEvent.ServerResult; import com.velocitypowered.api.proxy.server.RegisteredServer; import me.mastercapexd.auth.velocity.server.VelocityProxyServer; +import net.kyori.adventure.text.Component; public class AuthenticationListener { private final AuthPlugin plugin; @@ -23,6 +29,26 @@ public AuthenticationListener(AuthPlugin plugin) { this.plugin = plugin; } + @Subscribe + public void onPreLoginEvent(PreLoginEvent event, Continuation continuation) { + plugin.getLoginManagement().onPreLogin(event.getConnection().getRemoteAddress().getAddress().getHostAddress(), event.getUsername(), result -> { + PreLoginEvent.PreLoginComponentResult preLoginResult = switch (result.getState()) { + case DENIED -> { + ServerComponent component = result.getDisconnectMessage(); + Optional optionalComponent = component.safeAs(AdventureServerComponent.class).map(AdventureServerComponent::component); + if (optionalComponent.isEmpty()) + throw new UnsupportedOperationException("Cannot retrieve kyori Component from: " + component.getClass().getSimpleName() + ", " + component); + yield PreLoginEvent.PreLoginComponentResult.denied(optionalComponent.get()); + } + case FORCE_OFFLINE -> PreLoginEvent.PreLoginComponentResult.forceOfflineMode(); + case FORCE_ONLINE -> PreLoginEvent.PreLoginComponentResult.forceOnlineMode(); + }; + + event.setResult(preLoginResult); + continuation.resume(); + }); + } + @Subscribe public void onPostLoginEvent(PostLoginEvent event) { plugin.getCore().wrapPlayer(event.getPlayer()).ifPresent(plugin.getLoginManagement()::onLogin); diff --git a/velocity/src/main/java/me/mastercapexd/auth/velocity/player/VelocityServerPlayer.java b/velocity/src/main/java/me/mastercapexd/auth/velocity/player/VelocityServerPlayer.java index c8056ec8..a067cd7e 100644 --- a/velocity/src/main/java/me/mastercapexd/auth/velocity/player/VelocityServerPlayer.java +++ b/velocity/src/main/java/me/mastercapexd/auth/velocity/player/VelocityServerPlayer.java @@ -72,6 +72,11 @@ public Optional getCurrentServer() { return player.getCurrentServer().map(ServerConnection::getServer).map(VelocityProxyServer::new); } + @Override + public boolean isOnlineMode() { + return player.isOnlineMode(); + } + @Override public T getRealPlayer() { return (T) getPlayer();