Merge branch 'javafx' into aarch64

This commit is contained in:
Glavo
2021-09-08 14:56:25 +08:00
committed by GitHub
46 changed files with 1051 additions and 420 deletions

View File

@@ -50,25 +50,33 @@ public abstract class AccountFactory<T extends Account> {
}
}
public interface ProgressCallback {
void onProgressChanged(String stageName);
}
/**
* Informs how this account factory verifies user's credential.
*
* @see AccountLoginType
*/
public abstract AccountLoginType getLoginType();
/**
* Create a new(to be verified via network) account, and log in.
* @param selector for character selection if multiple characters belong to single account. Pick out which character to act as.
* @param username username of the account if needed.
* @param password password of the account if needed.
* @param additionalData extra data for specific account factory.
*
* @param selector for character selection if multiple characters belong to single account. Pick out which character to act as.
* @param username username of the account if needed.
* @param password password of the account if needed.
* @param progressCallback notify login progress.
* @param additionalData extra data for specific account factory.
* @return logged-in account.
* @throws AuthenticationException if an error occurs when logging in.
*/
public abstract T create(CharacterSelector selector, String username, String password, Object additionalData) throws AuthenticationException;
public abstract T create(CharacterSelector selector, String username, String password, ProgressCallback progressCallback, Object additionalData) throws AuthenticationException;
/**
* Create a existing(stored in local files) account.
*
* @param storage serialized account data.
* @return account stored in local storage. Credentials may expired, and you should refresh account state later.
*/

View File

