authentication reconstruction

This commit is contained in:
huanghongxun
2018-02-22 14:12:55 +08:00
parent 6e202341ad
commit 6c4231a6b2
58 changed files with 880 additions and 982 deletions

View File

@@ -22,7 +22,7 @@ import javafx.scene.image.Image;
import org.jackhuang.hmcl.Main; import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile; 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.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.setting.Settings; import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.FileDownloadTask;
@@ -82,12 +82,9 @@ public final class AccountHelper {
} }
public static Image getSkin(YggdrasilAccount account, double scaleRatio) { public static Image getSkin(YggdrasilAccount account, double scaleRatio) {
if (account.getSelectedProfile() == null) if (account.getCharacter() == null)
return getDefaultSkin(account, scaleRatio); return getDefaultSkin(account, scaleRatio);
String name = account.getSelectedProfile().getName(); File file = getSkinFile(account.getCharacter());
if (name == null)
return getDefaultSkin(account, scaleRatio);
File file = getSkinFile(name);
if (file.exists()) { if (file.exists()) {
Image original = new Image("file:" + file.getAbsolutePath()); Image original = new Image("file:" + file.getAbsolutePath());
return new Image("file:" + file.getAbsolutePath(), return new Image("file:" + file.getAbsolutePath(),
@@ -142,23 +139,33 @@ public final class AccountHelper {
@Override @Override
public void execute() throws Exception { public void execute() throws Exception {
if (account.canLogIn() && (account.getSelectedProfile() == null || refresh)) if (!account.isLoggedIn() && (account.getCharacter() == null || refresh))
DialogController.logIn(account); 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 { private static void downloadSkin(YggdrasilAccount account, GameProfile profile, boolean refresh, Proxy proxy) throws Exception {
account.clearCache(); account.clearCache();
if (profile == null) return; Optional<Texture> texture = account.getSkin(profile);
String name = profile.getName();
if (name == null) return;
Optional<ProfileTexture> texture = account.getSkin(profile);
if (!texture.isPresent()) return; if (!texture.isPresent()) return;
String url = texture.get().getUrl(); 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> texture = account.getSkin();
if (!texture.isPresent()) return;
String url = texture.get().getUrl();
File file = getSkinFile(account.getCharacter());
if (!refresh && file.exists()) if (!refresh && file.exists())
return; return;
new FileDownloadTask(NetworkUtils.toURL(url), file, proxy).run(); new FileDownloadTask(NetworkUtils.toURL(url), file, proxy).run();

View File

@@ -20,11 +20,17 @@ package org.jackhuang.hmcl.game;
import com.jfoenix.concurrency.JFXUtilities; import com.jfoenix.concurrency.JFXUtilities;
import javafx.application.Platform; import javafx.application.Platform;
import org.jackhuang.hmcl.Main; 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.download.DefaultDependencyManager;
import org.jackhuang.hmcl.launch.*; import org.jackhuang.hmcl.launch.*;
import org.jackhuang.hmcl.mod.CurseCompletionTask; 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.task.*;
import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.DialogController; import org.jackhuang.hmcl.ui.DialogController;
@@ -89,7 +95,7 @@ public final class LauncherHelper {
.then(Task.of(Main.i18n("account.methods"), variables -> { .then(Task.of(Main.i18n("account.methods"), variables -> {
try { try {
try { try {
variables.set("account", account.logIn(new SpecificCharacterSelector(Accounts.getCurrentCharacter(account)), Settings.INSTANCE.getProxy())); variables.set("account", account.logIn());
} catch (ServerDisconnectException e) { } catch (ServerDisconnectException e) {
if (account.canPlayOffline()) if (account.canPlayOffline())
variables.set("account", account.playOffline()); variables.set("account", account.playOffline());
@@ -302,7 +308,7 @@ public final class LauncherHelper {
forbiddenTokens = Collections.emptyMap(); forbiddenTokens = Collections.emptyMap();
else else
forbiddenTokens = Lang.mapOf( forbiddenTokens = Lang.mapOf(
new Pair<>(authInfo.getAuthToken(), "<access token>"), new Pair<>(authInfo.getAccessToken(), "<access token>"),
new Pair<>(authInfo.getUserId(), "<uuid>"), new Pair<>(authInfo.getUserId(), "<uuid>"),
new Pair<>(authInfo.getUsername(), "<player>") new Pair<>(authInfo.getUsername(), "<player>")
); );

View File

@@ -21,9 +21,15 @@ import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.Main; import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AccountFactory; import org.jackhuang.hmcl.auth.AccountFactory;
import org.jackhuang.hmcl.auth.OfflineAccount; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.OfflineAccountFactory; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccountFactory;
import org.jackhuang.hmcl.auth.yggdrasil.*; 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.task.FileDownloadTask;
import org.jackhuang.hmcl.util.*; import org.jackhuang.hmcl.util.*;
@@ -32,7 +38,6 @@ import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Optional;
/** /**
* @author huangyuhui * @author huangyuhui
@@ -46,7 +51,7 @@ public final class Accounts {
public static final Map<String, AccountFactory<?>> ACCOUNT_FACTORY = Lang.mapOf( public static final Map<String, AccountFactory<?>> ACCOUNT_FACTORY = Lang.mapOf(
new Pair<>(OFFLINE_ACCOUNT_KEY, OfflineAccountFactory.INSTANCE), 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)) new Pair<>(AUTHLIB_INJECTOR_ACCOUNT_KEY, new AuthlibInjectorAccountFactory(Accounts::downloadAuthlibInjector))
); );
@@ -59,26 +64,8 @@ public final class Accounts {
else return YGGDRASIL_ACCOUNT_KEY; 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<String> getCurrentCharacter(Map<Object, Object> storage) {
return Lang.get(storage, "properties", Map.class)
.flatMap(properties -> Lang.get(properties, "character", String.class));
}
static String getAccountId(Account account) { static String getAccountId(Account account) {
return getAccountId(account.getUsername(), getCurrentCharacter(account)); return getAccountId(account.getUsername(), account.getCharacter());
} }
static String getAccountId(String username, String character) { static String getAccountId(String username, String character) {

View File

@@ -26,7 +26,7 @@ import javafx.scene.text.Font;
import org.jackhuang.hmcl.Main; import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AccountFactory; 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.BMCLAPIDownloadProvider;
import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.MojangDownloadProvider; import org.jackhuang.hmcl.download.MojangDownloadProvider;
@@ -67,21 +67,23 @@ public class Settings {
private Map<String, Account> accounts = new HashMap<>(); private Map<String, Account> accounts = new HashMap<>();
{ {
for (Map<Object, Object> settings : SETTINGS.getAccounts()) { loadProxy();
Optional<String> characterName = Accounts.getCurrentCharacter(settings);
for (Iterator<Map<Object, Object>> iterator = SETTINGS.getAccounts().iterator(); iterator.hasNext(); ) {
Map<Object, Object> settings = iterator.next();
AccountFactory<?> factory = Accounts.ACCOUNT_FACTORY.get(Lang.get(settings, "type", String.class).orElse("")); 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. // unrecognized account type, so remove it.
SETTINGS.getAccounts().remove(settings); iterator.remove();
continue; continue;
} }
Account account; Account account;
try { try {
account = factory.fromStorage(settings); account = factory.fromStorage(settings, getProxy());
} catch (Exception e) { } catch (Exception e) {
SETTINGS.getAccounts().remove(settings);
// storage is malformed, delete. // storage is malformed, delete.
iterator.remove();
continue; continue;
} }
@@ -101,8 +103,6 @@ public class Settings {
} }
Lang.ignoringException(() -> Runtime.getRuntime().addShutdownHook(new Thread(this::save))); Lang.ignoringException(() -> Runtime.getRuntime().addShutdownHook(new Thread(this::save)));
loadProxy();
} }
private Config initSettings() { private Config initSettings() {
@@ -468,8 +468,6 @@ public class Settings {
* PROFILES * * PROFILES *
****************************************/ ****************************************/
private Profile selectedProfile;
public Profile getSelectedProfile() { public Profile getSelectedProfile() {
checkProfileMap(); checkProfileMap();

View File

@@ -35,8 +35,8 @@ import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.OfflineAccount; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.yggdrasil.AuthlibInjectorAccount; import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.game.AccountHelper; import org.jackhuang.hmcl.game.AccountHelper;
import org.jackhuang.hmcl.setting.Accounts; import org.jackhuang.hmcl.setting.Accounts;
@@ -82,7 +82,7 @@ public final class AccountItem extends StackPane {
chkSelected.getProperties().put("account", account); chkSelected.getProperties().put("account", account);
setSelected(Settings.INSTANCE.getSelectedAccount() == account); setSelected(Settings.INSTANCE.getSelectedAccount() == account);
lblUser.setText(Accounts.getCurrentCharacter(account)); lblUser.setText(account.getCharacter());
lblType.setText(AccountsPage.accountType(account)); lblType.setText(AccountsPage.accountType(account));
lblEmail.setText(account.getUsername()); lblEmail.setText(account.getUsername());

View File

@@ -26,9 +26,6 @@ import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AuthInfo; import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.NoSelectedCharacterException; 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.Schedulers;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
@@ -64,7 +61,7 @@ public class AccountLoginPane extends StackPane {
lblCreationWarning.setText(""); lblCreationWarning.setText("");
Task.ofResult("login", () -> { Task.ofResult("login", () -> {
try { try {
return oldAccount.logInWithPassword(new SpecificCharacterSelector(Accounts.getCurrentCharacter(oldAccount)), password, Settings.INSTANCE.getProxy()); return oldAccount.logInWithPassword(password);
} catch (Exception e) { } catch (Exception e) {
return e; return e;
} }

View File

@@ -36,7 +36,8 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.Main; import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.auth.*; 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.GameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.game.AccountHelper; import org.jackhuang.hmcl.game.AccountHelper;
@@ -52,6 +53,7 @@ import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
import java.util.Collection; import java.util.Collection;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -153,6 +155,7 @@ public final class AccountsPage extends StackPane implements DecoratorPage {
private void addNewAccount() { private void addNewAccount() {
txtUsername.setText(""); txtUsername.setText("");
txtPassword.setText(""); txtPassword.setText("");
lblCreationWarning.setText("");
dialog.show(); dialog.show();
} }
@@ -166,20 +169,19 @@ public final class AccountsPage extends StackPane implements DecoratorPage {
int type = cboType.getSelectionModel().getSelectedIndex(); int type = cboType.getSelectionModel().getSelectedIndex();
String username = txtUsername.getText(); String username = txtUsername.getText();
String password = txtPassword.getText(); String password = txtPassword.getText();
String apiRoot = Optional.ofNullable(cboServers.getSelectionModel().getSelectedItem()).map(TwoLineListItem::getSubtitle).orElse(null);
progressBar.setVisible(true); progressBar.setVisible(true);
lblCreationWarning.setText(""); lblCreationWarning.setText("");
Task.ofResult("create_account", () -> { Task.ofResult("create_account", () -> {
Account account; AccountFactory<?> factory;
switch (type) { switch (type) {
case 0: account = Accounts.ACCOUNT_FACTORY.get(Accounts.OFFLINE_ACCOUNT_KEY).fromUsername(username); break; case 0: factory = Accounts.ACCOUNT_FACTORY.get(Accounts.OFFLINE_ACCOUNT_KEY); break;
case 1: account = Accounts.ACCOUNT_FACTORY.get(Accounts.YGGDRASIL_ACCOUNT_KEY).fromUsername(username, password); break; case 1: factory = Accounts.ACCOUNT_FACTORY.get(Accounts.YGGDRASIL_ACCOUNT_KEY); break;
case 2: account = Accounts.ACCOUNT_FACTORY.get(Accounts.AUTHLIB_INJECTOR_ACCOUNT_KEY).fromUsername(username, password, cboServers.getSelectionModel().getSelectedItem().getSubtitle()); break; case 2: factory = Accounts.ACCOUNT_FACTORY.get(Accounts.AUTHLIB_INJECTOR_ACCOUNT_KEY); break;
default: throw new Error(); default: throw new Error();
} }
AuthInfo info = account.logIn(new CharacterSelector(), Settings.INSTANCE.getProxy()); return factory.create(new Selector(), username, password, apiRoot, Settings.INSTANCE.getProxy());
Accounts.setCurrentCharacter(account, info.getUsername());
return account;
}).finalized(Schedulers.javafx(), variables -> { }).finalized(Schedulers.javafx(), variables -> {
Settings.INSTANCE.addAccount(variables.get("create_account")); Settings.INSTANCE.addAccount(variables.get("create_account"));
dialog.close(); 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); 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 AdvancedListBox listBox = new AdvancedListBox();
private final JFXButton cancel = new JFXButton(); private final JFXButton cancel = new JFXButton();
@@ -263,7 +265,7 @@ public final class AccountsPage extends StackPane implements DecoratorPage {
@Override @Override
public GameProfile select(Account account, List<GameProfile> names) throws NoSelectedCharacterException { public GameProfile select(Account account, List<GameProfile> names) throws NoSelectedCharacterException {
if (!(account instanceof YggdrasilAccount)) if (!(account instanceof YggdrasilAccount))
return MultiCharacterSelector.DEFAULT.select(account, names); return CharacterSelector.DEFAULT.select(account, names);
YggdrasilAccount yggdrasilAccount = (YggdrasilAccount) account; YggdrasilAccount yggdrasilAccount = (YggdrasilAccount) account;
for (GameProfile profile : names) { for (GameProfile profile : names) {

View File

@@ -23,7 +23,7 @@ import javafx.geometry.Pos;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.auth.yggdrasil.AuthlibInjectorServerInfo; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServerInfo;
import java.util.function.Consumer; import java.util.function.Consumer;

View File

@@ -10,7 +10,7 @@ import javafx.scene.control.ScrollPane;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Main; 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.Accounts;
import org.jackhuang.hmcl.setting.Settings; import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Schedulers;

View File

@@ -20,7 +20,6 @@ package org.jackhuang.hmcl.ui;
import com.jfoenix.concurrency.JFXUtilities; import com.jfoenix.concurrency.JFXUtilities;
import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AuthInfo; import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.MultiCharacterSelector;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.task.SilentException; import org.jackhuang.hmcl.task.SilentException;
@@ -48,6 +47,6 @@ public final class DialogController {
latch.await(); latch.await();
return Optional.ofNullable(res.get()).orElseThrow(SilentException::new); return Optional.ofNullable(res.get()).orElseThrow(SilentException::new);
} }
return account.logIn(MultiCharacterSelector.DEFAULT); return account.logIn();
} }
} }

View File

@@ -36,7 +36,6 @@ import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.ModpackHelper; import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.mod.Modpack; import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.UnsupportedModpackException; import org.jackhuang.hmcl.mod.UnsupportedModpackException;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles; import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.Settings; import org.jackhuang.hmcl.setting.Settings;
@@ -90,7 +89,7 @@ public final class LeftPaneController {
accountItem.setVersionName(Main.i18n("account.missing")); accountItem.setVersionName(Main.i18n("account.missing"));
accountItem.setGameVersion(Main.i18n("message.unknown")); accountItem.setGameVersion(Main.i18n("message.unknown"));
} else { } else {
accountItem.setVersionName(Accounts.getCurrentCharacter(it)); accountItem.setVersionName(it.getCharacter());
accountItem.setGameVersion(AccountsPage.accountType(it)); accountItem.setGameVersion(AccountsPage.accountType(it));
} }

View File

@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.validation.base.ValidatorBase; import com.jfoenix.validation.base.ValidatorBase;
import javafx.beans.NamedArg; import javafx.beans.NamedArg;
import javafx.scene.control.TextInputControl; import javafx.scene.control.TextInputControl;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.StringUtils;
public class NumberValidator extends ValidatorBase { public class NumberValidator extends ValidatorBase {
@@ -51,11 +52,6 @@ public class NumberValidator extends ValidatorBase {
if (StringUtils.isBlank(textField.getText())) if (StringUtils.isBlank(textField.getText()))
hasErrors.set(!nullable); hasErrors.set(!nullable);
else else
try { hasErrors.set(Lang.toIntOrNull(textField.getText()) == null);
Integer.parseInt(textField.getText());
hasErrors.set(false);
} catch (NumberFormatException e) {
hasErrors.set(true);
}
} }
} }

View File

@@ -17,8 +17,6 @@
*/ */
package org.jackhuang.hmcl.auth; package org.jackhuang.hmcl.auth;
import java.net.Proxy;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
@@ -33,6 +31,12 @@ public abstract class Account {
*/ */
public abstract String getUsername(); public abstract String getUsername();
/**
*
* @return the character name
*/
public abstract String getCharacter();
/** /**
* @return the UUID * @return the UUID
*/ */
@@ -40,28 +44,12 @@ public abstract class Account {
/** /**
* log in. * log in.
* @param selector selects a character
* @return the specific player's info. * @return the specific player's info.
* @throws AuthenticationException if server error occurred. * @throws AuthenticationException if server error occurred.
*/ */
public AuthInfo logIn(MultiCharacterSelector selector) throws AuthenticationException { public abstract AuthInfo logIn() throws AuthenticationException;
return logIn(selector, Proxy.NO_PROXY);
}
/** public abstract AuthInfo logInWithPassword(String password) 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 boolean canPlayOffline(); public abstract boolean canPlayOffline();
@@ -73,24 +61,7 @@ public abstract class Account {
public abstract void logOut(); public abstract void logOut();
protected abstract Map<Object, Object> toStorageImpl(); public abstract Map<Object, Object> toStorage();
public final Map<Object, Object> toStorage() {
Map<Object, Object> storage = toStorageImpl();
if (!getProperties().isEmpty())
storage.put("properties", getProperties());
return storage;
}
private final Map<Object, Object> properties = new HashMap<>();
/**
* To save some necessary extra information here.
* @return the property map.
*/
public final Map<Object, Object> getProperties() {
return properties;
}
public abstract void clearCache(); public abstract void clearCache();
} }

View File

@@ -17,8 +17,7 @@
*/ */
package org.jackhuang.hmcl.auth; package org.jackhuang.hmcl.auth;
import org.jackhuang.hmcl.util.Lang; import java.net.Proxy;
import java.util.Map; import java.util.Map;
/** /**
@@ -27,23 +26,7 @@ import java.util.Map;
*/ */
public abstract class AccountFactory<T extends Account> { public abstract class AccountFactory<T extends Account> {
public final T fromUsername(String username) { public abstract T create(CharacterSelector selector, String username, String password, Object additionalData, Proxy proxy) throws AuthenticationException;
return fromUsername(username, "", null);
}
public final T fromUsername(String username, String password) { public abstract T fromStorage(Map<Object, Object> storage, Proxy proxy);
return fromUsername(username, password, null);
}
public abstract T fromUsername(String username, String password, Object additionalData);
protected abstract T fromStorageImpl(Map<Object, Object> storage);
public final T fromStorage(Map<Object, Object> 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;
}
} }

View File

@@ -17,10 +17,8 @@
*/ */
package org.jackhuang.hmcl.auth; package org.jackhuang.hmcl.auth;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
import org.jackhuang.hmcl.game.Arguments; import org.jackhuang.hmcl.game.Arguments;
import org.jackhuang.hmcl.util.Immutable; 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 username;
private final String userId; private final String userId;
private final String authToken; private final String accessToken;
private final UserType userType; private final UserType userType;
private final String userProperties; private final String userProperties;
private final String userPropertyMap;
private final Arguments arguments; private final Arguments arguments;
public AuthInfo(String username, String userId, String authToken) { public AuthInfo(String username, String userId, String accessToken) {
this(username, userId, authToken, UserType.LEGACY); this(username, userId, accessToken, UserType.LEGACY);
} }
public AuthInfo(String username, String userId, String authToken, UserType userType) { public AuthInfo(String username, String userId, String accessToken, UserType userType) {
this(username, userId, authToken, userType, "{}"); this(username, userId, accessToken, userType, "{}");
} }
public AuthInfo(String username, String userId, String authToken, UserType userType, String userProperties) { public AuthInfo(String username, String userId, String accessToken, UserType userType, String userProperties) {
this(username, userId, authToken, userType, userProperties, "{}"); this(username, userId, accessToken, userType, userProperties, null);
} }
public AuthInfo(String username, String userId, String authToken, UserType userType, String userProperties, String userPropertyMap) { public AuthInfo(String username, String userId, String accessToken, UserType userType, String userProperties, Arguments arguments) {
this(username, userId, authToken, userType, userProperties, userPropertyMap, null);
}
public AuthInfo(String username, String userId, String authToken, UserType userType, String userProperties, String userPropertyMap, Arguments arguments) {
this.username = username; this.username = username;
this.userId = userId; this.userId = userId;
this.authToken = authToken; this.accessToken = accessToken;
this.userType = userType; this.userType = userType;
this.userProperties = userProperties; this.userProperties = userProperties;
this.userPropertyMap = userPropertyMap;
this.arguments = arguments; 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() { public String getUsername() {
return username; return username;
} }
@@ -75,8 +63,8 @@ public final class AuthInfo {
return userId; return userId;
} }
public String getAuthToken() { public String getAccessToken() {
return authToken; return accessToken;
} }
public UserType getUserType() { public UserType getUserType() {
@@ -93,21 +81,11 @@ public final class AuthInfo {
return userProperties; 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() { public Arguments getArguments() {
return arguments; return arguments;
} }
public AuthInfo setArguments(Arguments 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);
} }
} }

View File

@@ -25,7 +25,7 @@ import java.util.List;
* This interface is for your application to open a GUI for user to choose the character * 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.. * 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. * Select one of {@code names} GameProfiles to login.
@@ -35,5 +35,5 @@ public interface MultiCharacterSelector {
*/ */
GameProfile select(Account account, List<GameProfile> names) throws NoSelectedCharacterException; GameProfile select(Account account, List<GameProfile> 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));
} }

View File

@@ -22,14 +22,4 @@ package org.jackhuang.hmcl.auth;
* @author huangyuhui * @author huangyuhui
*/ */
public final class InvalidCredentialsException extends AuthenticationException { public final class InvalidCredentialsException extends AuthenticationException {
private final Account account;
public InvalidCredentialsException(Account account) {
this.account = account;
}
public Account getAccount() {
return account;
}
} }

View File

@@ -21,15 +21,4 @@ package org.jackhuang.hmcl.auth;
* throws if wrong password. * throws if wrong password.
*/ */
public class InvalidPasswordException extends AuthenticationException { public class InvalidPasswordException extends AuthenticationException {
private final Account account;
public InvalidPasswordException(Account account) {
super();
this.account = account;
}
public Account getAccount() {
return account;
}
} }

View File

@@ -17,22 +17,9 @@
*/ */
package org.jackhuang.hmcl.auth; package org.jackhuang.hmcl.auth;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
/** /**
* *
* @author huangyuhui * @author huangyuhui
*/ */
public class InvalidTokenException extends AuthenticationException { public class InvalidTokenException extends AuthenticationException {
private final YggdrasilAccount account;
public InvalidTokenException(YggdrasilAccount account) {
super();
this.account = account;
}
public YggdrasilAccount getAccount() {
return account;
}
} }

View File

@@ -18,10 +18,10 @@
package org.jackhuang.hmcl.auth; 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. * valid character.
* *
* @see org.jackhuang.hmcl.auth.MultiCharacterSelector * @see CharacterSelector
* @author huangyuhui * @author huangyuhui
*/ */
public final class NoSelectedCharacterException extends AuthenticationException { public final class NoSelectedCharacterException extends AuthenticationException {

View File

@@ -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);
}
}

View File

@@ -24,7 +24,7 @@ import java.util.List;
/** /**
* Select character by name. * Select character by name.
*/ */
public class SpecificCharacterSelector implements MultiCharacterSelector { public class SpecificCharacterSelector implements CharacterSelector {
private final String id; private final String id;
/** /**

View File

@@ -15,18 +15,20 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}. * 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.AuthInfo;
import org.jackhuang.hmcl.auth.AuthenticationException; 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.game.Arguments;
import org.jackhuang.hmcl.task.GetTask; import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.util.ExceptionalSupplier; import org.jackhuang.hmcl.util.ExceptionalSupplier;
import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.NetworkUtils; import org.jackhuang.hmcl.util.NetworkUtils;
import java.net.Proxy;
import java.util.Base64; import java.util.Base64;
import java.util.Map; import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@@ -35,21 +37,30 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
private final String serverBaseURL; private final String serverBaseURL;
private final ExceptionalSupplier<String, ?> injectorJarPath; private final ExceptionalSupplier<String, ?> injectorJarPath;
public AuthlibInjectorAccount(ExceptionalSupplier<String, ?> injectorJarPath, String serverBaseURL, String username) { protected AuthlibInjectorAccount(YggdrasilService service, String serverBaseURL, ExceptionalSupplier<String, ?> injectorJarPath, String username, String clientToken, String character, YggdrasilSession session) {
super(serverBaseURL + "authserver/", serverBaseURL + "sessionserver/", username); super(service, username, clientToken, character, session);
this.injectorJarPath = injectorJarPath; this.injectorJarPath = injectorJarPath;
this.serverBaseURL = serverBaseURL; this.serverBaseURL = serverBaseURL;
} }
@Override @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<AuthInfo, AuthenticationException> supplier) throws AuthenticationException {
// Authlib Injector recommends launchers to pre-fetch the server basic information before launched the game to save time. // 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)); GetTask getTask = new GetTask(NetworkUtils.toURL(serverBaseURL));
AtomicBoolean flag = new AtomicBoolean(true); AtomicBoolean flag = new AtomicBoolean(true);
Thread thread = Lang.thread(() -> flag.set(getTask.test())); Thread thread = Lang.thread(() -> flag.set(getTask.test()));
AuthInfo info = super.logIn(selector, proxy); AuthInfo info = supplier.get();
try { try {
thread.join(); thread.join();
@@ -66,32 +77,9 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
} }
@Override @Override
public AuthInfo logInWithPassword(MultiCharacterSelector selector, String password, Proxy proxy) throws AuthenticationException { public Map<Object, Object> toStorage() {
// Authlib Injector recommends launchers to pre-fetch the server basic information before launched the game to save time. Map<Object, Object> map = super.toStorage();
GetTask getTask = new GetTask(NetworkUtils.toURL(serverBaseURL)); map.put("serverBaseURL", 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<Object, Object> toStorageImpl() {
Map<Object, Object> map = super.toStorageImpl();
map.put(STORAGE_KEY_SERVER_BASE_URL, serverBaseURL);
return map; return map;
} }
@@ -99,5 +87,4 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
return serverBaseURL; return serverBaseURL;
} }
public static final String STORAGE_KEY_SERVER_BASE_URL = "serverBaseURL";
} }

View File

@@ -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<AuthlibInjectorAccount> {
private final ExceptionalSupplier<String, ?> injectorJarPathSupplier;
public AuthlibInjectorAccountFactory(ExceptionalSupplier<String, ?> 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<Object, Object> 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));
}
}

View File

@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}. * 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 com.google.gson.JsonParseException;
import org.jackhuang.hmcl.util.Immutable; import org.jackhuang.hmcl.util.Immutable;

View File

@@ -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));
}
}

View File

@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}. * 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 { public class AuthlibInjectorServerInfo {
private final String serverIp; private final String serverIp;

View File

@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}. * 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 { public class AuthlibInjectorServerResponse {

View File

@@ -15,14 +15,16 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}. * 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.Lang;
import org.jackhuang.hmcl.util.Pair; import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.UUIDTypeAdapter; import org.jackhuang.hmcl.util.UUIDTypeAdapter;
import java.net.Proxy;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.UUID; import java.util.UUID;
@@ -58,7 +60,12 @@ public class OfflineAccount extends Account {
} }
@Override @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)) if (StringUtils.isBlank(username) || StringUtils.isBlank(uuid))
throw new AuthenticationException("Username cannot be empty"); throw new AuthenticationException("Username cannot be empty");
@@ -66,8 +73,8 @@ public class OfflineAccount extends Account {
} }
@Override @Override
public AuthInfo logInWithPassword(MultiCharacterSelector selector, String password, Proxy proxy) throws AuthenticationException { public AuthInfo logInWithPassword(String password) throws AuthenticationException {
return logIn(selector, proxy); return logIn();
} }
@Override @Override
@@ -86,7 +93,7 @@ public class OfflineAccount extends Account {
} }
@Override @Override
public Map<Object, Object> toStorageImpl() { public Map<Object, Object> toStorage() {
return Lang.mapOf( return Lang.mapOf(
new Pair<>("uuid", uuid), new Pair<>("uuid", uuid),
new Pair<>("username", username) new Pair<>("username", username)

View File

@@ -15,10 +15,14 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}. * 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.DigestUtils;
import org.jackhuang.hmcl.util.Lang;
import java.net.Proxy;
import java.util.Map; import java.util.Map;
/** /**
@@ -32,21 +36,18 @@ public class OfflineAccountFactory extends AccountFactory<OfflineAccount> {
} }
@Override @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)); return new OfflineAccount(username, getUUIDFromUserName(username));
} }
@Override @Override
public OfflineAccount fromStorageImpl(Map<Object, Object> storage) { public OfflineAccount fromStorage(Map<Object, Object> storage, Proxy proxy) {
Object username = storage.get("username"); String username = Lang.get(storage, "username", String.class)
if (username == null || !(username instanceof String)) .orElseThrow(() -> new IllegalStateException("Offline account configuration malformed."));
throw new IllegalStateException("Offline account configuration malformed."); String uuid = Lang.get(storage, "uuid", String.class)
.orElse(getUUIDFromUserName(username));
Object uuid = storage.get("uuid"); return new OfflineAccount(username, uuid);
if (uuid == null || !(uuid instanceof String))
uuid = getUUIDFromUserName((String) username);
return new OfflineAccount((String) username, (String) uuid);
} }
private static String getUUIDFromUserName(String username) { private static String getUUIDFromUserName(String username) {

View File

@@ -1,74 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* 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<String, Object> 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<String, Object> getAgent() {
return agent;
}
public boolean isRequestUser() {
return requestUser;
}
}

View File

@@ -21,30 +21,26 @@ package org.jackhuang.hmcl.auth.yggdrasil;
* *
* @author huangyuhui * @author huangyuhui
*/ */
public final class AuthenticationResponse { final class AuthenticationResponse extends ErrorResponse {
private final String accessToken; private final String accessToken;
private final String clientToken; private final String clientToken;
private final GameProfile selectedProfile; private final GameProfile selectedProfile;
private final GameProfile[] availableProfiles; private final GameProfile[] availableProfiles;
private final User user; private final User user;
private final String error;
private final String errorMessage;
private final String cause;
public AuthenticationResponse() { public AuthenticationResponse() {
this(null, null, null, null, null, null, null, null); 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) { 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.accessToken = accessToken;
this.clientToken = clientToken; this.clientToken = clientToken;
this.selectedProfile = selectedProfile; this.selectedProfile = selectedProfile;
this.availableProfiles = availableProfiles; this.availableProfiles = availableProfiles;
this.user = user; this.user = user;
this.error = error;
this.errorMessage = errorMessage;
this.cause = cause;
} }
public String getAccessToken() { public String getAccessToken() {
@@ -67,16 +63,4 @@ public final class AuthenticationResponse {
return user; return user;
} }
public String getError() {
return error;
}
public String getErrorMessage() {
return errorMessage;
}
public String getCause() {
return cause;
}
} }

View File

@@ -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<YggdrasilAccount> {
private final ExceptionalSupplier<String, ?> injectorJarPathSupplier;
public AuthlibInjectorAccountFactory(ExceptionalSupplier<String, ?> 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<Object, Object> 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<String> profileId = Lang.get(storage, STORAGE_KEY_PROFILE_ID, String.class);
Optional<String> 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;
}
}

View File

@@ -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;
}
}

View File

@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.auth.yggdrasil; package org.jackhuang.hmcl.auth.yggdrasil;
import com.google.gson.*; import com.google.gson.*;
import org.jackhuang.hmcl.auth.UserType;
import org.jackhuang.hmcl.util.Immutable; import org.jackhuang.hmcl.util.Immutable;
import java.lang.reflect.Type; import java.lang.reflect.Type;
@@ -40,7 +41,11 @@ public final class GameProfile {
} }
public GameProfile(UUID id, String name) { 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) { public GameProfile(UUID id, String name, PropertyMap properties, boolean legacy) {
@@ -66,6 +71,10 @@ public final class GameProfile {
return legacy; return legacy;
} }
public UserType getUserType() {
return UserType.fromLegacy(isLegacy());
}
public static class Serializer implements JsonSerializer<GameProfile>, JsonDeserializer<GameProfile> { public static class Serializer implements JsonSerializer<GameProfile>, JsonDeserializer<GameProfile> {
public static final Serializer INSTANCE = new Serializer(); public static final Serializer INSTANCE = new Serializer();

View File

@@ -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));
}
}

View File

@@ -1,46 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* 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;
}
}

View File

@@ -1,37 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* 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;
}
}

View File

@@ -18,33 +18,20 @@
package org.jackhuang.hmcl.auth.yggdrasil; package org.jackhuang.hmcl.auth.yggdrasil;
import com.google.gson.*; import com.google.gson.*;
import org.jackhuang.hmcl.util.Lang;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.*; import java.util.HashMap;
import java.util.Map;
public final class PropertyMap extends HashMap<String, Property> { public final class PropertyMap extends HashMap<String, String> {
public List<Map<String, String>> toList() { public static PropertyMap fromMap(Map<?, ?> map) {
List<Map<String, String>> properties = new ArrayList<>(); PropertyMap propertyMap = new PropertyMap();
for (Property profileProperty : values()) { for (Map.Entry<?, ?> entry : map.entrySet()) {
Map<String, String> property = new HashMap<>(); if (entry.getKey() instanceof String && entry.getValue() instanceof String)
property.put("name", profileProperty.getName()); propertyMap.put((String) entry.getKey(), (String) entry.getValue());
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<String> name = Lang.get((Map<?, ?>) propertyMap, "name", String.class);
Optional<String> value = Lang.get((Map<?, ?>) propertyMap, "value", String.class);
if (name.isPresent() && value.isPresent())
put(name.get(), new Property(name.get(), value.get()));
} }
return propertyMap;
} }
public static class Serializer implements JsonSerializer<PropertyMap>, JsonDeserializer<PropertyMap> { public static class Serializer implements JsonSerializer<PropertyMap>, JsonDeserializer<PropertyMap> {
@@ -57,19 +44,11 @@ public final class PropertyMap extends HashMap<String, Property> {
@Override @Override
public PropertyMap deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { public PropertyMap deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
PropertyMap result = new PropertyMap(); PropertyMap result = new PropertyMap();
if (json instanceof JsonObject) { for (JsonElement element : json.getAsJsonArray())
for (Map.Entry<String, JsonElement> entry : ((JsonObject) json).entrySet()) if (element instanceof JsonObject) {
if (entry.getValue() instanceof JsonArray) JsonObject object = (JsonObject) element;
for (JsonElement element : (JsonArray) entry.getValue()) result.put(object.get("name").getAsString(), object.get("value").getAsString());
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));
}
return result; return result;
} }
@@ -77,29 +56,14 @@ public final class PropertyMap extends HashMap<String, Property> {
@Override @Override
public JsonElement serialize(PropertyMap src, Type typeOfSrc, JsonSerializationContext context) { public JsonElement serialize(PropertyMap src, Type typeOfSrc, JsonSerializationContext context) {
JsonArray result = new JsonArray(); JsonArray result = new JsonArray();
for (Property property : src.values()) { for (Map.Entry<String, String> entry : src.entrySet()) {
JsonObject object = new JsonObject(); JsonObject object = new JsonObject();
object.addProperty("name", property.getName()); object.addProperty("name", entry.getKey());
object.addProperty("value", property.getValue()); object.addProperty("value", entry.getValue());
result.add(object); result.add(object);
} }
return result; return result;
} }
} }
public static class LegacySerializer
implements JsonSerializer<PropertyMap> {
@Override
public JsonElement serialize(PropertyMap src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject result = new JsonObject();
for (PropertyMap.Entry<String, Property> entry : src.entrySet()) {
JsonArray values = new JsonArray();
values.add(new JsonPrimitive(entry.getValue().getValue()));
result.add(entry.getKey(), values);
}
return result;
}
}
} }

View File

@@ -1,62 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* 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;
}
}

View File

@@ -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();
}
}

View File

@@ -22,16 +22,16 @@ import org.jackhuang.hmcl.util.Immutable;
import java.util.Map; import java.util.Map;
@Immutable @Immutable
public final class ProfileTexture { public final class Texture {
private final String url; private final String url;
private final Map<String, String> metadata; private final Map<String, String> metadata;
public ProfileTexture() { public Texture() {
this(null, null); this(null, null);
} }
public ProfileTexture(String url, Map<String, String> metadata) { public Texture(String url, Map<String, String> metadata) {
this.url = url; this.url = url;
this.metadata = metadata; this.metadata = metadata;
} }
@@ -46,8 +46,4 @@ public final class ProfileTexture {
else else
return metadata.get(key); return metadata.get(key);
} }
public enum Type {
SKIN, CAPE
}
} }

View File

@@ -27,13 +27,13 @@ import java.util.UUID;
public final class TextureResponse { public final class TextureResponse {
private final UUID profileId; private final UUID profileId;
private final String profileName; private final String profileName;
private final Map<ProfileTexture.Type, ProfileTexture> textures; private final Map<TextureType, Texture> textures;
public TextureResponse() { public TextureResponse() {
this(UUID.randomUUID(), "", Collections.emptyMap()); this(UUID.randomUUID(), "", Collections.emptyMap());
} }
public TextureResponse(UUID profileId, String profileName, Map<ProfileTexture.Type, ProfileTexture> textures) { public TextureResponse(UUID profileId, String profileName, Map<TextureType, Texture> textures) {
this.profileId = profileId; this.profileId = profileId;
this.profileName = profileName; this.profileName = profileName;
this.textures = textures; this.textures = textures;
@@ -47,7 +47,7 @@ public final class TextureResponse {
return profileName; return profileName;
} }
public Map<ProfileTexture.Type, ProfileTexture> getTextures() { public Map<TextureType, Texture> getTextures() {
return textures == null ? null : Collections.unmodifiableMap(textures); return textures == null ? null : Collections.unmodifiableMap(textures);
} }
} }

View File

@@ -0,0 +1,5 @@
package org.jackhuang.hmcl.auth.yggdrasil;
public enum TextureType {
SKIN, CAPE
}

View File

@@ -1,42 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* 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;
}
}

View File

@@ -17,46 +17,34 @@
*/ */
package org.jackhuang.hmcl.auth.yggdrasil; 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.auth.*;
import org.jackhuang.hmcl.util.Charsets;
import org.jackhuang.hmcl.util.NetworkUtils;
import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.UUIDTypeAdapter; import org.jackhuang.hmcl.util.UUIDTypeAdapter;
import java.io.IOException;
import java.net.Proxy;
import java.net.URL;
import java.util.*; import java.util.*;
/** /**
* *
* @author huang * @author huangyuhui
*/ */
public class YggdrasilAccount extends Account { public class YggdrasilAccount extends Account {
private final String username; private final String username;
private String password; private final YggdrasilService service;
private String userId;
private String accessToken = null;
private String clientToken = randomToken();
private boolean isOnline = false; private boolean isOnline = false;
private PropertyMap userProperties = new PropertyMap(); private YggdrasilSession session;
private GameProfile selectedProfile = null; private final String clientToken;
private GameProfile[] profiles; private String character;
private UserType userType = UserType.LEGACY;
public YggdrasilAccount(String baseAuthServer, String baseSessionServer, String username) { protected YggdrasilAccount(YggdrasilService service, String username, String clientToken, String character, YggdrasilSession session) {
this.baseAuthServer = baseAuthServer; this.service = service;
this.baseSessionServer = baseSessionServer;
this.baseProfile = baseSessionServer + "session/minecraft/profile/";
this.username = username; this.username = username;
this.session = session;
this.clientToken = clientToken;
this.character = character;
this.routeAuthenticate = NetworkUtils.toURL(baseAuthServer + "authenticate"); if (session == null || session.getSelectedProfile() == null || StringUtils.isBlank(session.getAccessToken()))
this.routeRefresh = NetworkUtils.toURL(baseAuthServer + "refresh"); this.session = null;
this.routeValidate = NetworkUtils.toURL(baseAuthServer + "validate");
} }
@Override @Override
@@ -64,146 +52,68 @@ public class YggdrasilAccount extends Account {
return username; return username;
} }
void setPassword(String password) { @Override
this.password = password; public String getCharacter() {
} return session.getSelectedProfile().getName();
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;
} }
public boolean isLoggedIn() { public boolean isLoggedIn() {
return StringUtils.isNotBlank(accessToken); return session != null && StringUtils.isNotBlank(session.getAccessToken());
} }
public boolean canPlayOnline() { public boolean canPlayOnline() {
return isLoggedIn() && selectedProfile != null && isOnline; return isLoggedIn() && session.getSelectedProfile() != null && isOnline;
}
public boolean canLogIn() {
return !canPlayOnline() && StringUtils.isNotBlank(username)
&& (StringUtils.isNotBlank(password) || StringUtils.isNotBlank(accessToken));
} }
@Override @Override
public AuthInfo logIn(MultiCharacterSelector selector, Proxy proxy) throws AuthenticationException { public AuthInfo logIn() throws AuthenticationException {
if (canPlayOnline()) if (!canPlayOnline()) {
return new AuthInfo(selectedProfile, accessToken, userType, GSON.toJson(userProperties)); logInWithToken();
else { selectProfile(new SpecificCharacterSelector(character));
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));
} }
return toAuthInfo();
} }
@Override @Override
public AuthInfo logInWithPassword(MultiCharacterSelector selector, String password, Proxy proxy) throws AuthenticationException { public final AuthInfo logInWithPassword(String password) throws AuthenticationException {
logInWithPassword0(password, proxy); return logInWithPassword(password, new SpecificCharacterSelector(character));
if (!isLoggedIn()) }
throw new AuthenticationException("Wrong password for account " + username);
if (selectedProfile == null) { protected AuthInfo logInWithPassword(String password, CharacterSelector selector) throws AuthenticationException {
if (profiles == null || profiles.length <= 0) 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); 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 { private void logInWithToken() throws AuthenticationException {
if (StringUtils.isNotBlank(accessToken)) { if (service.validate(session.getAccessToken(), clientToken)) {
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)) {
isOnline = true; isOnline = true;
return; return;
} }
logIn1(routeRefresh, new RefreshRequest(accessToken, clientToken, getSelectedProfile()), proxy); session = service.refresh(session.getAccessToken(), clientToken);
} }
public void logInWithPassword0(String password, Proxy proxy) throws AuthenticationException { private AuthInfo toAuthInfo() {
logIn1(routeAuthenticate, new AuthenticationRequest(username, password, clientToken), proxy); GameProfile profile = session.getSelectedProfile();
}
private void logIn1(URL url, Object input, Proxy proxy) throws AuthenticationException { return new AuthInfo(profile.getName(), UUIDTypeAdapter.fromUUID(profile.getId()), session.getAccessToken(), profile.getUserType(),
AuthenticationResponse response = makeRequest(url, input, proxy) YggdrasilService.GSON.toJson(Optional.ofNullable(session.getUser()).map(User::getProperties).orElseGet(PropertyMap::new)));
.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());
} }
@Override @Override
public boolean canPlayOffline() { public boolean canPlayOffline() {
return isLoggedIn() && getSelectedProfile() != null && !canPlayOnline(); return isLoggedIn() && session.getSelectedProfile() != null && !canPlayOnline();
} }
@Override @Override
@@ -211,117 +121,51 @@ public class YggdrasilAccount extends Account {
if (!canPlayOffline()) if (!canPlayOffline())
throw new IllegalStateException("Current account " + this + " cannot play offline."); throw new IllegalStateException("Current account " + this + " cannot play offline.");
return new AuthInfo(selectedProfile, accessToken, userType, GSON.toJson(userProperties)); return toAuthInfo();
} }
@Override @Override
public void logOut() { public void logOut() {
password = null;
userId = null;
accessToken = null;
isOnline = false; isOnline = false;
userProperties.clear(); session = null;
profiles = null;
selectedProfile = null;
} }
@Override @Override
public Map<Object, Object> toStorageImpl() { public Map<Object, Object> toStorage() {
HashMap<Object, Object> result = new HashMap<>(); HashMap<Object, Object> storage = new HashMap<>();
result.put(STORAGE_KEY_USER_NAME, getUsername()); storage.put("username", getUsername());
result.put(STORAGE_KEY_CLIENT_TOKEN, getClientToken()); storage.put("clientToken", clientToken);
if (getCurrentCharacterName() != null) storage.put("character", character);
result.put(STORAGE_KEY_USER_ID, getCurrentCharacterName()); if (session != null)
if (!userProperties.isEmpty()) storage.putAll(session.toStorage());
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());
if (!profile.getProperties().isEmpty()) return storage;
result.put(STORAGE_KEY_PROFILE_PROPERTIES, profile.getProperties().toList());
}
if (StringUtils.isNotBlank(accessToken))
result.put(STORAGE_KEY_ACCESS_TOKEN, accessToken);
return result;
}
private Optional<AuthenticationResponse> 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;
}
} }
public UUID getUUID() { public UUID getUUID() {
if (getSelectedProfile() == null) if (session == null)
return null; return null;
else else
return getSelectedProfile().getId(); return session.getSelectedProfile().getId();
} }
public Optional<ProfileTexture> getSkin(GameProfile profile) throws IOException, JsonParseException { public Optional<Texture> getSkin() throws AuthenticationException {
if (StringUtils.isBlank(userId)) return getSkin(session.getSelectedProfile());
throw new IllegalStateException("Not logged in"); }
Property textureProperty; public Optional<Texture> getSkin(GameProfile profile) throws AuthenticationException {
if (!service.getTextures(profile).isPresent()) {
if (profile.getProperties().containsKey("textures")) session.setAvailableProfile(profile = service.getCompleteGameProfile(profile.getId()));
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());
} }
TextureResponse texture; return service.getTextures(profile).map(map -> map.get(TextureType.SKIN));
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));
} }
@Override @Override
public void clearCache() { public void clearCache() {
Optional.ofNullable(getSelectedProfile()) Optional.ofNullable(session)
.map(YggdrasilSession::getSelectedProfile)
.map(GameProfile::getProperties) .map(GameProfile::getProperties)
.ifPresent(it -> it.remove("texture")); .ifPresent(it -> it.remove("texture"));
} }
@@ -331,30 +175,4 @@ public class YggdrasilAccount extends Account {
return "YggdrasilAccount[username=" + getUsername() + "]"; 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();
} }

View File

@@ -18,14 +18,15 @@
package org.jackhuang.hmcl.auth.yggdrasil; package org.jackhuang.hmcl.auth.yggdrasil;
import org.jackhuang.hmcl.auth.AccountFactory; 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.Lang;
import org.jackhuang.hmcl.util.UUIDTypeAdapter; import org.jackhuang.hmcl.util.UUIDTypeAdapter;
import java.util.List; import java.net.Proxy;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Objects;
import java.util.UUID;
import static org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount.*;
/** /**
* *
@@ -33,50 +34,40 @@ import static org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount.*;
*/ */
public class YggdrasilAccountFactory extends AccountFactory<YggdrasilAccount> { public class YggdrasilAccountFactory extends AccountFactory<YggdrasilAccount> {
private final String baseAuthServer; private final YggdrasilProvider provider;
private final String baseSessionServer;
public YggdrasilAccountFactory() { public YggdrasilAccountFactory(YggdrasilProvider provider) {
this(MOJANG_AUTH_SERVER, MOJANG_SESSION_SERVER); this.provider = provider;
}
public YggdrasilAccountFactory(String baseAuthServer, String baseSessionServer) {
this.baseAuthServer = baseAuthServer;
this.baseSessionServer = baseSessionServer;
} }
@Override @Override
public YggdrasilAccount fromUsername(String username, String password, Object additionalData) { public YggdrasilAccount create(CharacterSelector selector, String username, String password, Object additionalData, Proxy proxy) throws AuthenticationException {
YggdrasilAccount account = new YggdrasilAccount(MOJANG_AUTH_SERVER, MOJANG_SESSION_SERVER, username); Objects.requireNonNull(selector);
account.setPassword(password); 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; return account;
} }
@Override @Override
public YggdrasilAccount fromStorageImpl(Map<Object, Object> storage) { public YggdrasilAccount fromStorage(Map<Object, Object> storage, Proxy proxy) {
String username = Lang.get(storage, STORAGE_KEY_USER_NAME, String.class) Objects.requireNonNull(storage);
.orElseThrow(() -> new IllegalArgumentException("storage does not have key " + STORAGE_KEY_USER_NAME)); Objects.requireNonNull(proxy);
YggdrasilAccount account = new YggdrasilAccount(baseAuthServer, baseSessionServer, username); String username = Lang.get(storage, "username", String.class)
account.setUserId(Lang.get(storage, STORAGE_KEY_USER_ID, String.class).orElse(username)); .orElseThrow(() -> new IllegalArgumentException("storage does not have username"));
account.setAccessToken(Lang.get(storage, STORAGE_KEY_ACCESS_TOKEN, String.class).orElse(null)); String clientToken = Lang.get(storage, "clientToken", String.class)
account.setClientToken(Lang.get(storage, STORAGE_KEY_CLIENT_TOKEN, String.class) .orElseThrow(() -> new IllegalArgumentException("storage does not have client token."));
.orElseThrow(() -> new IllegalArgumentException("storage does not have key " + STORAGE_KEY_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) return new YggdrasilAccount(new YggdrasilService(provider, proxy), username, clientToken, character, YggdrasilSession.fromStorage(storage));
.ifPresent(account.getUserProperties()::fromList);
Optional<String> profileId = Lang.get(storage, STORAGE_KEY_PROFILE_ID, String.class);
Optional<String> 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;
} }
private static final String MOJANG_AUTH_SERVER = "https://authserver.mojang.com/"; public static String randomToken() {
private static final String MOJANG_SESSION_SERVER = "https://sessionserver.mojang.com/"; return UUIDTypeAdapter.fromUUID(UUID.randomUUID());
}
} }

View File

@@ -0,0 +1,21 @@
package org.jackhuang.hmcl.auth.yggdrasil;
import java.net.URL;
import java.util.UUID;
/**
* @see <a href="http://wiki.vg">http://wiki.vg</a>
*/
public interface YggdrasilProvider {
URL getAuthenticationURL();
URL getRefreshmentURL();
URL getValidationURL();
URL getInvalidationURL();
URL getProfilePropertiesURL(UUID uuid);
}

View File

@@ -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<String, Object> 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<Map<TextureType, Texture>> 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> T fromJson(String text, Class<T> 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;
}
}
}

View File

@@ -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<String> profileId = Lang.get(storage, "uuid", String.class);
Optional<String> 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<Object, Object> toStorage() {
Map<Object, Object> 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;
}
}

View File

@@ -47,7 +47,7 @@ public class LaunchOptions implements Serializable {
private String proxyUser; private String proxyUser;
private String proxyPass; private String proxyPass;
private boolean noGeneratedJVMArgs; private boolean noGeneratedJVMArgs;
private String precalledCommand; private String preLaunchCommand;
/** /**
* The game directory * The game directory
@@ -188,8 +188,8 @@ public class LaunchOptions implements Serializable {
/** /**
* Called command line before launching the game. * Called command line before launching the game.
*/ */
public String getPrecalledCommand() { public String getPreLaunchCommand() {
return precalledCommand; return preLaunchCommand;
} }
public static class Builder { public static class Builder {
@@ -296,7 +296,7 @@ public class LaunchOptions implements Serializable {
} }
public Builder setPrecalledCommand(String precalledCommand) { public Builder setPrecalledCommand(String precalledCommand) {
options.precalledCommand = precalledCommand; options.preLaunchCommand = precalledCommand;
return this; return this;
} }

View File

@@ -240,8 +240,8 @@ public class DefaultLauncher extends Launcher {
protected Map<String, String> getConfigurations() { protected Map<String, String> getConfigurations() {
return Lang.mapOf( return Lang.mapOf(
new Pair<>("${auth_player_name}", authInfo.getUsername()), new Pair<>("${auth_player_name}", authInfo.getUsername()),
new Pair<>("${auth_session}", authInfo.getAuthToken()), new Pair<>("${auth_session}", authInfo.getAccessToken()),
new Pair<>("${auth_access_token}", authInfo.getAuthToken()), new Pair<>("${auth_access_token}", authInfo.getAccessToken()),
new Pair<>("${auth_uuid}", authInfo.getUserId()), new Pair<>("${auth_uuid}", authInfo.getUserId()),
new Pair<>("${version_name}", Optional.ofNullable(options.getVersionName()).orElse(version.getId())), new Pair<>("${version_name}", Optional.ofNullable(options.getVersionName()).orElse(version.getId())),
new Pair<>("${profile_name}", Optional.ofNullable(options.getProfileName()).orElse("Minecraft")), new Pair<>("${profile_name}", Optional.ofNullable(options.getProfileName()).orElse("Minecraft")),
@@ -262,8 +262,8 @@ public class DefaultLauncher extends Launcher {
decompressNatives(nativeFolder); decompressNatives(nativeFolder);
if (StringUtils.isNotBlank(options.getPrecalledCommand())) if (StringUtils.isNotBlank(options.getPreLaunchCommand()))
Runtime.getRuntime().exec(options.getPrecalledCommand()).waitFor(); Runtime.getRuntime().exec(options.getPreLaunchCommand()).waitFor();
Process process; Process process;
try { try {
@@ -306,8 +306,8 @@ public class DefaultLauncher extends Launcher {
writer.write("cd /D %APPDATA%"); writer.write("cd /D %APPDATA%");
writer.newLine(); writer.newLine();
} }
if (StringUtils.isNotBlank(options.getPrecalledCommand())) { if (StringUtils.isNotBlank(options.getPreLaunchCommand())) {
writer.write(options.getPrecalledCommand()); writer.write(options.getPreLaunchCommand());
writer.newLine(); writer.newLine();
} }
writer.write(StringUtils.makeCommand(generateCommandLine(nativeFolder))); writer.write(StringUtils.makeCommand(generateCommandLine(nativeFolder)));

View File

@@ -270,7 +270,7 @@ public abstract class Task {
} }
else { else {
if (failure != null) if (failure != null)
failure.accept(variables.get(TaskExecutor.LAST_EXCEPION_ID)); failure.accept(variables.get(TaskExecutor.LAST_EXCEPTION_ID));
} }
}); });
} }

View File

@@ -20,10 +20,7 @@ package org.jackhuang.hmcl.task;
import org.jackhuang.hmcl.util.*; import org.jackhuang.hmcl.util.*;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level; import java.util.logging.Level;
@@ -109,7 +106,7 @@ public final class TaskExecutor {
return false; return false;
Invoker invoker = new Invoker(task, latch, success); Invoker invoker = new Invoker(task, latch, success);
try { try {
Future<?> future = task.getScheduler().schedule(invoker); Future<?> future = scheduler.schedule(invoker);
if (future != null) if (future != null)
workerQueue.add(future); workerQueue.add(future);
} catch (RejectedExecutionException e) { } catch (RejectedExecutionException e) {
@@ -152,7 +149,12 @@ public final class TaskExecutor {
task.setDependentsSucceeded(); task.setDependentsSucceeded();
task.setVariables(variables); task.setVariables(variables);
task.execute();
try {
task.getScheduler().schedule(task::execute).get();
} catch (ExecutionException e) {
throw (Exception) e.getCause();
}
if (task instanceof TaskResult<?>) { if (task instanceof TaskResult<?>) {
TaskResult<?> taskResult = (TaskResult<?>) task; TaskResult<?> taskResult = (TaskResult<?>) task;
@@ -181,7 +183,7 @@ public final class TaskExecutor {
// do nothing // do nothing
} catch (Exception e) { } catch (Exception e) {
lastException = 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); Logging.LOG.log(Level.FINE, "Task failed: " + task.getName(), e);
task.onDone().fireEvent(new TaskEvent(this, task, true)); task.onDone().fireEvent(new TaskEvent(this, task, true));
taskListeners.forEach(it -> it.onFailed(task, e)); 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";
} }

View File

@@ -58,8 +58,8 @@ public final class IOUtils {
return readFully(stream).toString(); return readFully(stream).toString();
} }
public static String readFullyAsString(InputStream stream, Charset charset) { public static String readFullyAsString(InputStream stream, Charset charset) throws IOException {
return Lang.invoke(() -> readFully(stream).toString(charset.name())); return readFully(stream).toString(charset.name());
} }
public static void copyTo(InputStream src, OutputStream dest) throws IOException { public static void copyTo(InputStream src, OutputStream dest) throws IOException {

View File

@@ -87,7 +87,7 @@ public final class Lang {
return r -> invoke(function, r); return r -> invoke(function, r);
} }
public static <T, R, E extends Exception> Function<T, R> liftFunction(ExceptionalFunction<T, R, E> function) { public static <T, R, E extends Exception> Function<T, R> liftFunction(ExceptionalFunction<T, R, E> function) throws E {
return hideFunction(function); return hideFunction(function);
} }
@@ -115,7 +115,7 @@ public final class Lang {
return () -> invoke(supplier); return () -> invoke(supplier);
} }
public static <T, E extends Exception> Supplier<T> liftException(ExceptionalSupplier<T, E> supplier) { public static <T, E extends Exception> Supplier<T> liftException(ExceptionalSupplier<T, E> supplier) throws E {
return hideException(supplier); return hideException(supplier);
} }
@@ -141,7 +141,7 @@ public final class Lang {
return it -> invokeConsumer(consumer, it); return it -> invokeConsumer(consumer, it);
} }
public static <T, E extends Exception> Consumer<T> liftConsumer(ExceptionalConsumer<T, E> consumer) { public static <T, E extends Exception> Consumer<T> liftConsumer(ExceptionalConsumer<T, E> consumer) throws E {
return hideConsumer(consumer); return hideConsumer(consumer);
} }

View File

@@ -26,7 +26,6 @@ import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter; import com.google.gson.stream.JsonWriter;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.Locale; import java.util.Locale;

View File

@@ -85,10 +85,10 @@ public final class NetworkUtils {
return doPost(u, post, contentType, Proxy.NO_PROXY); 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); byte[] bytes = post.getBytes(Charsets.UTF_8);
HttpURLConnection con = createConnection(u, proxy); HttpURLConnection con = createConnection(url, proxy);
con.setRequestMethod("POST"); con.setRequestMethod("POST");
con.setDoOutput(true); con.setDoOutput(true);
con.setRequestProperty("Content-Type", contentType + "; charset=utf-8"); con.setRequestProperty("Content-Type", contentType + "; charset=utf-8");