From dd4683e693caf6f5da5e62e14c75c1152daf2445 Mon Sep 17 00:00:00 2001 From: yuhuihuang Date: Thu, 24 Dec 2020 20:32:47 +0800 Subject: [PATCH] fix: Microsoft Account login --- .../jackhuang/hmcl/game/TexturesLoader.java | 32 +++++++++- .../hmcl/ui/account/AddAccountPane.java | 2 +- .../java/org/jackhuang/hmcl/auth/Account.java | 8 +++ .../hmcl/auth/CharacterSelector.java | 2 +- .../hmcl/auth/microsoft/MicrosoftAccount.java | 11 ++++ .../hmcl/auth/microsoft/MicrosoftService.java | 58 +++++++++++++++++-- .../hmcl/auth/microsoft/MicrosoftSession.java | 4 ++ .../hmcl/auth/yggdrasil/YggdrasilAccount.java | 17 ++++++ .../hmcl/auth/yggdrasil/YggdrasilService.java | 2 +- 9 files changed, 125 insertions(+), 11 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java index e5b4dc185..d8738a38d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java @@ -24,6 +24,8 @@ import javafx.scene.image.Image; import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.ServerResponseMalformedException; +import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount; +import org.jackhuang.hmcl.auth.microsoft.MicrosoftService; import org.jackhuang.hmcl.auth.yggdrasil.Texture; import org.jackhuang.hmcl.auth.yggdrasil.TextureModel; import org.jackhuang.hmcl.auth.yggdrasil.TextureType; @@ -181,6 +183,30 @@ public final class TexturesLoader { } }, uuidFallback); } + + public static ObjectBinding skinBinding(Account account) { + LoadedTexture uuidFallback = getDefaultSkin(TextureModel.detectUUID(account.getUUID())); + return BindingMapping.of(account.getTextures()) + .map(textures -> textures + .flatMap(it -> Optional.ofNullable(it.get(TextureType.SKIN))) + .filter(it -> StringUtils.isNotBlank(it.getUrl()))) + .asyncMap(it -> { + if (it.isPresent()) { + Texture texture = it.get(); + return CompletableFuture.supplyAsync(() -> { + try { + return loadTexture(texture); + } catch (IOException e) { + LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e); + return uuidFallback; + } + }, POOL); + } else { + return CompletableFuture.completedFuture(uuidFallback); + } + }, uuidFallback); + } + // ==== // ==== Avatar ==== @@ -209,8 +235,10 @@ public final class TexturesLoader { } public static ObjectBinding fxAvatarBinding(Account account, int size) { - if (account instanceof YggdrasilAccount) { - return fxAvatarBinding(((YggdrasilAccount) account).getYggdrasilService(), account.getUUID(), size); + if (account instanceof YggdrasilAccount || account instanceof MicrosoftAccount) { + return BindingMapping.of(skinBinding(account)) + .map(it -> toAvatar(it.image, size)) + .map(img -> SwingFXUtils.toFXImage(img, null)); } else { return Bindings.createObjectBinding( () -> SwingFXUtils.toFXImage(toAvatar(getDefaultSkin(TextureModel.detectUUID(account.getUUID())).image, size), null)); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AddAccountPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AddAccountPane.java index 44242c45b..b408bc425 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AddAccountPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AddAccountPane.java @@ -258,7 +258,7 @@ public class AddAccountPane extends StackPane { Controllers.dialog(new AddAuthlibInjectorServerPane()); } - private class Selector extends BorderPane implements CharacterSelector { + private static class Selector extends BorderPane implements CharacterSelector { private final AdvancedListBox listBox = new AdvancedListBox(); private final JFXButton cancel = new JFXButton(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/Account.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/Account.java index 32104f719..9d7751cb1 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/Account.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/Account.java @@ -21,6 +21,10 @@ import javafx.application.Platform; import javafx.beans.InvalidationListener; import javafx.beans.Observable; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.ObjectBinding; +import org.jackhuang.hmcl.auth.yggdrasil.Texture; +import org.jackhuang.hmcl.auth.yggdrasil.TextureType; import org.jackhuang.hmcl.util.ToStringBuilder; import org.jackhuang.hmcl.util.javafx.ObservableHelper; @@ -92,6 +96,10 @@ public abstract class Account implements Observable { Platform.runLater(helper::invalidate); } + public ObjectBinding>> getTextures() { + return Bindings.createObjectBinding(Optional::empty); + } + @Override public String toString() { return new ToStringBuilder(this) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/CharacterSelector.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/CharacterSelector.java index 35dcb344e..39e5777c2 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/CharacterSelector.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/CharacterSelector.java @@ -24,7 +24,7 @@ import java.util.List; /** * This interface is for your application to open a GUI for user to choose the character - * when a having-multi-character yggdrasil account is being logging in.. + * when a having-multi-character yggdrasil account is being logging in. */ public interface CharacterSelector { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftAccount.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftAccount.java index c91ddcc09..219b4eddc 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftAccount.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftAccount.java @@ -17,7 +17,11 @@ */ package org.jackhuang.hmcl.auth.microsoft; +import javafx.beans.binding.ObjectBinding; import org.jackhuang.hmcl.auth.*; +import org.jackhuang.hmcl.auth.yggdrasil.Texture; +import org.jackhuang.hmcl.auth.yggdrasil.TextureType; +import org.jackhuang.hmcl.util.javafx.BindingMapping; import java.util.Map; import java.util.Optional; @@ -109,6 +113,13 @@ public class MicrosoftAccount extends Account { return service; } + @Override + public ObjectBinding>> getTextures() { + return BindingMapping.of(service.getProfileRepository().binding(session.getAuthorization())) + .map(profile -> profile.flatMap(MicrosoftService::getTextures)); + + } + @Override public void clearCache() { authenticated = false; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftService.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftService.java index 42f9b559d..83897d702 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftService.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftService.java @@ -23,33 +23,53 @@ import org.jackhuang.hmcl.auth.AuthenticationException; import org.jackhuang.hmcl.auth.NoCharacterException; import org.jackhuang.hmcl.auth.ServerDisconnectException; import org.jackhuang.hmcl.auth.ServerResponseMalformedException; -import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException; +import org.jackhuang.hmcl.auth.yggdrasil.*; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.gson.TolerableValidationException; import org.jackhuang.hmcl.util.gson.Validation; import org.jackhuang.hmcl.util.io.HttpRequest; import org.jackhuang.hmcl.util.io.NetworkUtils; import org.jackhuang.hmcl.util.io.ResponseCodeException; +import org.jackhuang.hmcl.util.javafx.ObservableOptionalCache; import java.io.IOException; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; import java.util.function.Predicate; +import java.util.logging.Level; import java.util.regex.Matcher; import java.util.regex.Pattern; import static java.util.Objects.requireNonNull; import static org.jackhuang.hmcl.util.Lang.mapOf; +import static org.jackhuang.hmcl.util.Lang.threadPool; +import static org.jackhuang.hmcl.util.Logging.LOG; import static org.jackhuang.hmcl.util.Pair.pair; public class MicrosoftService { + private static final ThreadPoolExecutor POOL = threadPool("MicrosoftProfileProperties", true, 2, 10, TimeUnit.SECONDS); private static final Pattern OAUTH_URL_PATTERN = Pattern.compile("^https://login\\.live\\.com/oauth20_desktop\\.srf\\?code=(.*?)&lc=(.*?)$"); private final WebViewCallback callback; + private final ObservableOptionalCache profileRepository; + public MicrosoftService(WebViewCallback callback) { this.callback = callback; + this.profileRepository = new ObservableOptionalCache<>( + authorization -> { + LOG.info("Fetching properties"); + return getCompleteProfile(authorization); + }, + (uuid, e) -> LOG.log(Level.WARNING, "Failed to fetch properties of " + uuid, e), + POOL); + } + + public ObservableOptionalCache getProfileRepository() { + return profileRepository; } public MicrosoftSession authenticate() throws AuthenticationException { @@ -139,6 +159,18 @@ public class MicrosoftService { } } + public Optional getCompleteProfile(String authorization) throws AuthenticationException { + try { + return Optional.ofNullable(HttpRequest.GET(NetworkUtils.toURL("https://api.minecraftservices.com/minecraft/profile")) + .authorization(authorization) + .getJson(MinecraftProfileResponse.class)); + } catch (IOException e) { + throw new ServerDisconnectException(e); + } catch (JsonParseException e) { + throw new ServerResponseMalformedException(e); + } + } + public boolean validate(String tokenType, String accessToken) throws AuthenticationException { requireNonNull(tokenType); requireNonNull(accessToken); @@ -166,6 +198,21 @@ public class MicrosoftService { } } + public static Optional> getTextures(MinecraftProfileResponse profile) { + Objects.requireNonNull(profile); + + Map textures = new EnumMap<>(TextureType.class); + + if (!profile.skins.isEmpty()) { + textures.put(TextureType.SKIN, new Texture(profile.skins.get(0).url, null)); + } +// if (!profile.capes.isEmpty()) { +// textures.put(TextureType.CAPE, new Texture(profile.capes.get(0).url, null); +// } + + return Optional.of(textures); + } + private static class LiveAuthorizationResponse { @SerializedName("token_type") String tokenType; @@ -242,11 +289,11 @@ public class MicrosoftService { String keyId; } - private static class MinecraftProfileResponseSkin implements Validation { + public static class MinecraftProfileResponseSkin implements Validation { public String id; public String state; public String url; - public String variant; + public String variant; // CLASSIC, SLIM public String alias; @Override @@ -255,15 +302,14 @@ public class MicrosoftService { Validation.requireNonNull(state, "state cannot be null"); Validation.requireNonNull(url, "url cannot be null"); Validation.requireNonNull(variant, "variant cannot be null"); - Validation.requireNonNull(alias, "alias cannot be null"); } } - private static class MinecraftProfileResponseCape { + public static class MinecraftProfileResponseCape { } - private static class MinecraftProfileResponse extends MinecraftErrorResponse implements Validation { + public static class MinecraftProfileResponse extends MinecraftErrorResponse implements Validation { @SerializedName("id") UUID id; @SerializedName("name") diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftSession.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftSession.java index b885fc1a6..bf3e808e4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftSession.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/microsoft/MicrosoftSession.java @@ -49,6 +49,10 @@ public class MicrosoftSession { return accessToken; } + public String getAuthorization() { + return String.format("%s %s", getTokenType(), getAccessToken()); + } + public User getUser() { return user; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccount.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccount.java index 735d93cf9..108d17adb 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccount.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilAccount.java @@ -27,11 +27,14 @@ import org.jackhuang.hmcl.auth.CredentialExpiredException; import org.jackhuang.hmcl.auth.NoCharacterException; import org.jackhuang.hmcl.auth.ServerResponseMalformedException; import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter; +import org.jackhuang.hmcl.util.javafx.BindingMapping; import java.nio.file.Path; import java.util.*; +import java.util.logging.Level; import static java.util.Objects.requireNonNull; +import static org.jackhuang.hmcl.util.Logging.LOG; public class YggdrasilAccount extends Account { @@ -189,6 +192,20 @@ public class YggdrasilAccount extends Account { service.getProfileRepository().invalidate(characterUUID); } + @Override + public ObjectBinding>> getTextures() { + return BindingMapping.of(service.getProfileRepository().binding(getUUID())) + .map(profile -> profile.flatMap(it -> { + try { + return YggdrasilService.getTextures(it); + } catch (ServerResponseMalformedException e) { + LOG.log(Level.WARNING, "Failed to parse texture payload", e); + return Optional.empty(); + } + })); + + } + public void uploadSkin(String model, Path file) throws AuthenticationException, UnsupportedOperationException { service.uploadSkin(characterUUID, session.getAccessToken(), model, file); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilService.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilService.java index ed8dd1f01..9165c11c1 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilService.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/yggdrasil/YggdrasilService.java @@ -52,7 +52,7 @@ import static org.jackhuang.hmcl.util.Pair.pair; public class YggdrasilService { - private static final ThreadPoolExecutor POOL = threadPool("ProfileProperties", true, 2, 10, TimeUnit.SECONDS); + private static final ThreadPoolExecutor POOL = threadPool("YggdrasilProfileProperties", true, 2, 10, TimeUnit.SECONDS); public static final YggdrasilService MOJANG = new YggdrasilService(new MojangYggdrasilProvider());