fix: Microsoft Account login

This commit is contained in:
yuhuihuang
2020-12-24 20:32:47 +08:00
parent 8bc5d2112f
commit dd4683e693
9 changed files with 125 additions and 11 deletions

View File

@@ -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<Optional<Map<TextureType, Texture>>> getTextures() {
return Bindings.createObjectBinding(Optional::empty);
}
@Override
public String toString() {
return new ToStringBuilder(this)

View File

@@ -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 {

View File

@@ -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<Optional<Map<TextureType, Texture>>> getTextures() {
return BindingMapping.of(service.getProfileRepository().binding(session.getAuthorization()))
.map(profile -> profile.flatMap(MicrosoftService::getTextures));
}
@Override
public void clearCache() {
authenticated = false;

View File

@@ -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<String, MinecraftProfileResponse, AuthenticationException> 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<String, MinecraftProfileResponse, AuthenticationException> getProfileRepository() {
return profileRepository;
}
public MicrosoftSession authenticate() throws AuthenticationException {
@@ -139,6 +159,18 @@ public class MicrosoftService {
}
}
public Optional<MinecraftProfileResponse> 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<Map<TextureType, Texture>> getTextures(MinecraftProfileResponse profile) {
Objects.requireNonNull(profile);
Map<TextureType, Texture> 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")

View File

@@ -49,6 +49,10 @@ public class MicrosoftSession {
return accessToken;
}
public String getAuthorization() {
return String.format("%s %s", getTokenType(), getAccessToken());
}
public User getUser() {
return user;
}

View File

@@ -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<Optional<Map<TextureType, Texture>>> 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);
}

View File

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