diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/AccountHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/AccountHelper.java index aafe33b16..2cfcbf058 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/AccountHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/AccountHelper.java @@ -22,7 +22,7 @@ import javafx.scene.image.Image; import org.jackhuang.hmcl.Main; import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.yggdrasil.GameProfile; -import org.jackhuang.hmcl.auth.yggdrasil.ProfileTexture; +import org.jackhuang.hmcl.auth.yggdrasil.Texture; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; import org.jackhuang.hmcl.setting.Settings; import org.jackhuang.hmcl.task.FileDownloadTask; @@ -82,12 +82,9 @@ public final class AccountHelper { } public static Image getSkin(YggdrasilAccount account, double scaleRatio) { - if (account.getSelectedProfile() == null) + if (account.getCharacter() == null) return getDefaultSkin(account, scaleRatio); - String name = account.getSelectedProfile().getName(); - if (name == null) - return getDefaultSkin(account, scaleRatio); - File file = getSkinFile(name); + File file = getSkinFile(account.getCharacter()); if (file.exists()) { Image original = new Image("file:" + file.getAbsolutePath()); return new Image("file:" + file.getAbsolutePath(), @@ -142,23 +139,33 @@ public final class AccountHelper { @Override public void execute() throws Exception { - if (account.canLogIn() && (account.getSelectedProfile() == null || refresh)) + if (!account.isLoggedIn() && (account.getCharacter() == null || refresh)) DialogController.logIn(account); - downloadSkin(account, account.getSelectedProfile(), refresh, proxy); + downloadSkin(account, refresh, proxy); } } private static void downloadSkin(YggdrasilAccount account, GameProfile profile, boolean refresh, Proxy proxy) throws Exception { account.clearCache(); - if (profile == null) return; - String name = profile.getName(); - if (name == null) return; - Optional texture = account.getSkin(profile); + Optional texture = account.getSkin(profile); if (!texture.isPresent()) return; String url = texture.get().getUrl(); - File file = getSkinFile(name); + File file = getSkinFile(profile.getName()); + if (!refresh && file.exists()) + return; + new FileDownloadTask(NetworkUtils.toURL(url), file, proxy).run(); + } + + private static void downloadSkin(YggdrasilAccount account, boolean refresh, Proxy proxy) throws Exception { + account.clearCache(); + + if (account.getCharacter() == null) return; + Optional texture = account.getSkin(); + if (!texture.isPresent()) return; + String url = texture.get().getUrl(); + File file = getSkinFile(account.getCharacter()); if (!refresh && file.exists()) return; new FileDownloadTask(NetworkUtils.toURL(url), file, proxy).run(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java index 68436a289..a67f3d1d1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java @@ -20,11 +20,17 @@ package org.jackhuang.hmcl.game; import com.jfoenix.concurrency.JFXUtilities; import javafx.application.Platform; import org.jackhuang.hmcl.Main; -import org.jackhuang.hmcl.auth.*; +import org.jackhuang.hmcl.auth.Account; +import org.jackhuang.hmcl.auth.AuthInfo; +import org.jackhuang.hmcl.auth.AuthenticationException; +import org.jackhuang.hmcl.auth.ServerDisconnectException; import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.launch.*; import org.jackhuang.hmcl.mod.CurseCompletionTask; -import org.jackhuang.hmcl.setting.*; +import org.jackhuang.hmcl.setting.LauncherVisibility; +import org.jackhuang.hmcl.setting.Profile; +import org.jackhuang.hmcl.setting.Settings; +import org.jackhuang.hmcl.setting.VersionSetting; import org.jackhuang.hmcl.task.*; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.DialogController; @@ -89,7 +95,7 @@ public final class LauncherHelper { .then(Task.of(Main.i18n("account.methods"), variables -> { try { try { - variables.set("account", account.logIn(new SpecificCharacterSelector(Accounts.getCurrentCharacter(account)), Settings.INSTANCE.getProxy())); + variables.set("account", account.logIn()); } catch (ServerDisconnectException e) { if (account.canPlayOffline()) variables.set("account", account.playOffline()); @@ -302,7 +308,7 @@ public final class LauncherHelper { forbiddenTokens = Collections.emptyMap(); else forbiddenTokens = Lang.mapOf( - new Pair<>(authInfo.getAuthToken(), ""), + new Pair<>(authInfo.getAccessToken(), ""), new Pair<>(authInfo.getUserId(), ""), new Pair<>(authInfo.getUsername(), "") ); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java index a48f178b6..b4e31c246 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java @@ -21,9 +21,15 @@ import com.google.gson.JsonParseException; import org.jackhuang.hmcl.Main; import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.AccountFactory; -import org.jackhuang.hmcl.auth.OfflineAccount; -import org.jackhuang.hmcl.auth.OfflineAccountFactory; -import org.jackhuang.hmcl.auth.yggdrasil.*; +import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount; +import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccountFactory; +import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorBuildInfo; +import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServerResponse; +import org.jackhuang.hmcl.auth.offline.OfflineAccount; +import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory; +import org.jackhuang.hmcl.auth.yggdrasil.MojangYggdrasilProvider; +import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; +import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.util.*; @@ -32,7 +38,6 @@ import java.io.IOException; import java.net.URL; import java.util.HashMap; import java.util.Map; -import java.util.Optional; /** * @author huangyuhui @@ -46,7 +51,7 @@ public final class Accounts { public static final Map> ACCOUNT_FACTORY = Lang.mapOf( new Pair<>(OFFLINE_ACCOUNT_KEY, OfflineAccountFactory.INSTANCE), - new Pair<>(YGGDRASIL_ACCOUNT_KEY, new YggdrasilAccountFactory()), + new Pair<>(YGGDRASIL_ACCOUNT_KEY, new YggdrasilAccountFactory(MojangYggdrasilProvider.INSTANCE)), new Pair<>(AUTHLIB_INJECTOR_ACCOUNT_KEY, new AuthlibInjectorAccountFactory(Accounts::downloadAuthlibInjector)) ); @@ -59,26 +64,8 @@ public final class Accounts { else return YGGDRASIL_ACCOUNT_KEY; } - public static void setCurrentCharacter(Account account, String character) { - account.getProperties().put("character", character); - } - - public static boolean hasCurrentCharacter(Account account) { - return Lang.get(account.getProperties(), "character", String.class).isPresent(); - } - - public static String getCurrentCharacter(Account account) { - return Lang.get(account.getProperties(), "character", String.class) - .orElseThrow(() -> new IllegalArgumentException("Account " + account + " has not set current character.")); - } - - public static Optional getCurrentCharacter(Map storage) { - return Lang.get(storage, "properties", Map.class) - .flatMap(properties -> Lang.get(properties, "character", String.class)); - } - static String getAccountId(Account account) { - return getAccountId(account.getUsername(), getCurrentCharacter(account)); + return getAccountId(account.getUsername(), account.getCharacter()); } static String getAccountId(String username, String character) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.java index b35098556..3ff5bae75 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Settings.java @@ -26,7 +26,7 @@ import javafx.scene.text.Font; import org.jackhuang.hmcl.Main; import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.AccountFactory; -import org.jackhuang.hmcl.auth.yggdrasil.AuthlibInjectorAccount; +import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount; import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider; import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.download.MojangDownloadProvider; @@ -67,21 +67,23 @@ public class Settings { private Map accounts = new HashMap<>(); { - for (Map settings : SETTINGS.getAccounts()) { - Optional characterName = Accounts.getCurrentCharacter(settings); + loadProxy(); + + for (Iterator> iterator = SETTINGS.getAccounts().iterator(); iterator.hasNext(); ) { + Map settings = iterator.next(); AccountFactory factory = Accounts.ACCOUNT_FACTORY.get(Lang.get(settings, "type", String.class).orElse("")); - if (factory == null || !characterName.isPresent()) { + if (factory == null) { // unrecognized account type, so remove it. - SETTINGS.getAccounts().remove(settings); + iterator.remove(); continue; } Account account; try { - account = factory.fromStorage(settings); + account = factory.fromStorage(settings, getProxy()); } catch (Exception e) { - SETTINGS.getAccounts().remove(settings); // storage is malformed, delete. + iterator.remove(); continue; } @@ -101,8 +103,6 @@ public class Settings { } Lang.ignoringException(() -> Runtime.getRuntime().addShutdownHook(new Thread(this::save))); - - loadProxy(); } private Config initSettings() { @@ -468,8 +468,6 @@ public class Settings { * PROFILES * ****************************************/ - private Profile selectedProfile; - public Profile getSelectedProfile() { checkProfileMap(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AccountItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/AccountItem.java index 592ced84e..a26e529a6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AccountItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/AccountItem.java @@ -35,8 +35,8 @@ import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import org.jackhuang.hmcl.auth.Account; -import org.jackhuang.hmcl.auth.OfflineAccount; -import org.jackhuang.hmcl.auth.yggdrasil.AuthlibInjectorAccount; +import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount; +import org.jackhuang.hmcl.auth.offline.OfflineAccount; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; import org.jackhuang.hmcl.game.AccountHelper; import org.jackhuang.hmcl.setting.Accounts; @@ -82,7 +82,7 @@ public final class AccountItem extends StackPane { chkSelected.getProperties().put("account", account); setSelected(Settings.INSTANCE.getSelectedAccount() == account); - lblUser.setText(Accounts.getCurrentCharacter(account)); + lblUser.setText(account.getCharacter()); lblType.setText(AccountsPage.accountType(account)); lblEmail.setText(account.getUsername()); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AccountLoginPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/AccountLoginPane.java index 6d826b5e7..955b1b750 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AccountLoginPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/AccountLoginPane.java @@ -26,9 +26,6 @@ import javafx.scene.layout.StackPane; import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.AuthInfo; import org.jackhuang.hmcl.auth.NoSelectedCharacterException; -import org.jackhuang.hmcl.auth.SpecificCharacterSelector; -import org.jackhuang.hmcl.setting.Accounts; -import org.jackhuang.hmcl.setting.Settings; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; @@ -64,7 +61,7 @@ public class AccountLoginPane extends StackPane { lblCreationWarning.setText(""); Task.ofResult("login", () -> { try { - return oldAccount.logInWithPassword(new SpecificCharacterSelector(Accounts.getCurrentCharacter(oldAccount)), password, Settings.INSTANCE.getProxy()); + return oldAccount.logInWithPassword(password); } catch (Exception e) { return e; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AccountsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/AccountsPage.java index b66e0e1d9..34c9c31b9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AccountsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/AccountsPage.java @@ -36,7 +36,8 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.StackPane; import org.jackhuang.hmcl.Main; import org.jackhuang.hmcl.auth.*; -import org.jackhuang.hmcl.auth.yggdrasil.AuthlibInjectorAccount; +import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount; +import org.jackhuang.hmcl.auth.offline.OfflineAccount; import org.jackhuang.hmcl.auth.yggdrasil.GameProfile; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; import org.jackhuang.hmcl.game.AccountHelper; @@ -52,6 +53,7 @@ import org.jackhuang.hmcl.ui.wizard.DecoratorPage; import java.util.Collection; import java.util.LinkedList; import java.util.List; +import java.util.Optional; import java.util.concurrent.CountDownLatch; import java.util.stream.Collectors; @@ -153,6 +155,7 @@ public final class AccountsPage extends StackPane implements DecoratorPage { private void addNewAccount() { txtUsername.setText(""); txtPassword.setText(""); + lblCreationWarning.setText(""); dialog.show(); } @@ -166,20 +169,19 @@ public final class AccountsPage extends StackPane implements DecoratorPage { int type = cboType.getSelectionModel().getSelectedIndex(); String username = txtUsername.getText(); String password = txtPassword.getText(); + String apiRoot = Optional.ofNullable(cboServers.getSelectionModel().getSelectedItem()).map(TwoLineListItem::getSubtitle).orElse(null); progressBar.setVisible(true); lblCreationWarning.setText(""); Task.ofResult("create_account", () -> { - Account account; + AccountFactory factory; switch (type) { - case 0: account = Accounts.ACCOUNT_FACTORY.get(Accounts.OFFLINE_ACCOUNT_KEY).fromUsername(username); break; - case 1: account = Accounts.ACCOUNT_FACTORY.get(Accounts.YGGDRASIL_ACCOUNT_KEY).fromUsername(username, password); break; - case 2: account = Accounts.ACCOUNT_FACTORY.get(Accounts.AUTHLIB_INJECTOR_ACCOUNT_KEY).fromUsername(username, password, cboServers.getSelectionModel().getSelectedItem().getSubtitle()); break; + case 0: factory = Accounts.ACCOUNT_FACTORY.get(Accounts.OFFLINE_ACCOUNT_KEY); break; + case 1: factory = Accounts.ACCOUNT_FACTORY.get(Accounts.YGGDRASIL_ACCOUNT_KEY); break; + case 2: factory = Accounts.ACCOUNT_FACTORY.get(Accounts.AUTHLIB_INJECTOR_ACCOUNT_KEY); break; default: throw new Error(); } - AuthInfo info = account.logIn(new CharacterSelector(), Settings.INSTANCE.getProxy()); - Accounts.setCurrentCharacter(account, info.getUsername()); - return account; + return factory.create(new Selector(), username, password, apiRoot, Settings.INSTANCE.getProxy()); }).finalized(Schedulers.javafx(), variables -> { Settings.INSTANCE.addAccount(variables.get("create_account")); dialog.close(); @@ -236,7 +238,7 @@ public final class AccountsPage extends StackPane implements DecoratorPage { else throw new Error(Main.i18n("account.methods.no_method") + ": " + account); } - private static class CharacterSelector extends BorderPane implements MultiCharacterSelector { + private static class Selector extends BorderPane implements CharacterSelector { private final AdvancedListBox listBox = new AdvancedListBox(); private final JFXButton cancel = new JFXButton(); @@ -263,7 +265,7 @@ public final class AccountsPage extends StackPane implements DecoratorPage { @Override public GameProfile select(Account account, List names) throws NoSelectedCharacterException { if (!(account instanceof YggdrasilAccount)) - return MultiCharacterSelector.DEFAULT.select(account, names); + return CharacterSelector.DEFAULT.select(account, names); YggdrasilAccount yggdrasilAccount = (YggdrasilAccount) account; for (GameProfile profile : names) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AuthlibInjectorServerItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/AuthlibInjectorServerItem.java index 722f71738..dd327fd6e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AuthlibInjectorServerItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/AuthlibInjectorServerItem.java @@ -23,7 +23,7 @@ import javafx.geometry.Pos; import javafx.scene.control.Label; import javafx.scene.layout.BorderPane; import javafx.scene.layout.VBox; -import org.jackhuang.hmcl.auth.yggdrasil.AuthlibInjectorServerInfo; +import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServerInfo; import java.util.function.Consumer; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AuthlibInjectorServersPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/AuthlibInjectorServersPage.java index 592100871..f9e874077 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AuthlibInjectorServersPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/AuthlibInjectorServersPage.java @@ -10,7 +10,7 @@ import javafx.scene.control.ScrollPane; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import org.jackhuang.hmcl.Main; -import org.jackhuang.hmcl.auth.yggdrasil.AuthlibInjectorServerInfo; +import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServerInfo; import org.jackhuang.hmcl.setting.Accounts; import org.jackhuang.hmcl.setting.Settings; import org.jackhuang.hmcl.task.Schedulers; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/DialogController.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/DialogController.java index 2031be43e..97325326c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/DialogController.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/DialogController.java @@ -20,7 +20,6 @@ package org.jackhuang.hmcl.ui; import com.jfoenix.concurrency.JFXUtilities; import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.AuthInfo; -import org.jackhuang.hmcl.auth.MultiCharacterSelector; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; import org.jackhuang.hmcl.task.SilentException; @@ -48,6 +47,6 @@ public final class DialogController { latch.await(); return Optional.ofNullable(res.get()).orElseThrow(SilentException::new); } - return account.logIn(MultiCharacterSelector.DEFAULT); + return account.logIn(); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LeftPaneController.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LeftPaneController.java index 9463740ba..e93dfc4a3 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LeftPaneController.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LeftPaneController.java @@ -36,7 +36,6 @@ import org.jackhuang.hmcl.game.HMCLGameRepository; import org.jackhuang.hmcl.game.ModpackHelper; import org.jackhuang.hmcl.mod.Modpack; import org.jackhuang.hmcl.mod.UnsupportedModpackException; -import org.jackhuang.hmcl.setting.Accounts; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profiles; import org.jackhuang.hmcl.setting.Settings; @@ -90,7 +89,7 @@ public final class LeftPaneController { accountItem.setVersionName(Main.i18n("account.missing")); accountItem.setGameVersion(Main.i18n("message.unknown")); } else { - accountItem.setVersionName(Accounts.getCurrentCharacter(it)); + accountItem.setVersionName(it.getCharacter()); accountItem.setGameVersion(AccountsPage.accountType(it)); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/NumberValidator.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/NumberValidator.java index c22f0c48b..a621c0f14 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/NumberValidator.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/NumberValidator.java @@ -20,6 +20,7 @@ package org.jackhuang.hmcl.ui.construct; import com.jfoenix.validation.base.ValidatorBase; import javafx.beans.NamedArg; import javafx.scene.control.TextInputControl; +import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.StringUtils; public class NumberValidator extends ValidatorBase { @@ -51,11 +52,6 @@ public class NumberValidator extends ValidatorBase { if (StringUtils.isBlank(textField.getText())) hasErrors.set(!nullable); else - try { - Integer.parseInt(textField.getText()); - hasErrors.set(false); - } catch (NumberFormatException e) { - hasErrors.set(true); - } + hasErrors.set(Lang.toIntOrNull(textField.getText()) == null); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/Account.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/Account.java index fbbd350ff..9bc41f637 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/Account.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/Account.java @@ -17,8 +17,6 @@ */ package org.jackhuang.hmcl.auth; -import java.net.Proxy; -import java.util.HashMap; import java.util.Map; import java.util.UUID; @@ -33,6 +31,12 @@ public abstract class Account { */ public abstract String getUsername(); + /** + * + * @return the character name + */ + public abstract String getCharacter(); + /** * @return the UUID */ @@ -40,28 +44,12 @@ public abstract class Account { /** * log in. - * @param selector selects a character * @return the specific player's info. * @throws AuthenticationException if server error occurred. */ - public AuthInfo logIn(MultiCharacterSelector selector) throws AuthenticationException { - return logIn(selector, Proxy.NO_PROXY); - } + public abstract AuthInfo logIn() throws AuthenticationException; - /** - * log in. - * @param selector selects a character - * @param proxy by which connect to the server - * @return the specific player's info. - * @throws AuthenticationException if server error occurred. - */ - public abstract AuthInfo logIn(MultiCharacterSelector selector, Proxy proxy) throws AuthenticationException; - - public AuthInfo logInWithPassword(MultiCharacterSelector selector, String password) throws AuthenticationException { - return logInWithPassword(selector, password, Proxy.NO_PROXY); - } - - public abstract AuthInfo logInWithPassword(MultiCharacterSelector selector, String password, Proxy proxy) throws AuthenticationException; + public abstract AuthInfo logInWithPassword(String password) throws AuthenticationException; public abstract boolean canPlayOffline(); @@ -73,24 +61,7 @@ public abstract class Account { public abstract void logOut(); - protected abstract Map toStorageImpl(); - - public final Map toStorage() { - Map storage = toStorageImpl(); - if (!getProperties().isEmpty()) - storage.put("properties", getProperties()); - return storage; - } - - private final Map properties = new HashMap<>(); - - /** - * To save some necessary extra information here. - * @return the property map. - */ - public final Map getProperties() { - return properties; - } + public abstract Map toStorage(); public abstract void clearCache(); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/AccountFactory.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/AccountFactory.java index 5a52dc7ec..62f5fab78 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/AccountFactory.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/AccountFactory.java @@ -17,8 +17,7 @@ */ package org.jackhuang.hmcl.auth; -import org.jackhuang.hmcl.util.Lang; - +import java.net.Proxy; import java.util.Map; /** @@ -27,23 +26,7 @@ import java.util.Map; */ public abstract class AccountFactory { - public final T fromUsername(String username) { - return fromUsername(username, "", null); - } + public abstract T create(CharacterSelector selector, String username, String password, Object additionalData, Proxy proxy) throws AuthenticationException; - public final T fromUsername(String username, String password) { - return fromUsername(username, password, null); - } - - public abstract T fromUsername(String username, String password, Object additionalData); - - protected abstract T fromStorageImpl(Map storage); - - public final T fromStorage(Map storage) { - T account = fromStorageImpl(storage); - Map properties = Lang.get(storage, "properties", Map.class).orElse(null); - if (properties == null) return account; - account.getProperties().putAll(properties); - return account; - } + public abstract T fromStorage(Map storage, Proxy proxy); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/AuthInfo.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/AuthInfo.java index bea067300..e878ce602 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/AuthInfo.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/AuthInfo.java @@ -17,10 +17,8 @@ */ package org.jackhuang.hmcl.auth; -import org.jackhuang.hmcl.auth.yggdrasil.GameProfile; import org.jackhuang.hmcl.game.Arguments; import org.jackhuang.hmcl.util.Immutable; -import org.jackhuang.hmcl.util.UUIDTypeAdapter; /** * @@ -31,42 +29,32 @@ public final class AuthInfo { private final String username; private final String userId; - private final String authToken; + private final String accessToken; private final UserType userType; private final String userProperties; - private final String userPropertyMap; private final Arguments arguments; - public AuthInfo(String username, String userId, String authToken) { - this(username, userId, authToken, UserType.LEGACY); + public AuthInfo(String username, String userId, String accessToken) { + this(username, userId, accessToken, UserType.LEGACY); } - public AuthInfo(String username, String userId, String authToken, UserType userType) { - this(username, userId, authToken, userType, "{}"); + public AuthInfo(String username, String userId, String accessToken, UserType userType) { + this(username, userId, accessToken, userType, "{}"); } - public AuthInfo(String username, String userId, String authToken, UserType userType, String userProperties) { - this(username, userId, authToken, userType, userProperties, "{}"); + public AuthInfo(String username, String userId, String accessToken, UserType userType, String userProperties) { + this(username, userId, accessToken, userType, userProperties, null); } - public AuthInfo(String username, String userId, String authToken, UserType userType, String userProperties, String userPropertyMap) { - this(username, userId, authToken, userType, userProperties, userPropertyMap, null); - } - - public AuthInfo(String username, String userId, String authToken, UserType userType, String userProperties, String userPropertyMap, Arguments arguments) { + public AuthInfo(String username, String userId, String accessToken, UserType userType, String userProperties, Arguments arguments) { this.username = username; this.userId = userId; - this.authToken = authToken; + this.accessToken = accessToken; this.userType = userType; this.userProperties = userProperties; - this.userPropertyMap = userPropertyMap; this.arguments = arguments; } - public AuthInfo(GameProfile profile, String authToken, UserType userType, String userProperties) { - this(profile.getName(), UUIDTypeAdapter.fromUUID(profile.getId()), authToken, userType, userProperties); - } - public String getUsername() { return username; } @@ -75,8 +63,8 @@ public final class AuthInfo { return userId; } - public String getAuthToken() { - return authToken; + public String getAccessToken() { + return accessToken; } public UserType getUserType() { @@ -93,21 +81,11 @@ public final class AuthInfo { return userProperties; } - /** - * Properties of this user. - * Don't know the difference between user properties and user property map. - * - * @return the user property map in JSON. - */ - public String getUserPropertyMap() { - return userPropertyMap; - } - public Arguments getArguments() { return arguments; } public AuthInfo setArguments(Arguments arguments) { - return new AuthInfo(username, userId, authToken, userType, userProperties, userPropertyMap, arguments); + return new AuthInfo(username, userId, accessToken, userType, userProperties, arguments); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/MultiCharacterSelector.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/CharacterSelector.java similarity index 88% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/auth/MultiCharacterSelector.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/auth/CharacterSelector.java index 2495af28b..f5b4f46ff 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/MultiCharacterSelector.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/CharacterSelector.java @@ -25,7 +25,7 @@ import java.util.List; * This interface is for your application to open a GUI for user to choose the character * when a having-multi-character yggdrasil account is being logging in.. */ -public interface MultiCharacterSelector { +public interface CharacterSelector { /** * Select one of {@code names} GameProfiles to login. @@ -35,5 +35,5 @@ public interface MultiCharacterSelector { */ GameProfile select(Account account, List names) throws NoSelectedCharacterException; - MultiCharacterSelector DEFAULT = (account, names) -> names.stream().findFirst().orElseThrow(() -> new NoSelectedCharacterException(account)); + CharacterSelector DEFAULT = (account, names) -> names.stream().findFirst().orElseThrow(() -> new NoSelectedCharacterException(account)); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/InvalidCredentialsException.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/InvalidCredentialsException.java index 373e196a9..f443e287f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/InvalidCredentialsException.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/InvalidCredentialsException.java @@ -22,14 +22,4 @@ package org.jackhuang.hmcl.auth; * @author huangyuhui */ public final class InvalidCredentialsException extends AuthenticationException { - - private final Account account; - - public InvalidCredentialsException(Account account) { - this.account = account; - } - - public Account getAccount() { - return account; - } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/InvalidPasswordException.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/InvalidPasswordException.java index eaec730fa..ffc0d6356 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/InvalidPasswordException.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/InvalidPasswordException.java @@ -21,15 +21,4 @@ package org.jackhuang.hmcl.auth; * throws if wrong password. */ public class InvalidPasswordException extends AuthenticationException { - - private final Account account; - - public InvalidPasswordException(Account account) { - super(); - this.account = account; - } - - public Account getAccount() { - return account; - } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/InvalidTokenException.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/InvalidTokenException.java index 7cc06340e..8e95724ee 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/InvalidTokenException.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/InvalidTokenException.java @@ -17,22 +17,9 @@ */ package org.jackhuang.hmcl.auth; -import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; - /** * * @author huangyuhui */ public class InvalidTokenException extends AuthenticationException { - - private final YggdrasilAccount account; - - public InvalidTokenException(YggdrasilAccount account) { - super(); - this.account = account; - } - - public YggdrasilAccount getAccount() { - return account; - } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/NoSelectedCharacterException.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/NoSelectedCharacterException.java index 20ef569fb..c8d58c3a0 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/NoSelectedCharacterException.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/NoSelectedCharacterException.java @@ -18,10 +18,10 @@ package org.jackhuang.hmcl.auth; /** - * This exception gets threw when a monitor of {@link MultiCharacterSelector} cannot select a + * This exception gets threw when a monitor of {@link CharacterSelector} cannot select a * valid character. * - * @see org.jackhuang.hmcl.auth.MultiCharacterSelector + * @see CharacterSelector * @author huangyuhui */ public final class NoSelectedCharacterException extends AuthenticationException { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/ServerResponseMalformedException.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/ServerResponseMalformedException.java new file mode 100644 index 000000000..191df642b --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/ServerResponseMalformedException.java @@ -0,0 +1,18 @@ +package org.jackhuang.hmcl.auth; + +public class ServerResponseMalformedException extends AuthenticationException { + public ServerResponseMalformedException() { + } + + public ServerResponseMalformedException(String message) { + super(message); + } + + public ServerResponseMalformedException(String message, Throwable cause) { + super(message, cause); + } + + public ServerResponseMalformedException(Throwable cause) { + super(cause); + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/SpecificCharacterSelector.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/SpecificCharacterSelector.java index 081018eb2..dd75c6033 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/SpecificCharacterSelector.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/SpecificCharacterSelector.java @@ -24,7 +24,7 @@ import java.util.List; /** * Select character by name. */ -public class SpecificCharacterSelector implements MultiCharacterSelector { +public class SpecificCharacterSelector implements CharacterSelector { private final String id; /** diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/AuthlibInjectorAccount.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorAccount.java similarity index 57% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/AuthlibInjectorAccount.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorAccount.java index 06068d888..3f173e94b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/AuthlibInjectorAccount.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorAccount.java @@ -15,18 +15,20 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see {http://www.gnu.org/licenses/}. */ -package org.jackhuang.hmcl.auth.yggdrasil; +package org.jackhuang.hmcl.auth.authlibinjector; import org.jackhuang.hmcl.auth.AuthInfo; import org.jackhuang.hmcl.auth.AuthenticationException; -import org.jackhuang.hmcl.auth.MultiCharacterSelector; +import org.jackhuang.hmcl.auth.CharacterSelector; +import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; +import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService; +import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilSession; import org.jackhuang.hmcl.game.Arguments; import org.jackhuang.hmcl.task.GetTask; import org.jackhuang.hmcl.util.ExceptionalSupplier; import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.NetworkUtils; -import java.net.Proxy; import java.util.Base64; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; @@ -35,21 +37,30 @@ public class AuthlibInjectorAccount extends YggdrasilAccount { private final String serverBaseURL; private final ExceptionalSupplier injectorJarPath; - public AuthlibInjectorAccount(ExceptionalSupplier injectorJarPath, String serverBaseURL, String username) { - super(serverBaseURL + "authserver/", serverBaseURL + "sessionserver/", username); + protected AuthlibInjectorAccount(YggdrasilService service, String serverBaseURL, ExceptionalSupplier injectorJarPath, String username, String clientToken, String character, YggdrasilSession session) { + super(service, username, clientToken, character, session); this.injectorJarPath = injectorJarPath; this.serverBaseURL = serverBaseURL; } @Override - public AuthInfo logIn(MultiCharacterSelector selector, Proxy proxy) throws AuthenticationException { + public AuthInfo logIn() throws AuthenticationException { + return inject(super::logIn); + } + + @Override + protected AuthInfo logInWithPassword(String password, CharacterSelector selector) throws AuthenticationException { + return inject(() -> super.logInWithPassword(password, selector)); + } + + private AuthInfo inject(ExceptionalSupplier supplier) throws AuthenticationException { // Authlib Injector recommends launchers to pre-fetch the server basic information before launched the game to save time. GetTask getTask = new GetTask(NetworkUtils.toURL(serverBaseURL)); AtomicBoolean flag = new AtomicBoolean(true); Thread thread = Lang.thread(() -> flag.set(getTask.test())); - AuthInfo info = super.logIn(selector, proxy); + AuthInfo info = supplier.get(); try { thread.join(); @@ -66,32 +77,9 @@ public class AuthlibInjectorAccount extends YggdrasilAccount { } @Override - public AuthInfo logInWithPassword(MultiCharacterSelector selector, String password, Proxy proxy) throws AuthenticationException { - // Authlib Injector recommends launchers to pre-fetch the server basic information before launched the game to save time. - GetTask getTask = new GetTask(NetworkUtils.toURL(serverBaseURL)); - AtomicBoolean flag = new AtomicBoolean(true); - Thread thread = Lang.thread(() -> flag.set(getTask.test())); - - AuthInfo info = super.logInWithPassword(selector, password, proxy); - try { - thread.join(); - - String arg = "-javaagent:" + injectorJarPath.get() + "=" + serverBaseURL; - Arguments arguments = Arguments.addJVMArguments(null, arg); - - if (flag.get()) - arguments = Arguments.addJVMArguments(arguments, "-Dorg.to2mbn.authlibinjector.config.prefetched=" + new String(Base64.getEncoder().encode(getTask.getResult().getBytes()))); - - return info.setArguments(arguments); - } catch (Exception e) { - throw new AuthenticationException("Unable to get authlib injector jar path", e); - } - } - - @Override - public Map toStorageImpl() { - Map map = super.toStorageImpl(); - map.put(STORAGE_KEY_SERVER_BASE_URL, serverBaseURL); + public Map toStorage() { + Map map = super.toStorage(); + map.put("serverBaseURL", serverBaseURL); return map; } @@ -99,5 +87,4 @@ public class AuthlibInjectorAccount extends YggdrasilAccount { return serverBaseURL; } - public static final String STORAGE_KEY_SERVER_BASE_URL = "serverBaseURL"; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorAccountFactory.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorAccountFactory.java new file mode 100644 index 000000000..e3886dc05 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorAccountFactory.java @@ -0,0 +1,58 @@ +package org.jackhuang.hmcl.auth.authlibinjector; + +import org.jackhuang.hmcl.auth.AccountFactory; +import org.jackhuang.hmcl.auth.AuthenticationException; +import org.jackhuang.hmcl.auth.CharacterSelector; +import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService; +import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilSession; +import org.jackhuang.hmcl.util.ExceptionalSupplier; +import org.jackhuang.hmcl.util.Lang; +import org.jackhuang.hmcl.util.NetworkUtils; +import org.jackhuang.hmcl.util.UUIDTypeAdapter; + +import java.net.Proxy; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; + +public class AuthlibInjectorAccountFactory extends AccountFactory { + private final ExceptionalSupplier injectorJarPathSupplier; + + public AuthlibInjectorAccountFactory(ExceptionalSupplier injectorJarPathSupplier) { + this.injectorJarPathSupplier = injectorJarPathSupplier; + } + + @Override + public AuthlibInjectorAccount create(CharacterSelector selector, String username, String password, Object serverBaseURL, Proxy proxy) throws AuthenticationException { + Objects.requireNonNull(selector); + Objects.requireNonNull(username); + Objects.requireNonNull(password); + Objects.requireNonNull(proxy); + + if (!(serverBaseURL instanceof String) || !NetworkUtils.isURL((String) serverBaseURL)) + throw new IllegalArgumentException("Additional data should be server base url string for authlib injector accounts."); + + AuthlibInjectorAccount account = new AuthlibInjectorAccount(new YggdrasilService(new AuthlibInjectorProvider((String) serverBaseURL), proxy), + (String) serverBaseURL, injectorJarPathSupplier, username, UUIDTypeAdapter.fromUUID(UUID.randomUUID()), null, null); + account.logInWithPassword(password, selector); + return account; + } + + @Override + public AuthlibInjectorAccount fromStorage(Map storage, Proxy proxy) { + Objects.requireNonNull(storage); + Objects.requireNonNull(proxy); + + String username = Lang.get(storage, "username", String.class) + .orElseThrow(() -> new IllegalArgumentException("storage does not have username")); + String clientToken = Lang.get(storage, "clientToken", String.class) + .orElseThrow(() -> new IllegalArgumentException("storage does not have client token.")); + String character = Lang.get(storage, "clientToken", String.class) + .orElseThrow(() -> new IllegalArgumentException("storage does not have selected character name.")); + String apiRoot = Lang.get(storage, "serverBaseURL", String.class) + .orElseThrow(() -> new IllegalArgumentException("storage does not have API root.")); + + return new AuthlibInjectorAccount(new YggdrasilService(new AuthlibInjectorProvider(apiRoot), proxy), + apiRoot, injectorJarPathSupplier, username, clientToken, character, YggdrasilSession.fromStorage(storage)); + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/AuthlibInjectorBuildInfo.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorBuildInfo.java similarity index 97% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/AuthlibInjectorBuildInfo.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorBuildInfo.java index 56fbbe937..2fdac8fcf 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/AuthlibInjectorBuildInfo.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorBuildInfo.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see {http://www.gnu.org/licenses/}. */ -package org.jackhuang.hmcl.auth.yggdrasil; +package org.jackhuang.hmcl.auth.authlibinjector; import com.google.gson.JsonParseException; import org.jackhuang.hmcl.util.Immutable; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorProvider.java new file mode 100644 index 000000000..80d83aa1e --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorProvider.java @@ -0,0 +1,42 @@ +package org.jackhuang.hmcl.auth.authlibinjector; + +import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilProvider; +import org.jackhuang.hmcl.util.NetworkUtils; +import org.jackhuang.hmcl.util.UUIDTypeAdapter; + +import java.net.URL; +import java.util.UUID; + +public class AuthlibInjectorProvider implements YggdrasilProvider { + + private final String apiRoot; + + public AuthlibInjectorProvider(String apiRoot) { + this.apiRoot = apiRoot; + } + + @Override + public URL getAuthenticationURL() { + return NetworkUtils.toURL(apiRoot + "authserver/authenticate"); + } + + @Override + public URL getRefreshmentURL() { + return NetworkUtils.toURL(apiRoot + "authserver/refresh"); + } + + @Override + public URL getValidationURL() { + return NetworkUtils.toURL(apiRoot + "authserver/validate"); + } + + @Override + public URL getInvalidationURL() { + return NetworkUtils.toURL(apiRoot + "authserver/invalidate"); + } + + @Override + public URL getProfilePropertiesURL(UUID uuid) { + return NetworkUtils.toURL(apiRoot + "sessionserver/session/minecraft/profile/" + UUIDTypeAdapter.fromUUID(uuid)); + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/AuthlibInjectorServerInfo.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorServerInfo.java similarity index 95% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/AuthlibInjectorServerInfo.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorServerInfo.java index c9cba830d..41b5f7dcb 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/AuthlibInjectorServerInfo.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorServerInfo.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see {http://www.gnu.org/licenses/}. */ -package org.jackhuang.hmcl.auth.yggdrasil; +package org.jackhuang.hmcl.auth.authlibinjector; public class AuthlibInjectorServerInfo { private final String serverIp; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/AuthlibInjectorServerResponse.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorServerResponse.java similarity index 96% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/AuthlibInjectorServerResponse.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorServerResponse.java index 210333766..91e5faba0 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/AuthlibInjectorServerResponse.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorServerResponse.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see {http://www.gnu.org/licenses/}. */ -package org.jackhuang.hmcl.auth.yggdrasil; +package org.jackhuang.hmcl.auth.authlibinjector; public class AuthlibInjectorServerResponse { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/OfflineAccount.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccount.java similarity index 84% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/auth/OfflineAccount.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccount.java index cdcd416b1..1274b1733 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/OfflineAccount.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccount.java @@ -15,14 +15,16 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see {http://www.gnu.org/licenses/}. */ -package org.jackhuang.hmcl.auth; +package org.jackhuang.hmcl.auth.offline; +import org.jackhuang.hmcl.auth.Account; +import org.jackhuang.hmcl.auth.AuthInfo; +import org.jackhuang.hmcl.auth.AuthenticationException; import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.Pair; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.UUIDTypeAdapter; -import java.net.Proxy; import java.util.Map; import java.util.Objects; import java.util.UUID; @@ -58,7 +60,12 @@ public class OfflineAccount extends Account { } @Override - public AuthInfo logIn(MultiCharacterSelector selector, Proxy proxy) throws AuthenticationException { + public String getCharacter() { + return username; + } + + @Override + public AuthInfo logIn() throws AuthenticationException { if (StringUtils.isBlank(username) || StringUtils.isBlank(uuid)) throw new AuthenticationException("Username cannot be empty"); @@ -66,8 +73,8 @@ public class OfflineAccount extends Account { } @Override - public AuthInfo logInWithPassword(MultiCharacterSelector selector, String password, Proxy proxy) throws AuthenticationException { - return logIn(selector, proxy); + public AuthInfo logInWithPassword(String password) throws AuthenticationException { + return logIn(); } @Override @@ -86,7 +93,7 @@ public class OfflineAccount extends Account { } @Override - public Map toStorageImpl() { + public Map toStorage() { return Lang.mapOf( new Pair<>("uuid", uuid), new Pair<>("username", username) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/OfflineAccountFactory.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccountFactory.java similarity index 63% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/auth/OfflineAccountFactory.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccountFactory.java index 5d231d772..49b8dd47d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/OfflineAccountFactory.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/OfflineAccountFactory.java @@ -15,10 +15,14 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see {http://www.gnu.org/licenses/}. */ -package org.jackhuang.hmcl.auth; +package org.jackhuang.hmcl.auth.offline; +import org.jackhuang.hmcl.auth.AccountFactory; +import org.jackhuang.hmcl.auth.CharacterSelector; import org.jackhuang.hmcl.util.DigestUtils; +import org.jackhuang.hmcl.util.Lang; +import java.net.Proxy; import java.util.Map; /** @@ -32,21 +36,18 @@ public class OfflineAccountFactory extends AccountFactory { } @Override - public OfflineAccount fromUsername(String username, String password, Object additionalData) { + public OfflineAccount create(CharacterSelector selector, String username, String password, Object additionalData, Proxy proxy) { return new OfflineAccount(username, getUUIDFromUserName(username)); } @Override - public OfflineAccount fromStorageImpl(Map storage) { - Object username = storage.get("username"); - if (username == null || !(username instanceof String)) - throw new IllegalStateException("Offline account configuration malformed."); + public OfflineAccount fromStorage(Map storage, Proxy proxy) { + String username = Lang.get(storage, "username", String.class) + .orElseThrow(() -> new IllegalStateException("Offline account configuration malformed.")); + String uuid = Lang.get(storage, "uuid", String.class) + .orElse(getUUIDFromUserName(username)); - Object uuid = storage.get("uuid"); - if (uuid == null || !(uuid instanceof String)) - uuid = getUUIDFromUserName((String) username); - - return new OfflineAccount((String) username, (String) uuid); + return new OfflineAccount(username, uuid); } private static String getUUIDFromUserName(String username) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/AuthenticationRequest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/AuthenticationRequest.java deleted file mode 100644 index 221c368f7..000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/AuthenticationRequest.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Hello Minecraft! Launcher. - * Copyright (C) 2018 huangyuhui - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see {http://www.gnu.org/licenses/}. - */ -package org.jackhuang.hmcl.auth.yggdrasil; - -import org.jackhuang.hmcl.util.Lang; -import org.jackhuang.hmcl.util.Pair; - -import java.util.Map; - -/** - * - * @author huangyuhui - */ -public final class AuthenticationRequest { - - /** - * The user name of Minecraft account. - */ - private final String username; - - /** - * The password of Minecraft account. - */ - private final String password; - - /** - * The client token of this game. - */ - private final String clientToken; - - private final Map agent = Lang.mapOf( - new Pair<>("name", "minecraft"), - new Pair<>("version", 1)); - - private final boolean requestUser = true; - - public AuthenticationRequest(String username, String password, String clientToken) { - this.username = username; - this.password = password; - this.clientToken = clientToken; - } - - public String getUsername() { - return username; - } - - public String getClientToken() { - return clientToken; - } - - public Map getAgent() { - return agent; - } - - public boolean isRequestUser() { - return requestUser; - } - -} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/AuthenticationResponse.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/AuthenticationResponse.java index 46b006e6e..3b475a54d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/AuthenticationResponse.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/AuthenticationResponse.java @@ -21,30 +21,26 @@ package org.jackhuang.hmcl.auth.yggdrasil; * * @author huangyuhui */ -public final class AuthenticationResponse { +final class AuthenticationResponse extends ErrorResponse { private final String accessToken; private final String clientToken; private final GameProfile selectedProfile; private final GameProfile[] availableProfiles; private final User user; - private final String error; - private final String errorMessage; - private final String cause; public AuthenticationResponse() { this(null, null, null, null, null, null, null, null); } public AuthenticationResponse(String accessToken, String clientToken, GameProfile selectedProfile, GameProfile[] availableProfiles, User user, String error, String errorMessage, String cause) { + super(error, errorMessage, cause); + this.accessToken = accessToken; this.clientToken = clientToken; this.selectedProfile = selectedProfile; this.availableProfiles = availableProfiles; this.user = user; - this.error = error; - this.errorMessage = errorMessage; - this.cause = cause; } public String getAccessToken() { @@ -67,16 +63,4 @@ public final class AuthenticationResponse { return user; } - public String getError() { - return error; - } - - public String getErrorMessage() { - return errorMessage; - } - - public String getCause() { - return cause; - } - } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/AuthlibInjectorAccountFactory.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/AuthlibInjectorAccountFactory.java deleted file mode 100644 index 11ab4974b..000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/AuthlibInjectorAccountFactory.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.jackhuang.hmcl.auth.yggdrasil; - -import org.jackhuang.hmcl.auth.AccountFactory; -import org.jackhuang.hmcl.util.ExceptionalSupplier; -import org.jackhuang.hmcl.util.Lang; -import org.jackhuang.hmcl.util.NetworkUtils; -import org.jackhuang.hmcl.util.UUIDTypeAdapter; - -import java.util.List; -import java.util.Map; -import java.util.Optional; - -import static org.jackhuang.hmcl.auth.yggdrasil.AuthlibInjectorAccount.STORAGE_KEY_SERVER_BASE_URL; -import static org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount.*; - -public class AuthlibInjectorAccountFactory extends AccountFactory { - private final ExceptionalSupplier injectorJarPathSupplier; - - public AuthlibInjectorAccountFactory(ExceptionalSupplier injectorJarPathSupplier) { - this.injectorJarPathSupplier = injectorJarPathSupplier; - } - - @Override - public AuthlibInjectorAccount fromUsername(String username, String password, Object additionalData) { - if (!(additionalData instanceof String) || !NetworkUtils.isURL((String) additionalData)) - throw new IllegalArgumentException("Additional data should be server base url string for authlib injector accounts."); - - AuthlibInjectorAccount account = new AuthlibInjectorAccount(injectorJarPathSupplier, (String) additionalData, username); - account.setPassword(password); - return account; - } - - @Override - public AuthlibInjectorAccount fromStorageImpl(Map storage) { - String username = Lang.get(storage, STORAGE_KEY_USER_NAME, String.class) - .orElseThrow(() -> new IllegalArgumentException("storage does not have key " + STORAGE_KEY_USER_NAME)); - String serverBaseURL = Lang.get(storage, STORAGE_KEY_SERVER_BASE_URL, String.class) - .orElseThrow(() -> new IllegalArgumentException("storage does not have key " + STORAGE_KEY_SERVER_BASE_URL)); - - AuthlibInjectorAccount account = new AuthlibInjectorAccount(injectorJarPathSupplier, serverBaseURL, username); - account.setUserId(Lang.get(storage, STORAGE_KEY_USER_ID, String.class).orElse(username)); - account.setAccessToken(Lang.get(storage, STORAGE_KEY_ACCESS_TOKEN, String.class).orElse(null)); - account.setClientToken(Lang.get(storage, STORAGE_KEY_CLIENT_TOKEN, String.class) - .orElseThrow(() -> new IllegalArgumentException("storage does not have key " + STORAGE_KEY_CLIENT_TOKEN))); - - Lang.get(storage, STORAGE_KEY_USER_PROPERTIES, List.class) - .ifPresent(account.getUserProperties()::fromList); - Optional profileId = Lang.get(storage, STORAGE_KEY_PROFILE_ID, String.class); - Optional profileName = Lang.get(storage, STORAGE_KEY_PROFILE_NAME, String.class); - GameProfile profile = null; - if (profileId.isPresent() && profileName.isPresent()) { - profile = new GameProfile(UUIDTypeAdapter.fromString(profileId.get()), profileName.get()); - Lang.get(storage, STORAGE_KEY_PROFILE_PROPERTIES, List.class) - .ifPresent(profile.getProperties()::fromList); - } - account.setSelectedProfile(profile); - return account; - } -} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/ErrorResponse.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/ErrorResponse.java new file mode 100644 index 000000000..3327a486e --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/ErrorResponse.java @@ -0,0 +1,29 @@ +package org.jackhuang.hmcl.auth.yggdrasil; + +class ErrorResponse { + private final String error; + private final String errorMessage; + private final String cause; + + public ErrorResponse() { + this(null, null, null); + } + + public ErrorResponse(String error, String errorMessage, String cause) { + this.error = error; + this.errorMessage = errorMessage; + this.cause = cause; + } + + public String getError() { + return error; + } + + public String getErrorMessage() { + return errorMessage; + } + + public String getCause() { + return cause; + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/GameProfile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/GameProfile.java index 3a83c39a0..8ea3f4d45 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/GameProfile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/GameProfile.java @@ -18,6 +18,7 @@ package org.jackhuang.hmcl.auth.yggdrasil; import com.google.gson.*; +import org.jackhuang.hmcl.auth.UserType; import org.jackhuang.hmcl.util.Immutable; import java.lang.reflect.Type; @@ -40,7 +41,11 @@ public final class GameProfile { } public GameProfile(UUID id, String name) { - this(id, name, new PropertyMap(), false); + this(id, name, new PropertyMap()); + } + + public GameProfile(UUID id, String name, PropertyMap properties) { + this(id, name, properties, false); } public GameProfile(UUID id, String name, PropertyMap properties, boolean legacy) { @@ -66,6 +71,10 @@ public final class GameProfile { return legacy; } + public UserType getUserType() { + return UserType.fromLegacy(isLegacy()); + } + public static class Serializer implements JsonSerializer, JsonDeserializer { public static final Serializer INSTANCE = new Serializer(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/MojangYggdrasilProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/MojangYggdrasilProvider.java new file mode 100644 index 000000000..20e351fc3 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/MojangYggdrasilProvider.java @@ -0,0 +1,36 @@ +package org.jackhuang.hmcl.auth.yggdrasil; + +import org.jackhuang.hmcl.util.NetworkUtils; +import org.jackhuang.hmcl.util.UUIDTypeAdapter; + +import java.net.URL; +import java.util.UUID; + +public class MojangYggdrasilProvider implements YggdrasilProvider { + public static final MojangYggdrasilProvider INSTANCE = new MojangYggdrasilProvider(); + + @Override + public URL getAuthenticationURL() { + return NetworkUtils.toURL("https://authserver.mojang.com/authenticate"); + } + + @Override + public URL getRefreshmentURL() { + return NetworkUtils.toURL("https://authserver.mojang.com/refresh"); + } + + @Override + public URL getValidationURL() { + return NetworkUtils.toURL("https://authserver.mojang.com/validate"); + } + + @Override + public URL getInvalidationURL() { + return NetworkUtils.toURL("https://authserver.mojang.com/invalidate"); + } + + @Override + public URL getProfilePropertiesURL(UUID uuid) { + return NetworkUtils.toURL("https://sessionserver.mojang.com/session/minecraft/profile/" + UUIDTypeAdapter.fromUUID(uuid)); + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/ProfileResponse.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/ProfileResponse.java deleted file mode 100644 index 1bb562f23..000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/ProfileResponse.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Hello Minecraft! Launcher. - * Copyright (C) 2018 huangyuhui - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see {http://www.gnu.org/licenses/}. - */ -package org.jackhuang.hmcl.auth.yggdrasil; - -public class ProfileResponse { - private final String id; - private final String name; - private final PropertyMap properties; - - public ProfileResponse() { - this("", "", null); - } - - public ProfileResponse(String id, String name, PropertyMap properties) { - this.id = id; - this.name = name; - this.properties = properties; - } - - public String getId() { - return id; - } - - public String getName() { - return name; - } - - public PropertyMap getProperties() { - return properties; - } -} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/Property.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/Property.java deleted file mode 100644 index b52c0b3db..000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/Property.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Hello Minecraft! Launcher. - * Copyright (C) 2018 huangyuhui - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see {http://www.gnu.org/licenses/}. - */ -package org.jackhuang.hmcl.auth.yggdrasil; - -public class Property { - - private final String name; - private final String value; - - public Property(String name, String value) { - this.name = name; - this.value = value; - } - - public String getName() { - return name; - } - - public String getValue() { - return value; - } -} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/PropertyMap.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/PropertyMap.java index 1abdb4758..a69968dde 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/PropertyMap.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/PropertyMap.java @@ -18,33 +18,20 @@ package org.jackhuang.hmcl.auth.yggdrasil; import com.google.gson.*; -import org.jackhuang.hmcl.util.Lang; import java.lang.reflect.Type; -import java.util.*; +import java.util.HashMap; +import java.util.Map; -public final class PropertyMap extends HashMap { +public final class PropertyMap extends HashMap { - public List> toList() { - List> properties = new ArrayList<>(); - for (Property profileProperty : values()) { - Map property = new HashMap<>(); - property.put("name", profileProperty.getName()); - property.put("value", profileProperty.getValue()); - properties.add(property); - } - return properties; - } - - public void fromList(List list) { - for (Object propertyMap : list) { - if (!(propertyMap instanceof Map)) - continue; - Optional name = Lang.get((Map) propertyMap, "name", String.class); - Optional value = Lang.get((Map) propertyMap, "value", String.class); - if (name.isPresent() && value.isPresent()) - put(name.get(), new Property(name.get(), value.get())); + public static PropertyMap fromMap(Map map) { + PropertyMap propertyMap = new PropertyMap(); + for (Map.Entry entry : map.entrySet()) { + if (entry.getKey() instanceof String && entry.getValue() instanceof String) + propertyMap.put((String) entry.getKey(), (String) entry.getValue()); } + return propertyMap; } public static class Serializer implements JsonSerializer, JsonDeserializer { @@ -57,19 +44,11 @@ public final class PropertyMap extends HashMap { @Override public PropertyMap deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { PropertyMap result = new PropertyMap(); - if (json instanceof JsonObject) { - for (Map.Entry entry : ((JsonObject) json).entrySet()) - if (entry.getValue() instanceof JsonArray) - for (JsonElement element : (JsonArray) entry.getValue()) - result.put(entry.getKey(), new Property(entry.getKey(), element.getAsString())); - } else if ((json instanceof JsonArray)) - for (JsonElement element : (JsonArray) json) - if ((element instanceof JsonObject)) { - JsonObject object = (JsonObject) element; - String name = object.getAsJsonPrimitive("name").getAsString(); - String value = object.getAsJsonPrimitive("value").getAsString(); - result.put(name, new Property(name, value)); - } + for (JsonElement element : json.getAsJsonArray()) + if (element instanceof JsonObject) { + JsonObject object = (JsonObject) element; + result.put(object.get("name").getAsString(), object.get("value").getAsString()); + } return result; } @@ -77,29 +56,14 @@ public final class PropertyMap extends HashMap { @Override public JsonElement serialize(PropertyMap src, Type typeOfSrc, JsonSerializationContext context) { JsonArray result = new JsonArray(); - for (Property property : src.values()) { + for (Map.Entry entry : src.entrySet()) { JsonObject object = new JsonObject(); - object.addProperty("name", property.getName()); - object.addProperty("value", property.getValue()); + object.addProperty("name", entry.getKey()); + object.addProperty("value", entry.getValue()); result.add(object); } return result; } } - - public static class LegacySerializer - implements JsonSerializer { - - @Override - public JsonElement serialize(PropertyMap src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject result = new JsonObject(); - for (PropertyMap.Entry entry : src.entrySet()) { - JsonArray values = new JsonArray(); - values.add(new JsonPrimitive(entry.getValue().getValue())); - result.add(entry.getKey(), values); - } - return result; - } - } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/RefreshRequest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/RefreshRequest.java deleted file mode 100644 index 297dc3341..000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/RefreshRequest.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Hello Minecraft! Launcher. - * Copyright (C) 2018 huangyuhui - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see {http://www.gnu.org/licenses/}. - */ -package org.jackhuang.hmcl.auth.yggdrasil; - -/** - * - * @author huang - */ -public final class RefreshRequest { - - private final String accessToken; - private final String clientToken; - private final GameProfile selectedProfile; - private final boolean requestUser; - - public RefreshRequest(String accessToken, String clientToken) { - this(accessToken, clientToken, null); - } - - public RefreshRequest(String accessToken, String clientToken, GameProfile selectedProfile) { - this(accessToken, clientToken, selectedProfile, true); - } - - public RefreshRequest(String accessToken, String clientToken, GameProfile selectedProfile, boolean requestUser) { - this.accessToken = accessToken; - this.clientToken = clientToken; - this.selectedProfile = selectedProfile; - this.requestUser = requestUser; - } - - public String getAccessToken() { - return accessToken; - } - - public String getClientToken() { - return clientToken; - } - - public GameProfile getSelectedProfile() { - return selectedProfile; - } - - public boolean isRequestUser() { - return requestUser; - } - -} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/RemoteAuthenticationException.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/RemoteAuthenticationException.java new file mode 100644 index 000000000..923271d48 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/RemoteAuthenticationException.java @@ -0,0 +1,40 @@ +package org.jackhuang.hmcl.auth.yggdrasil; + +import org.jackhuang.hmcl.auth.AuthenticationException; + +public class RemoteAuthenticationException extends AuthenticationException { + + private final String name; + private final String message; + private final String cause; + + public RemoteAuthenticationException(String name, String message, String cause) { + super(buildMessage(name, message, cause)); + this.name = name; + this.message = message; + this.cause = cause; + } + + public String getRemoteName() { + return name; + } + + public String getRemoteMessage() { + return message; + } + + public String getRemoteCause() { + return cause; + } + + private static String buildMessage(String name, String message, String cause) { + StringBuilder builder = new StringBuilder(name); + if (message != null) + builder.append(": ").append(message); + + if (cause != null) + builder.append(": ").append(cause); + + return builder.toString(); + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/ProfileTexture.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/Texture.java similarity index 87% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/ProfileTexture.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/Texture.java index e78705112..6b1492832 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/ProfileTexture.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/Texture.java @@ -22,16 +22,16 @@ import org.jackhuang.hmcl.util.Immutable; import java.util.Map; @Immutable -public final class ProfileTexture { +public final class Texture { private final String url; private final Map metadata; - public ProfileTexture() { + public Texture() { this(null, null); } - public ProfileTexture(String url, Map metadata) { + public Texture(String url, Map metadata) { this.url = url; this.metadata = metadata; } @@ -46,8 +46,4 @@ public final class ProfileTexture { else return metadata.get(key); } - - public enum Type { - SKIN, CAPE - } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/TextureResponse.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/TextureResponse.java index 6285d1dfe..4e383859b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/TextureResponse.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/TextureResponse.java @@ -27,13 +27,13 @@ import java.util.UUID; public final class TextureResponse { private final UUID profileId; private final String profileName; - private final Map textures; + private final Map textures; public TextureResponse() { this(UUID.randomUUID(), "", Collections.emptyMap()); } - public TextureResponse(UUID profileId, String profileName, Map textures) { + public TextureResponse(UUID profileId, String profileName, Map textures) { this.profileId = profileId; this.profileName = profileName; this.textures = textures; @@ -47,7 +47,7 @@ public final class TextureResponse { return profileName; } - public Map getTextures() { + public Map getTextures() { return textures == null ? null : Collections.unmodifiableMap(textures); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/TextureType.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/TextureType.java new file mode 100644 index 000000000..34be51015 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/TextureType.java @@ -0,0 +1,5 @@ +package org.jackhuang.hmcl.auth.yggdrasil; + +public enum TextureType { + SKIN, CAPE +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/ValidateRequest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/ValidateRequest.java deleted file mode 100644 index dc089c2ad..000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/ValidateRequest.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Hello Minecraft! Launcher. - * Copyright (C) 2018 huangyuhui - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see {http://www.gnu.org/licenses/}. - */ -package org.jackhuang.hmcl.auth.yggdrasil; - -/** - * - * @author huangyuhui - */ -public final class ValidateRequest { - - private final String accessToken; - private final String clientToken; - - public ValidateRequest(String accessToken, String clientToken) { - this.accessToken = accessToken; - this.clientToken = clientToken; - } - - public String getAccessToken() { - return accessToken; - } - - public String getClientToken() { - return clientToken; - } - -} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccount.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccount.java index 351718c62..9939d5479 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccount.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccount.java @@ -17,46 +17,34 @@ */ package org.jackhuang.hmcl.auth.yggdrasil; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonParseException; import org.jackhuang.hmcl.auth.*; -import org.jackhuang.hmcl.util.Charsets; -import org.jackhuang.hmcl.util.NetworkUtils; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.UUIDTypeAdapter; -import java.io.IOException; -import java.net.Proxy; -import java.net.URL; import java.util.*; /** * - * @author huang + * @author huangyuhui */ public class YggdrasilAccount extends Account { private final String username; - private String password; - private String userId; - private String accessToken = null; - private String clientToken = randomToken(); + private final YggdrasilService service; private boolean isOnline = false; - private PropertyMap userProperties = new PropertyMap(); - private GameProfile selectedProfile = null; - private GameProfile[] profiles; - private UserType userType = UserType.LEGACY; + private YggdrasilSession session; + private final String clientToken; + private String character; - public YggdrasilAccount(String baseAuthServer, String baseSessionServer, String username) { - this.baseAuthServer = baseAuthServer; - this.baseSessionServer = baseSessionServer; - this.baseProfile = baseSessionServer + "session/minecraft/profile/"; + protected YggdrasilAccount(YggdrasilService service, String username, String clientToken, String character, YggdrasilSession session) { + this.service = service; this.username = username; + this.session = session; + this.clientToken = clientToken; + this.character = character; - this.routeAuthenticate = NetworkUtils.toURL(baseAuthServer + "authenticate"); - this.routeRefresh = NetworkUtils.toURL(baseAuthServer + "refresh"); - this.routeValidate = NetworkUtils.toURL(baseAuthServer + "validate"); + if (session == null || session.getSelectedProfile() == null || StringUtils.isBlank(session.getAccessToken())) + this.session = null; } @Override @@ -64,146 +52,68 @@ public class YggdrasilAccount extends Account { return username; } - void setPassword(String password) { - this.password = password; - } - - public String getCurrentCharacterName() { - return userId; - } - - void setUserId(String userId) { - this.userId = userId; - } - - void setAccessToken(String accessToken) { - this.accessToken = accessToken; - } - - String getClientToken() { - return clientToken; - } - - void setClientToken(String clientToken) { - this.clientToken = clientToken; - } - - PropertyMap getUserProperties() { - return userProperties; - } - - public GameProfile getSelectedProfile() { - return selectedProfile; - } - - void setSelectedProfile(GameProfile selectedProfile) { - this.selectedProfile = selectedProfile; + @Override + public String getCharacter() { + return session.getSelectedProfile().getName(); } public boolean isLoggedIn() { - return StringUtils.isNotBlank(accessToken); + return session != null && StringUtils.isNotBlank(session.getAccessToken()); } public boolean canPlayOnline() { - return isLoggedIn() && selectedProfile != null && isOnline; - } - - public boolean canLogIn() { - return !canPlayOnline() && StringUtils.isNotBlank(username) - && (StringUtils.isNotBlank(password) || StringUtils.isNotBlank(accessToken)); + return isLoggedIn() && session.getSelectedProfile() != null && isOnline; } @Override - public AuthInfo logIn(MultiCharacterSelector selector, Proxy proxy) throws AuthenticationException { - if (canPlayOnline()) - return new AuthInfo(selectedProfile, accessToken, userType, GSON.toJson(userProperties)); - else { - logIn0(proxy); - if (!isLoggedIn()) - throw new AuthenticationException("Wrong password for account " + username); - - if (selectedProfile == null) { - if (profiles == null || profiles.length <= 0) - throw new NoCharacterException(this); - - selectedProfile = selector.select(this, Arrays.asList(profiles)); - } - return new AuthInfo(selectedProfile, accessToken, userType, GSON.toJson(userProperties)); + public AuthInfo logIn() throws AuthenticationException { + if (!canPlayOnline()) { + logInWithToken(); + selectProfile(new SpecificCharacterSelector(character)); } + return toAuthInfo(); } @Override - public AuthInfo logInWithPassword(MultiCharacterSelector selector, String password, Proxy proxy) throws AuthenticationException { - logInWithPassword0(password, proxy); - if (!isLoggedIn()) - throw new AuthenticationException("Wrong password for account " + username); + public final AuthInfo logInWithPassword(String password) throws AuthenticationException { + return logInWithPassword(password, new SpecificCharacterSelector(character)); + } - if (selectedProfile == null) { - if (profiles == null || profiles.length <= 0) + protected AuthInfo logInWithPassword(String password, CharacterSelector selector) throws AuthenticationException { + session = service.authenticate(username, password, clientToken); + selectProfile(selector); + return toAuthInfo(); + } + + private void selectProfile(CharacterSelector selector) throws AuthenticationException { + if (session.getSelectedProfile() == null) { + if (session.getAvailableProfiles() == null || session.getAvailableProfiles().length <= 0) throw new NoCharacterException(this); - selectedProfile = selector.select(this, Arrays.asList(profiles)); + session.setSelectedProfile(selector.select(this, Arrays.asList(session.getAvailableProfiles()))); } - return new AuthInfo(selectedProfile, accessToken, userType, GSON.toJson(userProperties)); + + character = session.getSelectedProfile().getName(); } - private void logIn0(Proxy proxy) throws AuthenticationException { - if (StringUtils.isNotBlank(accessToken)) { - logInWithToken(proxy); - } else if (StringUtils.isNotBlank(password)) - logInWithPassword0(password, proxy); - else - throw new AuthenticationException("Password cannot be blank"); - } - - private void logInWithToken(Proxy proxy) throws AuthenticationException { - if (StringUtils.isBlank(userId)) - if (StringUtils.isNotBlank(username)) - userId = username; - else - throw new AuthenticationException("Invalid uuid and username"); - if (checkTokenValidity(proxy)) { + private void logInWithToken() throws AuthenticationException { + if (service.validate(session.getAccessToken(), clientToken)) { isOnline = true; return; } - logIn1(routeRefresh, new RefreshRequest(accessToken, clientToken, getSelectedProfile()), proxy); + session = service.refresh(session.getAccessToken(), clientToken); } - public void logInWithPassword0(String password, Proxy proxy) throws AuthenticationException { - logIn1(routeAuthenticate, new AuthenticationRequest(username, password, clientToken), proxy); - } + private AuthInfo toAuthInfo() { + GameProfile profile = session.getSelectedProfile(); - private void logIn1(URL url, Object input, Proxy proxy) throws AuthenticationException { - AuthenticationResponse response = makeRequest(url, input, proxy) - .orElseThrow(() -> new AuthenticationException("Server response empty")); - - if (!clientToken.equals(response.getClientToken())) - throw new AuthenticationException("Client token changed"); - - if (response.getSelectedProfile() != null) - userType = UserType.fromLegacy(response.getSelectedProfile().isLegacy()); - else if (response.getAvailableProfiles() != null && response.getAvailableProfiles().length > 0) - userType = UserType.fromLegacy(response.getAvailableProfiles()[0].isLegacy()); - - User user = response.getUser(); - if (user == null || user.getId() == null) - userId = null; - else - userId = user.getId(); - - isOnline = true; - profiles = response.getAvailableProfiles(); - selectedProfile = response.getSelectedProfile(); - userProperties.clear(); - accessToken = response.getAccessToken(); - - if (user != null && user.getProperties() != null) - userProperties.putAll(user.getProperties()); + return new AuthInfo(profile.getName(), UUIDTypeAdapter.fromUUID(profile.getId()), session.getAccessToken(), profile.getUserType(), + YggdrasilService.GSON.toJson(Optional.ofNullable(session.getUser()).map(User::getProperties).orElseGet(PropertyMap::new))); } @Override public boolean canPlayOffline() { - return isLoggedIn() && getSelectedProfile() != null && !canPlayOnline(); + return isLoggedIn() && session.getSelectedProfile() != null && !canPlayOnline(); } @Override @@ -211,117 +121,51 @@ public class YggdrasilAccount extends Account { if (!canPlayOffline()) throw new IllegalStateException("Current account " + this + " cannot play offline."); - return new AuthInfo(selectedProfile, accessToken, userType, GSON.toJson(userProperties)); + return toAuthInfo(); } @Override public void logOut() { - password = null; - userId = null; - accessToken = null; isOnline = false; - userProperties.clear(); - profiles = null; - selectedProfile = null; + session = null; } @Override - public Map toStorageImpl() { - HashMap result = new HashMap<>(); + public Map toStorage() { + HashMap storage = new HashMap<>(); - result.put(STORAGE_KEY_USER_NAME, getUsername()); - result.put(STORAGE_KEY_CLIENT_TOKEN, getClientToken()); - if (getCurrentCharacterName() != null) - result.put(STORAGE_KEY_USER_ID, getCurrentCharacterName()); - if (!userProperties.isEmpty()) - result.put(STORAGE_KEY_USER_PROPERTIES, userProperties.toList()); - GameProfile profile = selectedProfile; - if (profile != null && profile.getName() != null && profile.getId() != null) { - result.put(STORAGE_KEY_PROFILE_NAME, profile.getName()); - result.put(STORAGE_KEY_PROFILE_ID, profile.getId()); + storage.put("username", getUsername()); + storage.put("clientToken", clientToken); + storage.put("character", character); + if (session != null) + storage.putAll(session.toStorage()); - if (!profile.getProperties().isEmpty()) - result.put(STORAGE_KEY_PROFILE_PROPERTIES, profile.getProperties().toList()); - } - - if (StringUtils.isNotBlank(accessToken)) - result.put(STORAGE_KEY_ACCESS_TOKEN, accessToken); - - return result; - } - - private Optional makeRequest(URL url, Object input, Proxy proxy) throws AuthenticationException { - try { - String jsonResult = input == null ? NetworkUtils.doGet(url, proxy) : NetworkUtils.doPost(url, GSON.toJson(input), "application/json", proxy); - AuthenticationResponse response = GSON.fromJson(jsonResult, AuthenticationResponse.class); - if (response == null) - return Optional.empty(); - if (!StringUtils.isBlank(response.getError())) { - if (response.getErrorMessage() != null) - if (response.getErrorMessage().contains("Invalid credentials")) - throw new InvalidCredentialsException(this); - else if (response.getErrorMessage().contains("Invalid token")) - throw new InvalidTokenException(this); - else if (response.getErrorMessage().contains("Invalid username or password")) - throw new InvalidPasswordException(this); - throw new AuthenticationException(response.getError() + ": " + response.getErrorMessage()); - } - - return Optional.of(response); - } catch (IOException e) { - throw new ServerDisconnectException(e); - } catch (JsonParseException e) { - throw new AuthenticationException("Unable to parse server response", e); - } - } - - private boolean checkTokenValidity(Proxy proxy) { - if (accessToken == null) - return false; - - try { - makeRequest(routeValidate, new ValidateRequest(accessToken, clientToken), proxy); - return true; - } catch (AuthenticationException e) { - return false; - } + return storage; } public UUID getUUID() { - if (getSelectedProfile() == null) + if (session == null) return null; else - return getSelectedProfile().getId(); + return session.getSelectedProfile().getId(); } - public Optional getSkin(GameProfile profile) throws IOException, JsonParseException { - if (StringUtils.isBlank(userId)) - throw new IllegalStateException("Not logged in"); + public Optional getSkin() throws AuthenticationException { + return getSkin(session.getSelectedProfile()); + } - Property textureProperty; - - if (profile.getProperties().containsKey("textures")) - textureProperty = profile.getProperties().get("textures"); - else { - ProfileResponse response = GSON.fromJson(NetworkUtils.doGet(NetworkUtils.toURL(baseProfile + UUIDTypeAdapter.fromUUID(profile.getId()))), ProfileResponse.class); - if (response.getProperties() == null) return Optional.empty(); - textureProperty = response.getProperties().get("textures"); - if (textureProperty == null) return Optional.empty(); - profile.getProperties().putAll(response.getProperties()); + public Optional getSkin(GameProfile profile) throws AuthenticationException { + if (!service.getTextures(profile).isPresent()) { + session.setAvailableProfile(profile = service.getCompleteGameProfile(profile.getId())); } - TextureResponse texture; - String json = new String(Base64.getDecoder().decode(textureProperty.getValue()), Charsets.UTF_8); - texture = GSON.fromJson(json, TextureResponse.class); - if (texture == null || texture.getTextures() == null) - return Optional.empty(); - - return Optional.ofNullable(texture.getTextures().get(ProfileTexture.Type.SKIN)); + return service.getTextures(profile).map(map -> map.get(TextureType.SKIN)); } @Override public void clearCache() { - Optional.ofNullable(getSelectedProfile()) + Optional.ofNullable(session) + .map(YggdrasilSession::getSelectedProfile) .map(GameProfile::getProperties) .ifPresent(it -> it.remove("texture")); } @@ -331,30 +175,4 @@ public class YggdrasilAccount extends Account { return "YggdrasilAccount[username=" + getUsername() + "]"; } - private final String baseAuthServer; - private final String baseSessionServer; - private final String baseProfile; - private final URL routeAuthenticate; - private final URL routeRefresh; - private final URL routeValidate; - - static final String STORAGE_KEY_ACCESS_TOKEN = "accessToken"; - static final String STORAGE_KEY_PROFILE_NAME = "displayName"; - static final String STORAGE_KEY_PROFILE_ID = "uuid"; - static final String STORAGE_KEY_PROFILE_PROPERTIES = "profileProperties"; - static final String STORAGE_KEY_USER_NAME = "username"; - static final String STORAGE_KEY_USER_ID = "userid"; - static final String STORAGE_KEY_USER_PROPERTIES = "userProperties"; - static final String STORAGE_KEY_CLIENT_TOKEN = "clientToken"; - - public static String randomToken() { - return UUIDTypeAdapter.fromUUID(UUID.randomUUID()); - } - - private static final Gson GSON = new GsonBuilder() - .registerTypeAdapter(GameProfile.class, GameProfile.Serializer.INSTANCE) - .registerTypeAdapter(PropertyMap.class, PropertyMap.Serializer.INSTANCE) - .registerTypeAdapter(UUID.class, UUIDTypeAdapter.INSTANCE) - .create(); - } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccountFactory.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccountFactory.java index e31d5a401..9e324dad6 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccountFactory.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccountFactory.java @@ -18,14 +18,15 @@ package org.jackhuang.hmcl.auth.yggdrasil; import org.jackhuang.hmcl.auth.AccountFactory; +import org.jackhuang.hmcl.auth.AuthenticationException; +import org.jackhuang.hmcl.auth.CharacterSelector; import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.UUIDTypeAdapter; -import java.util.List; +import java.net.Proxy; import java.util.Map; -import java.util.Optional; - -import static org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount.*; +import java.util.Objects; +import java.util.UUID; /** * @@ -33,50 +34,40 @@ import static org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount.*; */ public class YggdrasilAccountFactory extends AccountFactory { - private final String baseAuthServer; - private final String baseSessionServer; + private final YggdrasilProvider provider; - public YggdrasilAccountFactory() { - this(MOJANG_AUTH_SERVER, MOJANG_SESSION_SERVER); - } - - public YggdrasilAccountFactory(String baseAuthServer, String baseSessionServer) { - this.baseAuthServer = baseAuthServer; - this.baseSessionServer = baseSessionServer; + public YggdrasilAccountFactory(YggdrasilProvider provider) { + this.provider = provider; } @Override - public YggdrasilAccount fromUsername(String username, String password, Object additionalData) { - YggdrasilAccount account = new YggdrasilAccount(MOJANG_AUTH_SERVER, MOJANG_SESSION_SERVER, username); - account.setPassword(password); + public YggdrasilAccount create(CharacterSelector selector, String username, String password, Object additionalData, Proxy proxy) throws AuthenticationException { + Objects.requireNonNull(selector); + Objects.requireNonNull(username); + Objects.requireNonNull(password); + Objects.requireNonNull(proxy); + + YggdrasilAccount account = new YggdrasilAccount(new YggdrasilService(provider, proxy), username, UUIDTypeAdapter.fromUUID(UUID.randomUUID()), null, null); + account.logInWithPassword(password, selector); return account; } @Override - public YggdrasilAccount fromStorageImpl(Map storage) { - String username = Lang.get(storage, STORAGE_KEY_USER_NAME, String.class) - .orElseThrow(() -> new IllegalArgumentException("storage does not have key " + STORAGE_KEY_USER_NAME)); + public YggdrasilAccount fromStorage(Map storage, Proxy proxy) { + Objects.requireNonNull(storage); + Objects.requireNonNull(proxy); - YggdrasilAccount account = new YggdrasilAccount(baseAuthServer, baseSessionServer, username); - account.setUserId(Lang.get(storage, STORAGE_KEY_USER_ID, String.class).orElse(username)); - account.setAccessToken(Lang.get(storage, STORAGE_KEY_ACCESS_TOKEN, String.class).orElse(null)); - account.setClientToken(Lang.get(storage, STORAGE_KEY_CLIENT_TOKEN, String.class) - .orElseThrow(() -> new IllegalArgumentException("storage does not have key " + STORAGE_KEY_CLIENT_TOKEN))); + String username = Lang.get(storage, "username", String.class) + .orElseThrow(() -> new IllegalArgumentException("storage does not have username")); + String clientToken = Lang.get(storage, "clientToken", String.class) + .orElseThrow(() -> new IllegalArgumentException("storage does not have client token.")); + String character = Lang.get(storage, "clientToken", String.class) + .orElseThrow(() -> new IllegalArgumentException("storage does not have selected character name.")); - Lang.get(storage, STORAGE_KEY_USER_PROPERTIES, List.class) - .ifPresent(account.getUserProperties()::fromList); - Optional profileId = Lang.get(storage, STORAGE_KEY_PROFILE_ID, String.class); - Optional profileName = Lang.get(storage, STORAGE_KEY_PROFILE_NAME, String.class); - GameProfile profile = null; - if (profileId.isPresent() && profileName.isPresent()) { - profile = new GameProfile(UUIDTypeAdapter.fromString(profileId.get()), profileName.get()); - Lang.get(storage, STORAGE_KEY_PROFILE_PROPERTIES, List.class) - .ifPresent(profile.getProperties()::fromList); - } - account.setSelectedProfile(profile); - return account; + return new YggdrasilAccount(new YggdrasilService(provider, proxy), username, clientToken, character, YggdrasilSession.fromStorage(storage)); } - private static final String MOJANG_AUTH_SERVER = "https://authserver.mojang.com/"; - private static final String MOJANG_SESSION_SERVER = "https://sessionserver.mojang.com/"; + public static String randomToken() { + return UUIDTypeAdapter.fromUUID(UUID.randomUUID()); + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilProvider.java new file mode 100644 index 000000000..a67583c84 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilProvider.java @@ -0,0 +1,21 @@ +package org.jackhuang.hmcl.auth.yggdrasil; + +import java.net.URL; +import java.util.UUID; + +/** + * @see http://wiki.vg + */ +public interface YggdrasilProvider { + + URL getAuthenticationURL(); + + URL getRefreshmentURL(); + + URL getValidationURL(); + + URL getInvalidationURL(); + + URL getProfilePropertiesURL(UUID uuid); + +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilService.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilService.java new file mode 100644 index 000000000..631d7d141 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilService.java @@ -0,0 +1,233 @@ +package org.jackhuang.hmcl.auth.yggdrasil; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonParseException; +import org.jackhuang.hmcl.auth.*; +import org.jackhuang.hmcl.util.*; + +import java.io.IOException; +import java.net.Proxy; +import java.net.URL; +import java.util.*; + +public class YggdrasilService { + + private final YggdrasilProvider provider; + private final Proxy proxy; + + public YggdrasilService(YggdrasilProvider provider) { + this(provider, Proxy.NO_PROXY); + } + + public YggdrasilService(YggdrasilProvider provider, Proxy proxy) { + this.provider = provider; + this.proxy = proxy; + } + + public YggdrasilSession authenticate(String username, String password, String clientToken) throws AuthenticationException { + Objects.requireNonNull(username); + Objects.requireNonNull(password); + Objects.requireNonNull(clientToken); + + Map request = new HashMap<>(); + request.put("agent", Lang.mapOf( + new Pair<>("name", "Minecraft"), + new Pair<>("version", 1) + )); + request.put("username", username); + request.put("password", password); + request.put("clientToken", clientToken); + request.put("requestUser", true); + + return handle(request(provider.getAuthenticationURL(), request), clientToken); + } + + public YggdrasilSession refresh(String accessToken, String clientToken) throws AuthenticationException { + Objects.requireNonNull(accessToken); + Objects.requireNonNull(clientToken); + + return handle(request(provider.getRefreshmentURL(), new RefreshRequest(accessToken, clientToken)), clientToken); + } + + public boolean validate(String accessToken) throws AuthenticationException { + return validate(accessToken, null); + } + + public boolean validate(String accessToken, String clientToken) throws AuthenticationException { + Objects.requireNonNull(accessToken); + + try { + requireEmpty(request(provider.getValidationURL(), new RefreshRequest(accessToken, clientToken))); + return true; + } catch (InvalidCredentialsException | InvalidTokenException e) { + return false; + } + } + + public void invalidate(String accessToken) throws AuthenticationException { + invalidate(accessToken, null); + } + + public void invalidate(String accessToken, String clientToken) throws AuthenticationException { + Objects.requireNonNull(accessToken); + + requireEmpty(request(provider.getInvalidationURL(), GSON.toJson(new RefreshRequest(accessToken, clientToken)))); + } + + /** + * Get complete game profile. + * + * Game profile provided from authentication is not complete (no skin data in properties). + * + * @param userId the userId that the character corresponding to. + * @return the complete game profile(filled with more properties), null if character corresponding to {@code userId} does not exist. + * @throws AuthenticationException if an I/O error occurred or server response malformed. + */ + public GameProfile getCompleteGameProfile(UUID userId) throws AuthenticationException { + Objects.requireNonNull(userId); + + ProfileResponse response = fromJson(request(provider.getProfilePropertiesURL(userId), null), ProfileResponse.class); + if (response == null) + return null; + + return new GameProfile(response.getId(), response.getName(), response.getProperties()); + } + + public Optional> getTextures(GameProfile profile) throws AuthenticationException { + Objects.requireNonNull(profile); + + return Optional.ofNullable(profile.getProperties()) + .map(properties -> properties.get("textures")) + .map(encodedTextures -> new String(Base64.getDecoder().decode(encodedTextures), Charsets.UTF_8)) + .map(Lang.liftFunction(textures -> fromJson(textures, TextureResponse.class))) + .map(TextureResponse::getTextures); + } + + private String request(URL url, Object input) throws AuthenticationException { + try { + if (input == null) + return NetworkUtils.doGet(url, proxy); + else + return NetworkUtils.doPost(url, input instanceof String ? (String) input : GSON.toJson(input), "application/json"); + } catch (IOException e) { + throw new ServerDisconnectException(e); + } + } + + private static YggdrasilSession handle(String responseText, String clientToken) throws AuthenticationException { + AuthenticationResponse response = fromJson(responseText, AuthenticationResponse.class); + handleErrorMessage(response); + + if (!clientToken.equals(response.getClientToken())) + throw new AuthenticationException("Client token changed from " + response.getClientToken() + " to " + clientToken); + + return new YggdrasilSession(response.getAccessToken(), response.getSelectedProfile(), response.getAvailableProfiles(), response.getUser()); + } + + private static void requireEmpty(String response) throws AuthenticationException { + if (StringUtils.isBlank(response)) + return; + + try { + handleErrorMessage(fromJson(response, ErrorResponse.class)); + } catch (JsonParseException e) { + throw new ServerResponseMalformedException(e); + } + } + + private static void handleErrorMessage(ErrorResponse response) throws AuthenticationException { + if (!StringUtils.isBlank(response.getError())) { + if (response.getErrorMessage() != null) + if (response.getErrorMessage().contains("Invalid credentials")) + throw new InvalidCredentialsException(); + else if (response.getErrorMessage().contains("Invalid token")) + throw new InvalidTokenException(); + else if (response.getErrorMessage().contains("Invalid username or password")) + throw new InvalidPasswordException(); + throw new RemoteAuthenticationException(response.getError(), response.getErrorMessage(), response.getCause()); + } + } + + private static T fromJson(String text, Class typeOfT) throws ServerResponseMalformedException { + try { + return GSON.fromJson(text, typeOfT); + } catch (JsonParseException e) { + throw new ServerResponseMalformedException(e); + } + } + + static final Gson GSON = new GsonBuilder() + .registerTypeAdapter(GameProfile.class, GameProfile.Serializer.INSTANCE) + .registerTypeAdapter(PropertyMap.class, PropertyMap.Serializer.INSTANCE) + .registerTypeAdapter(UUID.class, UUIDTypeAdapter.INSTANCE) + .create(); + + private static final class RefreshRequest { + + private final String accessToken; + private final String clientToken; + private final GameProfile selectedProfile; + private final boolean requestUser; + + public RefreshRequest(String accessToken, String clientToken) { + this(accessToken, clientToken, null); + } + + public RefreshRequest(String accessToken, String clientToken, GameProfile selectedProfile) { + this(accessToken, clientToken, selectedProfile, true); + } + + public RefreshRequest(String accessToken, String clientToken, GameProfile selectedProfile, boolean requestUser) { + this.accessToken = accessToken; + this.clientToken = clientToken; + this.selectedProfile = selectedProfile; + this.requestUser = requestUser; + } + + public String getAccessToken() { + return accessToken; + } + + public String getClientToken() { + return clientToken; + } + + public GameProfile getSelectedProfile() { + return selectedProfile; + } + + public boolean isRequestUser() { + return requestUser; + } + + } + + private static final class ProfileResponse { + private final UUID id; + private final String name; + private final PropertyMap properties; + + public ProfileResponse() { + this(UUID.randomUUID(), "", null); + } + + public ProfileResponse(UUID id, String name, PropertyMap properties) { + this.id = id; + this.name = name; + this.properties = properties; + } + + public UUID getId() { + return id; + } + + public String getName() { + return name; + } + + public PropertyMap getProperties() { + return properties; + } + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilSession.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilSession.java new file mode 100644 index 000000000..1fcd80163 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilSession.java @@ -0,0 +1,89 @@ +package org.jackhuang.hmcl.auth.yggdrasil; + +import org.jackhuang.hmcl.util.Lang; +import org.jackhuang.hmcl.util.UUIDTypeAdapter; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public class YggdrasilSession { + + private final String accessToken; + private GameProfile selectedProfile; + private final GameProfile[] availableProfiles; + private final User user; + + public YggdrasilSession(String accessToken, GameProfile selectedProfile, GameProfile[] availableProfiles, User user) { + this.accessToken = accessToken; + this.selectedProfile = selectedProfile; + this.availableProfiles = availableProfiles; + this.user = user; + } + + public String getAccessToken() { + return accessToken; + } + + public GameProfile getSelectedProfile() { + return selectedProfile; + } + + public GameProfile[] getAvailableProfiles() { + return availableProfiles; + } + + public User getUser() { + return user; + } + + public void setAvailableProfile(GameProfile profile) { + if (availableProfiles != null) + for (int i = 0; i < availableProfiles.length; ++i) + if (availableProfiles[i].getId().equals(profile.getId())) + availableProfiles[i] = profile; + + if (selectedProfile != null && profile.getId().equals(selectedProfile.getId())) + selectedProfile = profile; + } + + public void setSelectedProfile(GameProfile selectedProfile) { + this.selectedProfile = selectedProfile; + + setAvailableProfile(selectedProfile); + } + + public static YggdrasilSession fromStorage(Map storage) { + Optional profileId = Lang.get(storage, "uuid", String.class); + Optional profileName = Lang.get(storage, "displayName", String.class); + GameProfile profile = null; + if (profileId.isPresent() && profileName.isPresent()) { + profile = new GameProfile(UUIDTypeAdapter.fromString(profileId.get()), profileName.get(), + Lang.get(storage, "profileProperties", Map.class).map(PropertyMap::fromMap).orElseGet(PropertyMap::new)); + } + + return new YggdrasilSession( + Lang.get(storage, "accessToken", String.class).orElse(null), + profile, + null, + Lang.get(storage, "userid", String.class) + .map(userId -> new User(userId, Lang.get(storage, "userProperties", Map.class).map(PropertyMap::fromMap).orElse(null))) + .orElse(null) + ); + } + + public Map toStorage() { + Map storage = new HashMap<>(); + storage.put("accessToken", accessToken); + if (selectedProfile != null) { + storage.put("uuid", UUIDTypeAdapter.fromUUID(selectedProfile.getId())); + storage.put("displayName", selectedProfile.getName()); + storage.put("profileProperties", selectedProfile.getProperties()); + } + if (user != null) { + storage.put("userid", user.getId()); + storage.put("userProperties", user.getProperties()); + } + return storage; + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/LaunchOptions.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/LaunchOptions.java index 27146d257..6be33dd9e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/LaunchOptions.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/LaunchOptions.java @@ -47,7 +47,7 @@ public class LaunchOptions implements Serializable { private String proxyUser; private String proxyPass; private boolean noGeneratedJVMArgs; - private String precalledCommand; + private String preLaunchCommand; /** * The game directory @@ -188,8 +188,8 @@ public class LaunchOptions implements Serializable { /** * Called command line before launching the game. */ - public String getPrecalledCommand() { - return precalledCommand; + public String getPreLaunchCommand() { + return preLaunchCommand; } public static class Builder { @@ -296,7 +296,7 @@ public class LaunchOptions implements Serializable { } public Builder setPrecalledCommand(String precalledCommand) { - options.precalledCommand = precalledCommand; + options.preLaunchCommand = precalledCommand; return this; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java index 6090a7b86..680fce272 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java @@ -240,8 +240,8 @@ public class DefaultLauncher extends Launcher { protected Map getConfigurations() { return Lang.mapOf( new Pair<>("${auth_player_name}", authInfo.getUsername()), - new Pair<>("${auth_session}", authInfo.getAuthToken()), - new Pair<>("${auth_access_token}", authInfo.getAuthToken()), + new Pair<>("${auth_session}", authInfo.getAccessToken()), + new Pair<>("${auth_access_token}", authInfo.getAccessToken()), new Pair<>("${auth_uuid}", authInfo.getUserId()), new Pair<>("${version_name}", Optional.ofNullable(options.getVersionName()).orElse(version.getId())), new Pair<>("${profile_name}", Optional.ofNullable(options.getProfileName()).orElse("Minecraft")), @@ -262,8 +262,8 @@ public class DefaultLauncher extends Launcher { decompressNatives(nativeFolder); - if (StringUtils.isNotBlank(options.getPrecalledCommand())) - Runtime.getRuntime().exec(options.getPrecalledCommand()).waitFor(); + if (StringUtils.isNotBlank(options.getPreLaunchCommand())) + Runtime.getRuntime().exec(options.getPreLaunchCommand()).waitFor(); Process process; try { @@ -306,8 +306,8 @@ public class DefaultLauncher extends Launcher { writer.write("cd /D %APPDATA%"); writer.newLine(); } - if (StringUtils.isNotBlank(options.getPrecalledCommand())) { - writer.write(options.getPrecalledCommand()); + if (StringUtils.isNotBlank(options.getPreLaunchCommand())) { + writer.write(options.getPreLaunchCommand()); writer.newLine(); } writer.write(StringUtils.makeCommand(generateCommandLine(nativeFolder))); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java index d00b7f688..6d1875e17 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java @@ -270,7 +270,7 @@ public abstract class Task { } else { if (failure != null) - failure.accept(variables.get(TaskExecutor.LAST_EXCEPION_ID)); + failure.accept(variables.get(TaskExecutor.LAST_EXCEPTION_ID)); } }); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/TaskExecutor.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/TaskExecutor.java index c909427dc..2bb7d7361 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/TaskExecutor.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/TaskExecutor.java @@ -20,10 +20,7 @@ package org.jackhuang.hmcl.task; import org.jackhuang.hmcl.util.*; import java.util.*; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.Future; -import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; @@ -109,7 +106,7 @@ public final class TaskExecutor { return false; Invoker invoker = new Invoker(task, latch, success); try { - Future future = task.getScheduler().schedule(invoker); + Future future = scheduler.schedule(invoker); if (future != null) workerQueue.add(future); } catch (RejectedExecutionException e) { @@ -152,7 +149,12 @@ public final class TaskExecutor { task.setDependentsSucceeded(); task.setVariables(variables); - task.execute(); + + try { + task.getScheduler().schedule(task::execute).get(); + } catch (ExecutionException e) { + throw (Exception) e.getCause(); + } if (task instanceof TaskResult) { TaskResult taskResult = (TaskResult) task; @@ -181,7 +183,7 @@ public final class TaskExecutor { // do nothing } catch (Exception e) { lastException = e; - variables.set(LAST_EXCEPION_ID, e); + variables.set(LAST_EXCEPTION_ID, e); Logging.LOG.log(Level.FINE, "Task failed: " + task.getName(), e); task.onDone().fireEvent(new TaskEvent(this, task, true)); taskListeners.forEach(it -> it.onFailed(task, e)); @@ -226,5 +228,5 @@ public final class TaskExecutor { } - public static final String LAST_EXCEPION_ID = "lastException"; + public static final String LAST_EXCEPTION_ID = "lastException"; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/IOUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/IOUtils.java index df17e944c..b68619086 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/IOUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/IOUtils.java @@ -58,8 +58,8 @@ public final class IOUtils { return readFully(stream).toString(); } - public static String readFullyAsString(InputStream stream, Charset charset) { - return Lang.invoke(() -> readFully(stream).toString(charset.name())); + public static String readFullyAsString(InputStream stream, Charset charset) throws IOException { + return readFully(stream).toString(charset.name()); } public static void copyTo(InputStream src, OutputStream dest) throws IOException { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java index 68a4fb4f0..49089c7c7 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java @@ -87,7 +87,7 @@ public final class Lang { return r -> invoke(function, r); } - public static Function liftFunction(ExceptionalFunction function) { + public static Function liftFunction(ExceptionalFunction function) throws E { return hideFunction(function); } @@ -115,7 +115,7 @@ public final class Lang { return () -> invoke(supplier); } - public static Supplier liftException(ExceptionalSupplier supplier) { + public static Supplier liftException(ExceptionalSupplier supplier) throws E { return hideException(supplier); } @@ -141,7 +141,7 @@ public final class Lang { return it -> invokeConsumer(consumer, it); } - public static Consumer liftConsumer(ExceptionalConsumer consumer) { + public static Consumer liftConsumer(ExceptionalConsumer consumer) throws E { return hideConsumer(consumer); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/LowerCaseEnumTypeAdapterFactory.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/LowerCaseEnumTypeAdapterFactory.java index 1cdbc0229..e5a1626be 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/LowerCaseEnumTypeAdapterFactory.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/LowerCaseEnumTypeAdapterFactory.java @@ -26,7 +26,6 @@ import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; import java.io.IOException; -import java.util.Arrays; import java.util.HashMap; import java.util.Locale; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/NetworkUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/NetworkUtils.java index e747bd638..2c2c425f5 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/NetworkUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/NetworkUtils.java @@ -85,10 +85,10 @@ public final class NetworkUtils { return doPost(u, post, contentType, Proxy.NO_PROXY); } - public static String doPost(URL u, String post, String contentType, Proxy proxy) throws IOException { + public static String doPost(URL url, String post, String contentType, Proxy proxy) throws IOException { byte[] bytes = post.getBytes(Charsets.UTF_8); - HttpURLConnection con = createConnection(u, proxy); + HttpURLConnection con = createConnection(url, proxy); con.setRequestMethod("POST"); con.setDoOutput(true); con.setRequestProperty("Content-Type", contentType + "; charset=utf-8");