@@ -50,7 +50,7 @@ public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjecto
}
@Override
public AuthlibInjectorAccount create(CharacterSelector selector, String username, String password, Object additionalData) throws AuthenticationException {
public AuthlibInjectorAccount create(CharacterSelector selector, String username, String password, ProgressCallback progressCallback, Object additionalData) throws AuthenticationException {
Objects.requireNonNull(selector);
Objects.requireNonNull(username);
Objects.requireNonNull(password);

View File

@@ -21,14 +21,17 @@ 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.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.logging.Level;
import static java.util.Objects.requireNonNull;
import static org.jackhuang.hmcl.util.Logging.LOG;
public class MicrosoftAccount extends Account {
@@ -77,7 +80,7 @@ public class MicrosoftAccount extends Account {
@Override
public AuthInfo logIn() throws AuthenticationException {
if (!authenticated) {
if (service.validate(session.getTokenType(), session.getAccessToken())) {
if (service.validate(session.getNotAfter(), session.getTokenType(), session.getAccessToken())) {
authenticated = true;
} else {
MicrosoftSession acquiredSession = service.refresh(session);
@@ -116,9 +119,15 @@ public class MicrosoftAccount extends Account {
@Override
public ObjectBinding<Optional<Map<TextureType, Texture>>> getTextures() {
return BindingMapping.of(service.getProfileRepository().binding(session.getAuthorization()))
.map(profile -> profile.flatMap(MicrosoftService::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();
}
}));
}
@Override

View File

@@ -38,7 +38,7 @@ public class MicrosoftAccountFactory extends AccountFactory<MicrosoftAccount> {
}
@Override
public MicrosoftAccount create(CharacterSelector selector, String username, String password, Object additionalData) throws AuthenticationException {
public MicrosoftAccount create(CharacterSelector selector, String username, String password, ProgressCallback progressCallback, Object additionalData) throws AuthenticationException {
Objects.requireNonNull(selector);
return new MicrosoftAccount(service, selector);

View File

@@ -17,15 +17,16 @@
*/
package org.jackhuang.hmcl.auth.microsoft;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.auth.yggdrasil.CompleteGameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
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.gson.*;
import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.io.ResponseCodeException;
@@ -60,17 +61,17 @@ public class MicrosoftService {
private final OAuthCallback callback;
private final ObservableOptionalCache<String, MinecraftProfileResponse, AuthenticationException> profileRepository;
private final ObservableOptionalCache<UUID, CompleteGameProfile, AuthenticationException> profileRepository;
public MicrosoftService(OAuthCallback callback) {
this.callback = requireNonNull(callback);
this.profileRepository = new ObservableOptionalCache<>(authorization -> {
LOG.info("Fetching properties");
return getCompleteProfile(authorization);
this.profileRepository = new ObservableOptionalCache<>(uuid -> {
LOG.info("Fetching properties of " + uuid);
return getCompleteGameProfile(uuid);
}, (uuid, e) -> LOG.log(Level.WARNING, "Failed to fetch properties of " + uuid, e), POOL);
}
public ObservableOptionalCache<String, MinecraftProfileResponse, AuthenticationException> getProfileRepository() {
public ObservableOptionalCache<UUID, CompleteGameProfile, AuthenticationException> getProfileRepository() {
return profileRepository;
}
@@ -213,10 +214,14 @@ public class MicrosoftService {
}
}
public boolean validate(String tokenType, String accessToken) throws AuthenticationException {
public boolean validate(long notAfter, String tokenType, String accessToken) throws AuthenticationException {
requireNonNull(tokenType);
requireNonNull(accessToken);
if (System.currentTimeMillis() > notAfter) {
return false;
}
try {
getMinecraftProfile(tokenType, accessToken);
return true;
@@ -266,7 +271,7 @@ public class MicrosoftService {
.createConnection();
int responseCode = conn.getResponseCode();
if (responseCode == HTTP_NOT_FOUND) {
throw new NoCharacterException();
throw new NoMinecraftJavaEditionProfileException();
} else if (responseCode != 200) {
throw new ResponseCodeException(new URL("https://api.minecraftservices.com/minecraft/profile"), responseCode);
}
@@ -275,6 +280,31 @@ public class MicrosoftService {
return JsonUtils.fromNonNullJson(result, MinecraftProfileResponse.class);
}
public Optional<CompleteGameProfile> getCompleteGameProfile(UUID uuid) throws AuthenticationException {
Objects.requireNonNull(uuid);
return Optional.ofNullable(GSON.fromJson(request(NetworkUtils.toURL("https://sessionserver.mojang.com/session/minecraft/profile/" + UUIDTypeAdapter.fromUUID(uuid)), null), CompleteGameProfile.class));
}
private static String request(URL url, Object payload) throws AuthenticationException {
try {
if (payload == null)
return NetworkUtils.doGet(url);
else
return NetworkUtils.doPost(url, payload instanceof String ? (String) payload : GSON.toJson(payload), "application/json");
} catch (IOException e) {
throw new ServerDisconnectException(e);
}
}
private static <T> T fromJson(String text, Class<T> typeOfT) throws ServerResponseMalformedException {
try {
return GSON.fromJson(text, typeOfT);
} catch (JsonParseException e) {
throw new ServerResponseMalformedException(text, e);
}
}
public static class XboxAuthorizationException extends AuthenticationException {
private final long errorCode;
@@ -290,6 +320,9 @@ public class MicrosoftService {
public static final long ADD_FAMILY = 2148916238L;
}
public static class NoMinecraftJavaEditionProfileException extends AuthenticationException {
}
/**
* Error response: {"error":"invalid_grant","error_description":"The provided
* value for the 'redirect_uri' is not valid. The value must exactly match the
@@ -487,4 +520,9 @@ public class MicrosoftService {
String waitFor() throws InterruptedException, ExecutionException;
}
private static final Gson GSON = new GsonBuilder()
.registerTypeAdapter(UUID.class, UUIDTypeAdapter.INSTANCE)
.registerTypeAdapterFactory(ValidationTypeAdapterFactory.INSTANCE)
.create();
}

View File

@@ -47,7 +47,7 @@ public final class OfflineAccountFactory extends AccountFactory<OfflineAccount>
}
@Override
public OfflineAccount create(CharacterSelector selector, String username, String password, Object additionalData) {
public OfflineAccount create(CharacterSelector selector, String username, String password, ProgressCallback progressCallback, Object additionalData) {
return new OfflineAccount(username, getUUIDFromUserName(username));
}

View File

@@ -48,7 +48,7 @@ public class YggdrasilAccountFactory extends AccountFactory<YggdrasilAccount> {
}
@Override
public YggdrasilAccount create(CharacterSelector selector, String username, String password, Object additionalData) throws AuthenticationException {
public YggdrasilAccount create(CharacterSelector selector, String username, String password, ProgressCallback progressCallback, Object additionalData) throws AuthenticationException {
Objects.requireNonNull(selector);
Objects.requireNonNull(username);
Objects.requireNonNull(password);