重构YggdrasilService相关代码
This commit is contained in:
@@ -302,7 +302,7 @@ public final class LauncherHelper {
|
||||
else
|
||||
forbiddenTokens = mapOf(
|
||||
pair(authInfo.getAccessToken(), "<access token>"),
|
||||
pair(authInfo.getUserId(), "<uuid>"),
|
||||
pair(UUIDTypeAdapter.fromUUID(authInfo.getUUID()), "<uuid>"),
|
||||
pair(authInfo.getUsername(), "<player>")
|
||||
);
|
||||
|
||||
|
||||
@@ -61,8 +61,6 @@ public abstract class Account {
|
||||
*/
|
||||
public abstract AuthInfo playOffline();
|
||||
|
||||
public abstract void logOut();
|
||||
|
||||
public abstract Map<Object, Object> toStorage();
|
||||
|
||||
public abstract void clearCache();
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.auth;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import org.jackhuang.hmcl.game.Arguments;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
|
||||
@@ -28,27 +30,19 @@ import org.jackhuang.hmcl.util.Immutable;
|
||||
public final class AuthInfo {
|
||||
|
||||
private final String username;
|
||||
private final String userId;
|
||||
private final UUID uuid;
|
||||
private final String accessToken;
|
||||
private final UserType userType;
|
||||
private final String userProperties;
|
||||
private final Arguments arguments;
|
||||
|
||||
public AuthInfo(String username, String userId, String accessToken) {
|
||||
this(username, userId, accessToken, UserType.LEGACY);
|
||||
public AuthInfo(String username, UUID uuid, String accessToken, UserType userType, String userProperties) {
|
||||
this(username, uuid, accessToken, userType, userProperties, null);
|
||||
}
|
||||
|
||||
public AuthInfo(String username, String userId, String accessToken, UserType userType) {
|
||||
this(username, userId, accessToken, userType, "{}");
|
||||
}
|
||||
|
||||
public AuthInfo(String username, String userId, String accessToken, UserType userType, String userProperties) {
|
||||
this(username, userId, accessToken, userType, userProperties, null);
|
||||
}
|
||||
|
||||
public AuthInfo(String username, String userId, String accessToken, UserType userType, String userProperties, Arguments arguments) {
|
||||
public AuthInfo(String username, UUID uuid, String accessToken, UserType userType, String userProperties, Arguments arguments) {
|
||||
this.username = username;
|
||||
this.userId = userId;
|
||||
this.uuid = uuid;
|
||||
this.accessToken = accessToken;
|
||||
this.userType = userType;
|
||||
this.userProperties = userProperties;
|
||||
@@ -59,8 +53,8 @@ public final class AuthInfo {
|
||||
return username;
|
||||
}
|
||||
|
||||
public String getUserId() {
|
||||
return userId;
|
||||
public UUID getUUID() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public String getAccessToken() {
|
||||
@@ -81,11 +75,14 @@ public final class AuthInfo {
|
||||
return userProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null if no argument is specified
|
||||
*/
|
||||
public Arguments getArguments() {
|
||||
return arguments;
|
||||
}
|
||||
|
||||
public AuthInfo setArguments(Arguments arguments) {
|
||||
return new AuthInfo(username, userId, accessToken, userType, userProperties, arguments);
|
||||
public AuthInfo withArguments(Arguments arguments) {
|
||||
return new AuthInfo(username, uuid, accessToken, userType, userProperties, arguments);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,8 +39,8 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
|
||||
private final String serverBaseURL;
|
||||
private final ExceptionalSupplier<String, ?> injectorJarPath;
|
||||
|
||||
protected AuthlibInjectorAccount(YggdrasilService service, String serverBaseURL, ExceptionalSupplier<String, ?> injectorJarPath, String username, String clientToken, String character, YggdrasilSession session) {
|
||||
super(service, username, clientToken, character, session);
|
||||
protected AuthlibInjectorAccount(YggdrasilService service, String serverBaseURL, ExceptionalSupplier<String, ?> injectorJarPath, String username, String character, YggdrasilSession session) {
|
||||
super(service, username, character, session);
|
||||
|
||||
this.injectorJarPath = injectorJarPath;
|
||||
this.serverBaseURL = serverBaseURL;
|
||||
@@ -72,7 +72,7 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
|
||||
if (flag.get())
|
||||
arguments = Arguments.addJVMArguments(arguments, "-Dorg.to2mbn.authlibinjector.config.prefetched=" + new String(Base64.getEncoder().encode(getTask.getResult().getBytes()), UTF_8));
|
||||
|
||||
return info.setArguments(arguments);
|
||||
return info.withArguments(arguments);
|
||||
} catch (Exception e) {
|
||||
throw new AuthenticationException("Unable to get authlib injector jar path", e);
|
||||
}
|
||||
|
||||
@@ -7,12 +7,10 @@ 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.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.UUIDTypeAdapter;
|
||||
|
||||
import java.net.Proxy;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
|
||||
@@ -34,7 +32,7 @@ public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjecto
|
||||
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);
|
||||
(String) serverBaseURL, injectorJarPathSupplier, username, null, null);
|
||||
account.logInWithPassword(password, selector);
|
||||
return account;
|
||||
}
|
||||
@@ -46,14 +44,12 @@ public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjecto
|
||||
|
||||
String username = tryCast(storage.get("username"), String.class)
|
||||
.orElseThrow(() -> new IllegalArgumentException("storage does not have username"));
|
||||
String clientToken = tryCast(storage.get("clientToken"), String.class)
|
||||
.orElseThrow(() -> new IllegalArgumentException("storage does not have client token."));
|
||||
String character = tryCast(storage.get("clientToken"), String.class)
|
||||
.orElseThrow(() -> new IllegalArgumentException("storage does not have selected character name."));
|
||||
String apiRoot = tryCast(storage.get("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));
|
||||
apiRoot, injectorJarPathSupplier, username, character, YggdrasilSession.fromStorage(storage));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,15 +20,18 @@ package org.jackhuang.hmcl.auth.offline;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.UserType;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.ToStringBuilder;
|
||||
import org.jackhuang.hmcl.util.UUIDTypeAdapter;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huang
|
||||
@@ -36,9 +39,9 @@ import java.util.UUID;
|
||||
public class OfflineAccount extends Account {
|
||||
|
||||
private final String username;
|
||||
private final String uuid;
|
||||
private final UUID uuid;
|
||||
|
||||
OfflineAccount(String username, String uuid) {
|
||||
OfflineAccount(String username, UUID uuid) {
|
||||
Objects.requireNonNull(username);
|
||||
Objects.requireNonNull(uuid);
|
||||
|
||||
@@ -51,7 +54,7 @@ public class OfflineAccount extends Account {
|
||||
|
||||
@Override
|
||||
public UUID getUUID() {
|
||||
return UUIDTypeAdapter.fromString(uuid);
|
||||
return uuid;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -69,7 +72,7 @@ public class OfflineAccount extends Account {
|
||||
if (StringUtils.isBlank(username))
|
||||
throw new AuthenticationException("Username cannot be empty");
|
||||
|
||||
return new AuthInfo(username, uuid, uuid);
|
||||
return new AuthInfo(username, uuid, UUIDTypeAdapter.fromUUID(UUID.randomUUID()), UserType.MOJANG, "{}");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -77,11 +80,6 @@ public class OfflineAccount extends Account {
|
||||
return logIn();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logOut() {
|
||||
// Offline account need not log out.
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canPlayOffline() {
|
||||
return false;
|
||||
@@ -95,7 +93,7 @@ public class OfflineAccount extends Account {
|
||||
@Override
|
||||
public Map<Object, Object> toStorage() {
|
||||
return mapOf(
|
||||
pair("uuid", uuid),
|
||||
pair("uuid", UUIDTypeAdapter.fromUUID(uuid)),
|
||||
pair("username", username)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,9 +23,9 @@ import org.jackhuang.hmcl.util.UUIDTypeAdapter;
|
||||
|
||||
import java.net.Proxy;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.jackhuang.hmcl.util.DigestUtils.digest;
|
||||
import static org.jackhuang.hmcl.util.Hex.encodeHex;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
|
||||
/**
|
||||
@@ -47,17 +47,15 @@ public class OfflineAccountFactory extends AccountFactory<OfflineAccount> {
|
||||
public OfflineAccount fromStorage(Map<Object, Object> storage, Proxy proxy) {
|
||||
String username = tryCast(storage.get("username"), String.class)
|
||||
.orElseThrow(() -> new IllegalStateException("Offline account configuration malformed."));
|
||||
String uuid = tryCast(storage.get("uuid"), String.class)
|
||||
UUID uuid = tryCast(storage.get("uuid"), String.class)
|
||||
.map(UUIDTypeAdapter::fromString)
|
||||
.orElse(getUUIDFromUserName(username));
|
||||
|
||||
// Check if the uuid is vaild
|
||||
UUIDTypeAdapter.fromString(uuid);
|
||||
|
||||
return new OfflineAccount(username, uuid);
|
||||
}
|
||||
|
||||
private static String getUUIDFromUserName(String username) {
|
||||
return encodeHex(digest("MD5", username));
|
||||
private static UUID getUUIDFromUserName(String username) {
|
||||
return UUID.nameUUIDFromBytes(("OfflinePlayer:" + username).getBytes(UTF_8));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,66 +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
|
||||
*/
|
||||
final class AuthenticationResponse extends ErrorResponse {
|
||||
|
||||
private final String accessToken;
|
||||
private final String clientToken;
|
||||
private final GameProfile selectedProfile;
|
||||
private final GameProfile[] availableProfiles;
|
||||
private final User user;
|
||||
|
||||
public AuthenticationResponse() {
|
||||
this(null, null, null, null, null, null, null, null);
|
||||
}
|
||||
|
||||
public AuthenticationResponse(String accessToken, String clientToken, GameProfile selectedProfile, GameProfile[] availableProfiles, User user, String error, String errorMessage, String cause) {
|
||||
super(error, errorMessage, cause);
|
||||
|
||||
this.accessToken = accessToken;
|
||||
this.clientToken = clientToken;
|
||||
this.selectedProfile = selectedProfile;
|
||||
this.availableProfiles = availableProfiles;
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
public String getAccessToken() {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
public String getClientToken() {
|
||||
return clientToken;
|
||||
}
|
||||
|
||||
public GameProfile getSelectedProfile() {
|
||||
return selectedProfile;
|
||||
}
|
||||
|
||||
public GameProfile[] getAvailableProfiles() {
|
||||
return availableProfiles;
|
||||
}
|
||||
|
||||
public User getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,9 @@ public final class GameProfile {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return nullable
|
||||
*/
|
||||
public PropertyMap getProperties() {
|
||||
return properties;
|
||||
}
|
||||
|
||||
@@ -1,53 +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.Immutable;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@Immutable
|
||||
public final class TextureResponse {
|
||||
private final UUID profileId;
|
||||
private final String profileName;
|
||||
private final Map<TextureType, Texture> textures;
|
||||
|
||||
public TextureResponse() {
|
||||
this(UUID.randomUUID(), "", Collections.emptyMap());
|
||||
}
|
||||
|
||||
public TextureResponse(UUID profileId, String profileName, Map<TextureType, Texture> textures) {
|
||||
this.profileId = profileId;
|
||||
this.profileName = profileName;
|
||||
this.textures = textures;
|
||||
}
|
||||
|
||||
public UUID getProfileId() {
|
||||
return profileId;
|
||||
}
|
||||
|
||||
public String getProfileName() {
|
||||
return profileName;
|
||||
}
|
||||
|
||||
public Map<TextureType, Texture> getTextures() {
|
||||
return textures == null ? null : Collections.unmodifiableMap(textures);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.auth.yggdrasil;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import org.jackhuang.hmcl.auth.*;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.UUIDTypeAdapter;
|
||||
@@ -34,14 +33,12 @@ public class YggdrasilAccount extends Account {
|
||||
private final YggdrasilService service;
|
||||
private boolean isOnline = false;
|
||||
private YggdrasilSession session;
|
||||
private final String clientToken;
|
||||
private String character;
|
||||
|
||||
protected YggdrasilAccount(YggdrasilService service, String username, String clientToken, String character, YggdrasilSession session) {
|
||||
protected YggdrasilAccount(YggdrasilService service, String username, String character, YggdrasilSession session) {
|
||||
this.service = service;
|
||||
this.username = username;
|
||||
this.session = session;
|
||||
this.clientToken = clientToken;
|
||||
this.character = character;
|
||||
|
||||
if (session == null || session.getSelectedProfile() == null || StringUtils.isBlank(session.getAccessToken()))
|
||||
@@ -72,7 +69,7 @@ public class YggdrasilAccount extends Account {
|
||||
logInWithToken();
|
||||
selectProfile(new SpecificCharacterSelector(character));
|
||||
}
|
||||
return toAuthInfo();
|
||||
return session.toAuthInfo();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -81,9 +78,9 @@ public class YggdrasilAccount extends Account {
|
||||
}
|
||||
|
||||
protected AuthInfo logInWithPassword(String password, CharacterSelector selector) throws AuthenticationException {
|
||||
session = service.authenticate(username, password, clientToken);
|
||||
session = service.authenticate(username, password, UUIDTypeAdapter.fromUUID(UUID.randomUUID()));
|
||||
selectProfile(selector);
|
||||
return toAuthInfo();
|
||||
return session.toAuthInfo();
|
||||
}
|
||||
|
||||
private void selectProfile(CharacterSelector selector) throws AuthenticationException {
|
||||
@@ -91,25 +88,18 @@ public class YggdrasilAccount extends Account {
|
||||
if (session.getAvailableProfiles() == null || session.getAvailableProfiles().length <= 0)
|
||||
throw new NoCharacterException(this);
|
||||
|
||||
session.setSelectedProfile(selector.select(this, Arrays.asList(session.getAvailableProfiles())));
|
||||
session = service.refresh(session.getAccessToken(), session.getClientToken(), selector.select(this, Arrays.asList(session.getAvailableProfiles())));
|
||||
}
|
||||
|
||||
character = session.getSelectedProfile().getName();
|
||||
}
|
||||
|
||||
private void logInWithToken() throws AuthenticationException {
|
||||
if (service.validate(session.getAccessToken(), clientToken)) {
|
||||
if (service.validate(session.getAccessToken(), session.getClientToken())) {
|
||||
isOnline = true;
|
||||
return;
|
||||
}
|
||||
session = service.refresh(session.getAccessToken(), clientToken);
|
||||
}
|
||||
|
||||
private AuthInfo toAuthInfo() {
|
||||
GameProfile profile = session.getSelectedProfile();
|
||||
|
||||
return new AuthInfo(profile.getName(), UUIDTypeAdapter.fromUUID(profile.getId()), session.getAccessToken(), profile.getUserType(),
|
||||
new GsonBuilder().registerTypeAdapter(PropertyMap.class, PropertyMap.LegacySerializer.INSTANCE).create().toJson(Optional.ofNullable(session.getUser()).map(User::getProperties).orElseGet(PropertyMap::new)));
|
||||
session = service.refresh(session.getAccessToken(), session.getClientToken(), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -122,13 +112,7 @@ public class YggdrasilAccount extends Account {
|
||||
if (!canPlayOffline())
|
||||
throw new IllegalStateException("Current account " + this + " cannot play offline.");
|
||||
|
||||
return toAuthInfo();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logOut() {
|
||||
isOnline = false;
|
||||
session = null;
|
||||
return session.toAuthInfo();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -136,7 +120,6 @@ public class YggdrasilAccount extends Account {
|
||||
HashMap<Object, Object> storage = new HashMap<>();
|
||||
|
||||
storage.put("username", getUsername());
|
||||
storage.put("clientToken", clientToken);
|
||||
storage.put("character", character);
|
||||
if (session != null)
|
||||
storage.putAll(session.toStorage());
|
||||
@@ -144,6 +127,7 @@ public class YggdrasilAccount extends Account {
|
||||
return storage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getUUID() {
|
||||
if (session == null || session.getSelectedProfile() == null)
|
||||
return null;
|
||||
@@ -157,7 +141,7 @@ public class YggdrasilAccount extends Account {
|
||||
|
||||
public Optional<Texture> getSkin(GameProfile profile) throws AuthenticationException {
|
||||
if (!service.getTextures(profile).isPresent()) {
|
||||
session.setAvailableProfile(profile = service.getCompleteGameProfile(profile.getId()));
|
||||
profile = service.getCompleteGameProfile(profile.getId()).orElse(profile);
|
||||
}
|
||||
|
||||
return service.getTextures(profile).map(map -> map.get(TextureType.SKIN));
|
||||
|
||||
@@ -48,7 +48,7 @@ public class YggdrasilAccountFactory extends AccountFactory<YggdrasilAccount> {
|
||||
Objects.requireNonNull(password);
|
||||
Objects.requireNonNull(proxy);
|
||||
|
||||
YggdrasilAccount account = new YggdrasilAccount(new YggdrasilService(provider, proxy), username, UUIDTypeAdapter.fromUUID(UUID.randomUUID()), null, null);
|
||||
YggdrasilAccount account = new YggdrasilAccount(new YggdrasilService(provider, proxy), username, null, null);
|
||||
account.logInWithPassword(password, selector);
|
||||
return account;
|
||||
}
|
||||
@@ -60,12 +60,10 @@ public class YggdrasilAccountFactory extends AccountFactory<YggdrasilAccount> {
|
||||
|
||||
String username = tryCast(storage.get("username"), String.class)
|
||||
.orElseThrow(() -> new IllegalArgumentException("storage does not have username"));
|
||||
String clientToken = tryCast(storage.get("clientToken"), String.class)
|
||||
.orElseThrow(() -> new IllegalArgumentException("storage does not have client token."));
|
||||
String character = tryCast(storage.get("clientToken"), String.class)
|
||||
.orElseThrow(() -> new IllegalArgumentException("storage does not have selected character name."));
|
||||
|
||||
return new YggdrasilAccount(new YggdrasilService(provider, proxy), username, clientToken, character, YggdrasilSession.fromStorage(storage));
|
||||
return new YggdrasilAccount(new YggdrasilService(provider, proxy), username, character, YggdrasilSession.fromStorage(storage));
|
||||
}
|
||||
|
||||
public static String randomToken() {
|
||||
|
||||
@@ -1,20 +1,33 @@
|
||||
package org.jackhuang.hmcl.auth.yggdrasil;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.jackhuang.hmcl.util.Lang.liftFunction;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
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.*;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.InvalidCredentialsException;
|
||||
import org.jackhuang.hmcl.auth.InvalidPasswordException;
|
||||
import org.jackhuang.hmcl.auth.InvalidTokenException;
|
||||
import org.jackhuang.hmcl.auth.ServerDisconnectException;
|
||||
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
|
||||
import org.jackhuang.hmcl.util.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.UUIDTypeAdapter;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonParseException;
|
||||
|
||||
public class YggdrasilService {
|
||||
|
||||
@@ -45,14 +58,30 @@ public class YggdrasilService {
|
||||
request.put("clientToken", clientToken);
|
||||
request.put("requestUser", true);
|
||||
|
||||
return handle(request(provider.getAuthenticationURL(), request), clientToken);
|
||||
return handleAuthenticationResponse(request(provider.getAuthenticationURL(), request), clientToken);
|
||||
}
|
||||
|
||||
public YggdrasilSession refresh(String accessToken, String clientToken) throws AuthenticationException {
|
||||
private Map<String, Object> createRequestWithCredentials(String accessToken, String clientToken) {
|
||||
Map<String, Object> request = new HashMap<>();
|
||||
request.put("accessToken", accessToken);
|
||||
request.put("clientToken", clientToken);
|
||||
return request;
|
||||
}
|
||||
|
||||
public YggdrasilSession refresh(String accessToken, String clientToken, GameProfile characterToSelect) throws AuthenticationException {
|
||||
Objects.requireNonNull(accessToken);
|
||||
Objects.requireNonNull(clientToken);
|
||||
|
||||
return handle(request(provider.getRefreshmentURL(), new RefreshRequest(accessToken, clientToken)), clientToken);
|
||||
Map<String, Object> request = createRequestWithCredentials(accessToken, clientToken);
|
||||
request.put("requestUser", true);
|
||||
|
||||
if (characterToSelect != null) {
|
||||
request.put("selectedProfile", mapOf(
|
||||
pair("id", characterToSelect.getId()),
|
||||
pair("name", characterToSelect.getName())));
|
||||
}
|
||||
|
||||
return handleAuthenticationResponse(request(provider.getRefreshmentURL(), request), clientToken);
|
||||
}
|
||||
|
||||
public boolean validate(String accessToken) throws AuthenticationException {
|
||||
@@ -63,7 +92,7 @@ public class YggdrasilService {
|
||||
Objects.requireNonNull(accessToken);
|
||||
|
||||
try {
|
||||
requireEmpty(request(provider.getValidationURL(), new RefreshRequest(accessToken, clientToken)));
|
||||
requireEmpty(request(provider.getValidationURL(), createRequestWithCredentials(accessToken, clientToken)));
|
||||
return true;
|
||||
} catch (InvalidCredentialsException | InvalidTokenException e) {
|
||||
return false;
|
||||
@@ -77,7 +106,7 @@ public class YggdrasilService {
|
||||
public void invalidate(String accessToken, String clientToken) throws AuthenticationException {
|
||||
Objects.requireNonNull(accessToken);
|
||||
|
||||
requireEmpty(request(provider.getInvalidationURL(), GSON.toJson(new RefreshRequest(accessToken, clientToken))));
|
||||
requireEmpty(request(provider.getInvalidationURL(), createRequestWithCredentials(accessToken, clientToken)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,49 +114,34 @@ public class YggdrasilService {
|
||||
*
|
||||
* 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.
|
||||
* @param uuid the uuid that the character corresponding to.
|
||||
* @return the complete game profile(filled with more properties)
|
||||
*/
|
||||
public GameProfile getCompleteGameProfile(UUID userId) throws AuthenticationException {
|
||||
Objects.requireNonNull(userId);
|
||||
public Optional<GameProfile> getCompleteGameProfile(UUID uuid) throws AuthenticationException {
|
||||
Objects.requireNonNull(uuid);
|
||||
|
||||
ProfileResponse response = fromJson(request(provider.getProfilePropertiesURL(userId), null), ProfileResponse.class);
|
||||
if (response == null)
|
||||
return null;
|
||||
|
||||
return new GameProfile(response.getId(), response.getName(), response.getProperties());
|
||||
return Optional.ofNullable(fromJson(request(provider.getProfilePropertiesURL(uuid), null), ProfileResponse.class))
|
||||
.map(response -> new GameProfile(response.id, response.name, response.properties));
|
||||
}
|
||||
|
||||
public Optional<Map<TextureType, Texture>> getTextures(GameProfile profile) throws AuthenticationException {
|
||||
Objects.requireNonNull(profile);
|
||||
|
||||
return Optional.ofNullable(profile.getProperties())
|
||||
.map(properties -> properties.get("textures"))
|
||||
.flatMap(properties -> Optional.ofNullable(properties.get("textures")))
|
||||
.map(encodedTextures -> new String(Base64.getDecoder().decode(encodedTextures), UTF_8))
|
||||
.map(Lang.liftFunction(textures -> fromJson(textures, TextureResponse.class)))
|
||||
.map(TextureResponse::getTextures);
|
||||
.map(liftFunction(textures -> fromJson(textures, TextureResponse.class)))
|
||||
.flatMap(response -> Optional.ofNullable(response.textures));
|
||||
}
|
||||
|
||||
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 {
|
||||
private static YggdrasilSession handleAuthenticationResponse(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);
|
||||
if (!clientToken.equals(response.clientToken))
|
||||
throw new AuthenticationException("Client token changed from " + clientToken + " to " + response.clientToken);
|
||||
|
||||
return new YggdrasilSession(response.getAccessToken(), response.getSelectedProfile(), response.getAvailableProfiles(), response.getUser());
|
||||
return new YggdrasilSession(response.clientToken, response.accessToken, response.selectedProfile, response.availableProfiles, response.user);
|
||||
}
|
||||
|
||||
private static void requireEmpty(String response) throws AuthenticationException {
|
||||
@@ -142,15 +156,30 @@ public class YggdrasilService {
|
||||
}
|
||||
|
||||
private static void handleErrorMessage(ErrorResponse response) throws AuthenticationException {
|
||||
if (!StringUtils.isBlank(response.getError())) {
|
||||
if (response.getErrorMessage() != null)
|
||||
if (response.getErrorMessage().contains("Invalid credentials"))
|
||||
if (!StringUtils.isBlank(response.error)) {
|
||||
if (response.errorMessage != null && "ForbiddenOperationException".equals(response.error)) {
|
||||
if (response.errorMessage.contains("Invalid credentials"))
|
||||
throw new InvalidCredentialsException();
|
||||
else if (response.getErrorMessage().contains("Invalid token"))
|
||||
|
||||
else if (response.errorMessage.contains("Invalid token"))
|
||||
throw new InvalidTokenException();
|
||||
else if (response.getErrorMessage().contains("Invalid username or password"))
|
||||
|
||||
else if (response.errorMessage.contains("Invalid username or password"))
|
||||
throw new InvalidPasswordException();
|
||||
throw new RemoteAuthenticationException(response.getError(), response.getErrorMessage(), response.getCause());
|
||||
}
|
||||
|
||||
throw new RemoteAuthenticationException(response.error, response.errorMessage, response.cause);
|
||||
}
|
||||
}
|
||||
|
||||
private String request(URL url, Object payload) throws AuthenticationException {
|
||||
try {
|
||||
if (payload == null)
|
||||
return NetworkUtils.doGet(url, proxy);
|
||||
else
|
||||
return NetworkUtils.doPost(url, payload instanceof String ? (String) payload : GSON.toJson(payload), "application/json");
|
||||
} catch (IOException e) {
|
||||
throw new ServerDisconnectException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,77 +191,34 @@ public class YggdrasilService {
|
||||
}
|
||||
}
|
||||
|
||||
static final Gson GSON = new GsonBuilder()
|
||||
private class ProfileResponse {
|
||||
public UUID id;
|
||||
public String name;
|
||||
public PropertyMap properties;
|
||||
}
|
||||
|
||||
private class TextureResponse {
|
||||
public Map<TextureType, Texture> textures;
|
||||
}
|
||||
|
||||
private class AuthenticationResponse extends ErrorResponse {
|
||||
public String accessToken;
|
||||
public String clientToken;
|
||||
public GameProfile selectedProfile;
|
||||
public GameProfile[] availableProfiles;
|
||||
public User user;
|
||||
}
|
||||
|
||||
private class ErrorResponse {
|
||||
public String error;
|
||||
public String errorMessage;
|
||||
public String cause;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,54 @@
|
||||
package org.jackhuang.hmcl.auth.yggdrasil;
|
||||
|
||||
import org.jackhuang.hmcl.util.UUIDTypeAdapter;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
import org.jackhuang.hmcl.auth.UserType;
|
||||
import org.jackhuang.hmcl.util.UUIDTypeAdapter;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
||||
public class YggdrasilSession {
|
||||
|
||||
private final String accessToken;
|
||||
private String clientToken;
|
||||
private String accessToken;
|
||||
private GameProfile selectedProfile;
|
||||
private final GameProfile[] availableProfiles;
|
||||
private final User user;
|
||||
private GameProfile[] availableProfiles;
|
||||
private User user;
|
||||
|
||||
public YggdrasilSession(String accessToken, GameProfile selectedProfile, GameProfile[] availableProfiles, User user) {
|
||||
public YggdrasilSession(String clientToken, String accessToken, GameProfile selectedProfile, GameProfile[] availableProfiles, User user) {
|
||||
this.clientToken = clientToken;
|
||||
this.accessToken = accessToken;
|
||||
this.selectedProfile = selectedProfile;
|
||||
this.availableProfiles = availableProfiles;
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
public String getClientToken() {
|
||||
return clientToken;
|
||||
}
|
||||
|
||||
public String getAccessToken() {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return nullable (null if no character is selected)
|
||||
*/
|
||||
public GameProfile getSelectedProfile() {
|
||||
return selectedProfile;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return nullable (null if the YggdrasilSession is loaded from storage)
|
||||
*/
|
||||
public GameProfile[] getAvailableProfiles() {
|
||||
return availableProfiles;
|
||||
}
|
||||
@@ -38,53 +57,40 @@ public class YggdrasilSession {
|
||||
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 = tryCast(storage.get("uuid"), String.class);
|
||||
Optional<String> profileName = tryCast(storage.get("displayName"), String.class);
|
||||
GameProfile profile = null;
|
||||
if (profileId.isPresent() && profileName.isPresent()) {
|
||||
profile = new GameProfile(UUIDTypeAdapter.fromString(profileId.get()), profileName.get(),
|
||||
tryCast(storage.get("profileProperties"), Map.class).map(PropertyMap::fromMap).orElseGet(PropertyMap::new));
|
||||
}
|
||||
|
||||
return new YggdrasilSession(
|
||||
tryCast(storage.get("accessToken"), String.class).orElse(null),
|
||||
profile,
|
||||
null,
|
||||
tryCast(storage.get("userid"), String.class)
|
||||
.map(userId -> new User(userId, tryCast(storage.get("userProperties"), Map.class).map(PropertyMap::fromMap).orElse(null)))
|
||||
.orElse(null)
|
||||
);
|
||||
UUID uuid = tryCast(storage.get("uuid"), String.class).map(UUIDTypeAdapter::fromString).orElseThrow(() -> new IllegalArgumentException("uuid is missing"));
|
||||
String name = tryCast(storage.get("displayName"), String.class).orElseThrow(() -> new IllegalArgumentException("displayName is missing"));
|
||||
String clientToken = tryCast(storage.get("clientToken"), String.class).orElseThrow(() -> new IllegalArgumentException("clientToken is missing"));
|
||||
String accessToken = tryCast(storage.get("accessToken"), String.class).orElseThrow(() -> new IllegalArgumentException("accessToken is missing"));
|
||||
String userId = tryCast(storage.get("userid"), String.class).orElseThrow(() -> new IllegalArgumentException("userid is missing"));
|
||||
PropertyMap userProperties = tryCast(storage.get("userProperties"), Map.class).map(PropertyMap::fromMap).orElse(null);
|
||||
return new YggdrasilSession(clientToken, accessToken, new GameProfile(uuid, name), null, new User(userId, userProperties));
|
||||
}
|
||||
|
||||
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;
|
||||
if (selectedProfile == null)
|
||||
throw new IllegalStateException("No character is selected");
|
||||
if (user == null)
|
||||
throw new IllegalStateException("No user is specified");
|
||||
|
||||
return mapOf(
|
||||
pair("clientToken", clientToken),
|
||||
pair("accessToken", accessToken),
|
||||
pair("uuid", UUIDTypeAdapter.fromUUID(selectedProfile.getId())),
|
||||
pair("displayName", selectedProfile.getName()),
|
||||
pair("userid", user.getId()),
|
||||
pair("userProperties", user.getProperties()));
|
||||
}
|
||||
|
||||
public AuthInfo toAuthInfo() {
|
||||
if (selectedProfile == null)
|
||||
throw new IllegalStateException("No character is selected");
|
||||
if (user == null)
|
||||
throw new IllegalStateException("No user is specified");
|
||||
|
||||
return new AuthInfo(selectedProfile.getName(), selectedProfile.getId(), accessToken, UserType.MOJANG,
|
||||
Optional.ofNullable(user.getProperties()).map(GSON_PROPERTIES::toJson).orElse("{}"));
|
||||
}
|
||||
|
||||
private static final Gson GSON_PROPERTIES = new GsonBuilder().registerTypeAdapter(PropertyMap.class, PropertyMap.LegacySerializer.INSTANCE).create();
|
||||
}
|
||||
|
||||
@@ -239,7 +239,7 @@ public class DefaultLauncher extends Launcher {
|
||||
pair("${auth_player_name}", authInfo.getUsername()),
|
||||
pair("${auth_session}", authInfo.getAccessToken()),
|
||||
pair("${auth_access_token}", authInfo.getAccessToken()),
|
||||
pair("${auth_uuid}", authInfo.getUserId()),
|
||||
pair("${auth_uuid}", UUIDTypeAdapter.fromUUID(authInfo.getUUID())),
|
||||
pair("${version_name}", Optional.ofNullable(options.getVersionName()).orElse(version.getId())),
|
||||
pair("${profile_name}", Optional.ofNullable(options.getProfileName()).orElse("Minecraft")),
|
||||
pair("${version_type}", version.getType().getId()),
|
||||
|
||||
Reference in New Issue
Block a user