diff --git a/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/YggdrasilAuthenticator.java b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/YggdrasilAuthenticator.java index fc9e3df3c..5a702dcb5 100644 --- a/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/YggdrasilAuthenticator.java +++ b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/YggdrasilAuthenticator.java @@ -24,13 +24,12 @@ import org.jackhuang.hellominecraft.C; import org.jackhuang.hellominecraft.HMCLog; import org.jackhuang.hellominecraft.utils.ArrayUtils; import org.jackhuang.hellominecraft.views.Selector; -import org.jackhuang.mojang.authlib.GameProfile; -import org.jackhuang.mojang.authlib.UserType; -import org.jackhuang.mojang.authlib.properties.PropertyMap; -import org.jackhuang.mojang.authlib.yggdrasil.YggdrasilAuthenticationService; -import org.jackhuang.mojang.authlib.yggdrasil.YggdrasilUserAuthentication; -import org.jackhuang.mojang.util.LegacyPropertyMapSerializer; -import org.jackhuang.mojang.util.UUIDTypeAdapter; +import org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.GameProfile; +import org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.properties.PropertyMap; +import org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.YggdrasilAuthenticationService; +import org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.YggdrasilUserAuthentication; +import org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.properties.LegacyPropertyMapSerializer; +import org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.UUIDTypeAdapter; /** * @@ -58,7 +57,7 @@ public final class YggdrasilAuthenticator extends IAuthenticator { result.setUserPropertyMap(new GsonBuilder().registerTypeAdapter(PropertyMap.class, new PropertyMap.Serializer()).create().toJson(ua.getUserProperties())); result.setAccessToken(ua.getAuthenticatedToken()); result.setSession(ua.getAuthenticatedToken()); - result.setUserType(ua.getUserType().getName()); + result.setUserType("mojang"); return result; } UserProfileProvider result = new UserProfileProvider(); @@ -102,15 +101,14 @@ public final class YggdrasilAuthenticator extends IAuthenticator { if (authToken == null) authToken = "0"; result.setAccessToken(authToken); result.setSession(authToken); - result.setUserType(ua.getUserType().getName()); } catch (Exception ex) { result.setErrorReason(ex.getMessage()); result.setSuccess(false); result.setUserName(ua.getUserID()); - result.setUserType(UserType.MOJANG.getName()); HMCLog.err("Failed to login by yggdrasil authentication.", ex); } + result.setUserType("mojang"); return result; } diff --git a/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/AuthenticationService.java b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/AuthenticationService.java new file mode 100644 index 000000000..51d02ef95 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/AuthenticationService.java @@ -0,0 +1,6 @@ +package org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil; + +public abstract interface AuthenticationService { + + public abstract UserAuthentication createUserAuthentication(); +} diff --git a/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/BaseUserAuthentication.java b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/BaseUserAuthentication.java new file mode 100644 index 000000000..9b5729291 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/BaseUserAuthentication.java @@ -0,0 +1,246 @@ +package org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.jackhuang.hellominecraft.logging.logger.Logger; +import org.jackhuang.hellominecraft.utils.StrUtils; +import org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.properties.Property; +import org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.properties.PropertyMap; + +public abstract class BaseUserAuthentication + implements UserAuthentication { + + private static final Logger LOGGER = new Logger("BaseUserAuthentication"); + protected static final String STORAGE_KEY_PROFILE_NAME = "displayName"; + protected static final String STORAGE_KEY_PROFILE_ID = "uuid"; + protected static final String STORAGE_KEY_PROFILE_PROPERTIES = "profileProperties"; + protected static final String STORAGE_KEY_USER_NAME = "username"; + protected static final String STORAGE_KEY_USER_ID = "userid"; + protected static final String STORAGE_KEY_USER_PROPERTIES = "userProperties"; + private final AuthenticationService authenticationService; + private final PropertyMap userProperties = new PropertyMap(); + private String userid; + private String username; + private String password; + private GameProfile selectedProfile; + + protected BaseUserAuthentication(AuthenticationService authenticationService) { + this.authenticationService = authenticationService; + } + + @Override + public boolean canLogIn() { + return (!canPlayOnline()) && (StrUtils.isNotBlank(getUsername())) && (StrUtils.isNotBlank(getPassword())); + } + + @Override + public void logOut() { + this.password = null; + this.userid = null; + setSelectedProfile(null); + getModifiableUserProperties().clear(); + } + + @Override + public boolean isLoggedIn() { + return getSelectedProfile() != null; + } + + @Override + public void setUsername(String username) { + if ((isLoggedIn()) && (canPlayOnline())) { + throw new IllegalStateException("Cannot change username whilst logged in & online"); + } + + this.username = username; + } + + @Override + public void setPassword(String password) { + if ((isLoggedIn()) && (canPlayOnline()) && (StrUtils.isNotBlank(password))) { + throw new IllegalStateException("Cannot set password whilst logged in & online"); + } + + this.password = password; + } + + protected String getUsername() { + return this.username; + } + + protected String getPassword() { + return this.password; + } + + @Override + public void loadFromStorage(Map credentials) { + logOut(); + + setUsername((String)credentials.get("username")); + + if (credentials.containsKey("userid")) { + this.userid = (String)credentials.get("userid"); + } else { + this.userid = this.username; + } + + if (credentials.containsKey("userProperties")) { + try { + List list = (List) credentials.get("userProperties"); + + for (Map propertyMap : list) { + String name = (String) propertyMap.get("name"); + String value = (String) propertyMap.get("value"); + String signature = (String) propertyMap.get("signature"); + + if (signature == null) { + getModifiableUserProperties().put(name, new Property(name, value)); + } else { + getModifiableUserProperties().put(name, new Property(name, value, signature)); + } + } + } catch (Throwable t) { + LOGGER.warn("Couldn't deserialize user properties", t); + } + } + + if ((credentials.containsKey("displayName")) && (credentials.containsKey("uuid"))) { + GameProfile profile = new GameProfile(UUIDTypeAdapter.fromString((String)credentials.get("uuid")), (String)credentials.get("displayName")); + if (credentials.containsKey("profileProperties")) { + try { + List list = (List) credentials.get("profileProperties"); + for (Map propertyMap : list) { + String name = (String) propertyMap.get("name"); + String value = (String) propertyMap.get("value"); + String signature = (String) propertyMap.get("signature"); + + if (signature == null) { + profile.getProperties().put(name, new Property(name, value)); + } else { + profile.getProperties().put(name, new Property(name, value, signature)); + } + } + } catch (Throwable t) { + LOGGER.warn("Couldn't deserialize profile properties", t); + } + } + setSelectedProfile(profile); + } + } + + @Override + public Map saveForStorage() { + Map result = new HashMap(); + + if (getUsername() != null) { + result.put("username", getUsername()); + } + if (getUserID() != null) { + result.put("userid", getUserID()); + } else if (getUsername() != null) { + result.put("username", getUsername()); + } + + if (!getUserProperties().isEmpty()) { + List properties = new ArrayList(); + for (Property userProperty : getUserProperties().values()) { + Map property = new HashMap(); + property.put("name", userProperty.getName()); + property.put("value", userProperty.getValue()); + property.put("signature", userProperty.getSignature()); + properties.add(property); + } + result.put("userProperties", properties); + } + + GameProfile sel = getSelectedProfile(); + if (sel != null) { + result.put("displayName", sel.getName()); + result.put("uuid", sel.getId()); + + List properties = new ArrayList(); + for (Property profileProperty : sel.getProperties().values()) { + Map property = new HashMap(); + property.put("name", profileProperty.getName()); + property.put("value", profileProperty.getValue()); + property.put("signature", profileProperty.getSignature()); + properties.add(property); + } + + if (!properties.isEmpty()) { + result.put("profileProperties", properties); + } + } + + return result; + } + + protected void setSelectedProfile(GameProfile selectedProfile) { + this.selectedProfile = selectedProfile; + } + + @Override + public GameProfile getSelectedProfile() { + return this.selectedProfile; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + + result.append(getClass().getSimpleName()); + result.append("{"); + + if (isLoggedIn()) { + result.append("Logged in as "); + result.append(getUsername()); + + if (getSelectedProfile() != null) { + result.append(" / "); + result.append(getSelectedProfile()); + result.append(" - "); + + if (canPlayOnline()) { + result.append("Online"); + } else { + result.append("Offline"); + } + } + } else { + result.append("Not logged in"); + } + + result.append("}"); + + return result.toString(); + } + + public AuthenticationService getAuthenticationService() { + return this.authenticationService; + } + + @Override + public String getUserID() { + return this.userid; + } + + @Override + public PropertyMap getUserProperties() { + if (isLoggedIn()) { + PropertyMap result = new PropertyMap(); + result.putAll(getModifiableUserProperties()); + return result; + } + return new PropertyMap(); + } + + protected PropertyMap getModifiableUserProperties() { + return this.userProperties; + } + + protected void setUserid(String userid) { + this.userid = userid; + } +} \ No newline at end of file diff --git a/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/GameProfile.java b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/GameProfile.java new file mode 100644 index 000000000..530d0044d --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/GameProfile.java @@ -0,0 +1,72 @@ +package org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil; + +import org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.properties.PropertyMap; +import java.util.UUID; +import org.jackhuang.hellominecraft.utils.StrUtils; + +public class GameProfile { + + private final UUID id; + private final String name; + private final PropertyMap properties = new PropertyMap(); + private boolean legacy; + + public GameProfile(UUID id, String name) { + if ((id == null) && (StrUtils.isBlank(name))) { + throw new IllegalArgumentException("Name and ID cannot both be blank"); + } + + this.id = id; + this.name = name; + } + + public UUID getId() { + return this.id; + } + + public String getName() { + return this.name; + } + + public PropertyMap getProperties() { + return this.properties; + } + + public boolean isComplete() { + return (this.id != null) && (StrUtils.isNotBlank(getName())); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if ((o == null) || (getClass() != o.getClass())) { + return false; + } + + GameProfile that = (GameProfile) o; + + if (this.id != null ? !this.id.equals(that.id) : that.id != null) { + return false; + } + return this.name != null ? this.name.equals(that.name) : that.name == null; + } + + @Override + public int hashCode() { + int result = this.id != null ? this.id.hashCode() : 0; + result = 31 * result + (this.name != null ? this.name.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "GameProfile{" + "id=" + id + ", name=" + name + ", properties=" + properties + ", legacy=" + legacy + '}'; + } + + + public boolean isLegacy() { + return this.legacy; + } +} \ No newline at end of file diff --git a/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/HttpAuthenticationService.java b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/HttpAuthenticationService.java new file mode 100644 index 000000000..0371c5765 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/HttpAuthenticationService.java @@ -0,0 +1,148 @@ +package org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.HttpURLConnection; +import java.net.Proxy; +import java.net.URL; +import java.net.URLEncoder; +import java.util.Map; +import org.jackhuang.hellominecraft.logging.logger.Logger; +import org.jackhuang.hellominecraft.utils.system.IOUtils; +import org.jackhuang.hellominecraft.utils.NetUtils; +import org.jackhuang.hellominecraft.utils.Utils; + +public abstract class HttpAuthenticationService implements AuthenticationService { + + private static final Logger LOGGER = new Logger("HttpAuthenticationService"); + private final Proxy proxy; + + protected HttpAuthenticationService(Proxy proxy) { + this.proxy = proxy; + } + + public Proxy getProxy() { + return this.proxy; + } + + protected HttpURLConnection createUrlConnection(URL url) throws IOException { + LOGGER.debug("Opening connection to " + url); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(this.proxy); + connection.setConnectTimeout(15000); + connection.setReadTimeout(15000); + connection.setUseCaches(false); + return connection; + } + + public String performPostRequest(URL url, String post, String contentType) throws IOException { + Utils.requireNonNull(url); + Utils.requireNonNull(post); + Utils.requireNonNull(contentType); + HttpURLConnection connection = createUrlConnection(url); + byte[] postAsBytes = post.getBytes("UTF-8"); + + connection.setRequestProperty("Content-Type", contentType + "; charset=utf-8"); + connection.setRequestProperty("Content-Length", "" + postAsBytes.length); + connection.setDoOutput(true); + + LOGGER.debug("Writing POST data to " + url + ": " + post); + + OutputStream outputStream = null; + try { + outputStream = connection.getOutputStream(); + IOUtils.write(postAsBytes, outputStream); + } finally { + IOUtils.closeQuietly(outputStream); + } + + LOGGER.debug("Reading data from " + url); + + InputStream inputStream = null; + try { + inputStream = connection.getInputStream(); + String result = NetUtils.getStreamContent(inputStream, "UTF-8"); + LOGGER.debug("Successful read, server response was " + connection.getResponseCode()); + LOGGER.debug("Response: " + result); + String str1 = result; + return str1; + } catch (IOException e) { + IOUtils.closeQuietly(inputStream); + inputStream = connection.getErrorStream(); + + if (inputStream != null) { + LOGGER.debug("Reading error page from " + url); + String result = NetUtils.getStreamContent(inputStream, "UTF-8"); + LOGGER.debug("Successful read, server response was " + connection.getResponseCode()); + LOGGER.debug("Response: " + result); + String str2 = result; + return str2; + } + LOGGER.debug("Request failed", e); + throw e; + } finally { + IOUtils.closeQuietly(inputStream); + } + } + + public String performGetRequest(URL url) + throws IOException { + Utils.requireNonNull(url); + HttpURLConnection connection = createUrlConnection(url); + + LOGGER.debug("Reading data from " + url); + + InputStream inputStream = null; + try { + inputStream = connection.getInputStream(); + String result = NetUtils.getStreamContent(inputStream, "UTF-8"); + LOGGER.debug("Successful read, server response was " + connection.getResponseCode()); + LOGGER.debug("Response: " + result); + String str1 = result; + return str1; + } catch (IOException e) { + IOUtils.closeQuietly(inputStream); + inputStream = connection.getErrorStream(); + + if (inputStream != null) { + LOGGER.debug("Reading error page from " + url); + String result = NetUtils.getStreamContent(inputStream, "UTF-8"); + LOGGER.debug("Successful read, server response was " + connection.getResponseCode()); + LOGGER.debug("Response: " + result); + String str2 = result; + return str2; + } + LOGGER.debug("Request failed", e); + throw e; + } finally { + IOUtils.closeQuietly(inputStream); + } + } + + public static String buildQuery(Map query) { + if (query == null) return ""; + StringBuilder builder = new StringBuilder(); + + for (Map.Entry entry : query.entrySet()) { + if (builder.length() > 0) + builder.append('&'); + try { + builder.append(URLEncoder.encode(entry.getKey(), "UTF-8")); + } catch (UnsupportedEncodingException e) { + LOGGER.error("Unexpected exception building query", e); + } + + if (entry.getValue() != null) { + builder.append('='); + try { + builder.append(URLEncoder.encode(entry.getValue().toString(), "UTF-8")); + } catch (UnsupportedEncodingException e) { + LOGGER.error("Unexpected exception building query", e); + } + } + } + + return builder.toString(); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/HttpUserAuthentication.java b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/HttpUserAuthentication.java new file mode 100644 index 000000000..6f512d297 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/HttpUserAuthentication.java @@ -0,0 +1,13 @@ +package org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil; + +public abstract class HttpUserAuthentication extends BaseUserAuthentication { + + protected HttpUserAuthentication(HttpAuthenticationService authenticationService) { + super(authenticationService); + } + + @Override + public HttpAuthenticationService getAuthenticationService() { + return (HttpAuthenticationService) super.getAuthenticationService(); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/UUIDTypeAdapter.java b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/UUIDTypeAdapter.java new file mode 100644 index 000000000..704bb039c --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/UUIDTypeAdapter.java @@ -0,0 +1,29 @@ +package org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.util.UUID; + +public class UUIDTypeAdapter extends TypeAdapter { + + @Override + public void write(JsonWriter out, UUID value) + throws IOException { + out.value(fromUUID(value)); + } + + @Override + public UUID read(JsonReader in) throws IOException { + return fromString(in.nextString()); + } + + public static String fromUUID(UUID value) { + return value.toString().replace("-", ""); + } + + public static UUID fromString(String input) { + return UUID.fromString(input.replaceFirst("(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", "$1-$2-$3-$4-$5")); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/UserAuthentication.java b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/UserAuthentication.java new file mode 100644 index 000000000..e6f6be4d5 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/UserAuthentication.java @@ -0,0 +1,40 @@ +package org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil; + +import org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.exceptions.AuthenticationException; +import org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.properties.PropertyMap; +import java.util.Map; + +public abstract interface UserAuthentication { + + public abstract boolean canLogIn(); + + public abstract void logIn() + throws AuthenticationException; + + public abstract void logOut(); + + public abstract boolean isLoggedIn(); + + public abstract boolean canPlayOnline(); + + public abstract GameProfile[] getAvailableProfiles(); + + public abstract GameProfile getSelectedProfile(); + + public abstract void selectGameProfile(GameProfile paramGameProfile) + throws AuthenticationException; + + public abstract void loadFromStorage(Map paramMap); + + public abstract Map saveForStorage(); + + public abstract void setUsername(String paramString); + + public abstract void setPassword(String paramString); + + public abstract String getAuthenticatedToken(); + + public abstract String getUserID(); + + public abstract PropertyMap getUserProperties(); +} diff --git a/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/YggdrasilAuthenticationService.java b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/YggdrasilAuthenticationService.java new file mode 100644 index 000000000..6d1f9eb52 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/YggdrasilAuthenticationService.java @@ -0,0 +1,96 @@ +package org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.GameProfile; +import org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.HttpAuthenticationService; +import org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.UserAuthentication; +import org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.exceptions.AuthenticationException; +import org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.exceptions.InvalidCredentialsException; +import org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.properties.PropertyMap; +import org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.response.Response; +import org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.UUIDTypeAdapter; +import java.io.IOException; +import java.lang.reflect.Type; +import java.net.Proxy; +import java.net.URL; +import java.util.UUID; +import org.jackhuang.hellominecraft.C; +import org.jackhuang.hellominecraft.utils.StrUtils; + +public class YggdrasilAuthenticationService extends HttpAuthenticationService { + + private final String clientToken; + private final Gson gson; + + public YggdrasilAuthenticationService(Proxy proxy, String clientToken) { + super(proxy); + this.clientToken = clientToken; + GsonBuilder builder = new GsonBuilder(); + builder.registerTypeAdapter(GameProfile.class, new GameProfileSerializer()); + builder.registerTypeAdapter(PropertyMap.class, new PropertyMap.Serializer()); + builder.registerTypeAdapter(UUID.class, new UUIDTypeAdapter()); + this.gson = builder.create(); + } + + @Override + public UserAuthentication createUserAuthentication() { + return new YggdrasilUserAuthentication(this); + } + + protected T makeRequest(URL url, Object input, Class classOfT) throws AuthenticationException { + try { + String jsonResult = input == null ? performGetRequest(url) : performPostRequest(url, this.gson.toJson(input), "application/json"); + Response result = (Response) this.gson.fromJson(jsonResult, classOfT); + + if (result == null) { + return null; + } + + if (StrUtils.isNotBlank(result.getError())) { + if (result.getError().equals("ForbiddenOperationException")) { + throw new InvalidCredentialsException(result.getErrorMessage()); + } + throw new AuthenticationException(result.getErrorMessage()); + } + + return (T) result; + } catch (IOException | IllegalStateException | JsonParseException e) { + throw new AuthenticationException(C.i18n("login.failed.connect_authentication_server"), e); + } + } + + public String getClientToken() { + return this.clientToken; + } + + private static class GameProfileSerializer implements JsonSerializer, JsonDeserializer { + + @Override + public GameProfile deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + JsonObject object = (JsonObject) json; + UUID id = object.has("id") ? (UUID) context.deserialize(object.get("id"), UUID.class) : null; + String name = object.has("name") ? object.getAsJsonPrimitive("name").getAsString() : null; + return new GameProfile(id, name); + } + + @Override + public JsonElement serialize(GameProfile src, Type typeOfSrc, JsonSerializationContext context) { + JsonObject result = new JsonObject(); + if (src.getId() != null) { + result.add("id", context.serialize(src.getId())); + } + if (src.getName() != null) { + result.addProperty("name", src.getName()); + } + return result; + } + } +} \ No newline at end of file diff --git a/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/YggdrasilUserAuthentication.java b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/YggdrasilUserAuthentication.java new file mode 100644 index 000000000..7e207b908 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/YggdrasilUserAuthentication.java @@ -0,0 +1,225 @@ +package org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil; + +import java.net.URL; +import java.util.Arrays; +import java.util.Map; +import org.jackhuang.hellominecraft.C; +import org.jackhuang.hellominecraft.logging.logger.Logger; +import org.jackhuang.hellominecraft.utils.ArrayUtils; +import org.jackhuang.hellominecraft.utils.NetUtils; +import org.jackhuang.hellominecraft.utils.StrUtils; +import org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.GameProfile; +import org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.HttpUserAuthentication; +import org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.exceptions.AuthenticationException; +import org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.exceptions.InvalidCredentialsException; +import org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.request.AuthenticationRequest; +import org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.request.RefreshRequest; +import org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.response.AuthenticationResponse; +import org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.response.RefreshResponse; +import org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.response.User; + +public class YggdrasilUserAuthentication extends HttpUserAuthentication { + + private static final Logger LOGGER = new Logger("YggdrasilUserAuthentication"); + private static final String BASE_URL = "https://authserver.mojang.com/"; + private static final URL ROUTE_AUTHENTICATE = NetUtils.constantURL(BASE_URL + "authenticate"); + private static final URL ROUTE_REFRESH = NetUtils.constantURL(BASE_URL + "refresh"); + private static final String STORAGE_KEY_ACCESS_TOKEN = "accessToken"; + private GameProfile[] profiles; + private String accessToken; + private boolean isOnline; + + public YggdrasilUserAuthentication(YggdrasilAuthenticationService authenticationService) { + super(authenticationService); + } + + @Override + public boolean canLogIn() { + return (!canPlayOnline()) && (StrUtils.isNotBlank(getUsername())) && ((StrUtils.isNotBlank(getPassword())) || (StrUtils.isNotBlank(getAuthenticatedToken()))); + } + + @Override + public void logIn() throws AuthenticationException { + if (StrUtils.isBlank(getUsername())) { + throw new InvalidCredentialsException(C.i18n("login.invalid_username")); + } + + if (StrUtils.isNotBlank(getAuthenticatedToken())) { + logInWithToken(); + } else if (StrUtils.isNotBlank(getPassword())) { + logInWithPassword(); + } else { + throw new InvalidCredentialsException(C.i18n("login.invalid_password")); + } + } + + protected void logInWithPassword() throws AuthenticationException { + if (StrUtils.isBlank(getUsername())) { + throw new InvalidCredentialsException(C.i18n("login.invalid_username")); + } + if (StrUtils.isBlank(getPassword())) { + throw new InvalidCredentialsException(C.i18n("login.invalid_password")); + } + + LOGGER.info("Logging in with username & password"); + + AuthenticationRequest request = new AuthenticationRequest(this, getUsername(), getPassword()); + AuthenticationResponse response = (AuthenticationResponse) getAuthenticationService().makeRequest(ROUTE_AUTHENTICATE, request, AuthenticationResponse.class); + + if (!response.getClientToken().equals(getAuthenticationService().getClientToken())) { + throw new AuthenticationException(C.i18n("login.changed_client_token")); + } + + User user = response.getUser(); + + if ((user != null) && (user.getId() != null)) { + setUserid(user.getId()); + } else { + setUserid(getUsername()); + } + + this.isOnline = true; + this.accessToken = response.getAccessToken(); + + this.profiles = response.getAvailableProfiles(); + setSelectedProfile(response.getSelectedProfile()); + getModifiableUserProperties().clear(); + + updateUserProperties(user); + } + + protected void updateUserProperties(User user) { + if (user == null) { + return; + } + + if (user.getProperties() != null) { + getModifiableUserProperties().putAll(user.getProperties()); + } + } + + protected void logInWithToken() throws AuthenticationException { + if (StrUtils.isBlank(getUserID())) { + if (StrUtils.isBlank(getUsername())) { + setUserid(getUsername()); + } else { + throw new InvalidCredentialsException(C.i18n("login.invalid_uuid_and_username")); + } + } + if (StrUtils.isBlank(getAuthenticatedToken())) { + throw new InvalidCredentialsException(C.i18n("login.invalid_access_token")); + } + + LOGGER.info("Logging in with access token"); + + RefreshRequest request = new RefreshRequest(this); + RefreshResponse response = (RefreshResponse) getAuthenticationService().makeRequest(ROUTE_REFRESH, request, RefreshResponse.class); + + if (!response.getClientToken().equals(getAuthenticationService().getClientToken())) { + throw new AuthenticationException(C.i18n("login.changed_client_token")); + } + + if ((response.getUser() != null) && (response.getUser().getId() != null)) { + setUserid(response.getUser().getId()); + } else { + setUserid(getUsername()); + } + + this.isOnline = true; + this.accessToken = response.getAccessToken(); + this.profiles = response.getAvailableProfiles(); + setSelectedProfile(response.getSelectedProfile()); + getModifiableUserProperties().clear(); + + updateUserProperties(response.getUser()); + } + + @Override + public void logOut() { + super.logOut(); + + this.accessToken = null; + this.profiles = null; + this.isOnline = false; + } + + @Override + public GameProfile[] getAvailableProfiles() { + return this.profiles; + } + + @Override + public boolean isLoggedIn() { + return StrUtils.isNotBlank(this.accessToken); + } + + @Override + public boolean canPlayOnline() { + return (isLoggedIn()) && (getSelectedProfile() != null) && (this.isOnline); + } + + @Override + public void selectGameProfile(GameProfile profile) throws AuthenticationException { + if (!isLoggedIn()) { + throw new AuthenticationException(C.i18n("login.profile.not_logged_in")); + } + if (getSelectedProfile() != null) { + throw new AuthenticationException(C.i18n("login.profile.selected")); + } + if ((profile == null) || (!ArrayUtils.contains(this.profiles, profile))) { + throw new IllegalArgumentException("Invalid profile '" + profile + "'"); + } + + RefreshRequest request = new RefreshRequest(this, profile); + RefreshResponse response = (RefreshResponse) getAuthenticationService().makeRequest(ROUTE_REFRESH, request, RefreshResponse.class); + + if (!response.getClientToken().equals(getAuthenticationService().getClientToken())) { + throw new AuthenticationException(C.i18n("login.changed_client_token")); + } + + this.isOnline = true; + this.accessToken = response.getAccessToken(); + setSelectedProfile(response.getSelectedProfile()); + } + + @Override + public void loadFromStorage(Map credentials) { + super.loadFromStorage(credentials); + + this.accessToken = (String)credentials.get(STORAGE_KEY_ACCESS_TOKEN); + } + + @Override + public Map saveForStorage() { + Map result = super.saveForStorage(); + + if (StrUtils.isNotBlank(getAuthenticatedToken())) { + result.put("accessToken", getAuthenticatedToken()); + } + + return result; + } + + @Deprecated + public String getSessionToken() { + if ((isLoggedIn()) && (getSelectedProfile() != null) && (canPlayOnline())) { + return String.format("token:%s:%s", new Object[]{getAuthenticatedToken(), getSelectedProfile().getId()}); + } + return null; + } + + @Override + public String getAuthenticatedToken() { + return this.accessToken; + } + + @Override + public String toString() { + return "YggdrasilAuthenticationService{profiles=" + Arrays.toString(this.profiles) + ", selectedProfile=" + getSelectedProfile() + ", username='" + getUsername() + '\'' + ", isLoggedIn=" + isLoggedIn() + ", canPlayOnline=" + canPlayOnline() + ", accessToken='" + this.accessToken + '\'' + ", clientToken='" + getAuthenticationService().getClientToken() + '\'' + '}'; + } + + @Override + public YggdrasilAuthenticationService getAuthenticationService() { + return (YggdrasilAuthenticationService) super.getAuthenticationService(); + } +} \ No newline at end of file diff --git a/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/exceptions/AuthenticationException.java b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/exceptions/AuthenticationException.java new file mode 100644 index 000000000..73ed7aa80 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/exceptions/AuthenticationException.java @@ -0,0 +1,19 @@ +package org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.exceptions; + +public class AuthenticationException extends Exception { + + public AuthenticationException() { + } + + public AuthenticationException(String message) { + super(message); + } + + public AuthenticationException(String message, Throwable cause) { + super(message, cause); + } + + public AuthenticationException(Throwable cause) { + super(cause); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/exceptions/InvalidCredentialsException.java b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/exceptions/InvalidCredentialsException.java new file mode 100644 index 000000000..a4066fc15 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/exceptions/InvalidCredentialsException.java @@ -0,0 +1,19 @@ +package org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.exceptions; + +public class InvalidCredentialsException extends AuthenticationException { + + public InvalidCredentialsException() { + } + + public InvalidCredentialsException(String message) { + super(message); + } + + public InvalidCredentialsException(String message, Throwable cause) { + super(message, cause); + } + + public InvalidCredentialsException(Throwable cause) { + super(cause); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/properties/LegacyPropertyMapSerializer.java b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/properties/LegacyPropertyMapSerializer.java new file mode 100644 index 000000000..fb4010b55 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/properties/LegacyPropertyMapSerializer.java @@ -0,0 +1,37 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.properties; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import java.lang.reflect.Type; + +/** + * + * @author huangyuhui + */ +public class LegacyPropertyMapSerializer + implements JsonSerializer { + + @Override + public JsonElement serialize(PropertyMap src, Type typeOfSrc, JsonSerializationContext context) { + JsonObject result = new JsonObject(); + + for (String key : src.keySet()) { + JsonArray values = new JsonArray(); + + values.add(new JsonPrimitive(src.get(key).getValue())); + + result.add(key, values); + } + + return result; + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/properties/Property.java b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/properties/Property.java new file mode 100644 index 000000000..933890e5c --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/properties/Property.java @@ -0,0 +1,57 @@ +package org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.properties; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import org.jackhuang.hellominecraft.utils.code.Base64; + +public class Property { + + private final String name; + private final String value; + private final String signature; + + public Property(String value, String name) { + this(value, name, null); + } + + public Property(String name, String value, String signature) { + this.name = name; + this.value = value; + this.signature = signature; + } + + public String getName() { + return this.name; + } + + public String getValue() { + return this.value; + } + + public String getSignature() { + return this.signature; + } + + public boolean hasSignature() { + return this.signature != null; + } + + public boolean isSignatureValid(PublicKey publicKey) { + try { + Signature signature = Signature.getInstance("SHA1withRSA"); + signature.initVerify(publicKey); + signature.update(this.value.getBytes()); + return signature.verify(Base64.decode(this.signature.toCharArray())); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } catch (InvalidKeyException e) { + e.printStackTrace(); + } catch (SignatureException e) { + e.printStackTrace(); + } + return false; + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/properties/PropertyMap.java b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/properties/PropertyMap.java new file mode 100644 index 000000000..5022a081d --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/properties/PropertyMap.java @@ -0,0 +1,79 @@ +package org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.properties; + +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +public class PropertyMap extends HashMap { + + public PropertyMap() { + } + + public static class Serializer implements JsonSerializer, JsonDeserializer { + + @Override + public PropertyMap deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + PropertyMap result = new PropertyMap(); + Iterator i$; + Map.Entry entry; + if ((json instanceof JsonObject)) { + JsonObject object = (JsonObject) json; + + for (i$ = object.entrySet().iterator(); i$.hasNext();) { + entry = (Map.Entry) i$.next(); + if ((entry.getValue() instanceof JsonArray)) { + for (JsonElement element : (JsonArray) entry.getValue()) { + result.put(entry.getKey(), + new Property((String) 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(); + + if (object.has("signature")) { + result.put(name, new Property(name, value, object.getAsJsonPrimitive("signature").getAsString())); + } else { + result.put(name, new Property(name, value)); + } + } + } + } + + return result; + } + + @Override + public JsonElement serialize(PropertyMap src, Type typeOfSrc, JsonSerializationContext context) { + JsonArray result = new JsonArray(); + + for (Property property : src.values()) { + JsonObject object = new JsonObject(); + + object.addProperty("name", property.getName()); + object.addProperty("value", property.getValue()); + + if (property.hasSignature()) { + object.addProperty("signature", property.getSignature()); + } + + result.add(object); + } + + return result; + } + } +} \ No newline at end of file diff --git a/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/request/AuthenticationRequest.java b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/request/AuthenticationRequest.java new file mode 100644 index 000000000..6713a46d7 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/request/AuthenticationRequest.java @@ -0,0 +1,23 @@ +package org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.request; + +import java.util.HashMap; +import org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.YggdrasilUserAuthentication; + +public class AuthenticationRequest { + + public HashMap agent; + public String username; + public String password; + public String clientToken; + public boolean requestUser = true; + + public AuthenticationRequest(YggdrasilUserAuthentication authenticationService, String username, String password) { + agent = new HashMap<>(); + agent.put("name", "Minecraft"); + agent.put("version", 1); + + this.username = username; + this.clientToken = authenticationService.getAuthenticationService().getClientToken(); + this.password = password; + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/request/RefreshRequest.java b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/request/RefreshRequest.java new file mode 100644 index 000000000..31d437fdd --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/request/RefreshRequest.java @@ -0,0 +1,22 @@ +package org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.request; + +import org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.GameProfile; +import org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.YggdrasilUserAuthentication; + +public class RefreshRequest { + + public String clientToken; + public String accessToken; + public GameProfile selectedProfile; + public boolean requestUser = true; + + public RefreshRequest(YggdrasilUserAuthentication authenticationService) { + this(authenticationService, null); + } + + public RefreshRequest(YggdrasilUserAuthentication authenticationService, GameProfile profile) { + this.clientToken = authenticationService.getAuthenticationService().getClientToken(); + this.accessToken = authenticationService.getAuthenticatedToken(); + this.selectedProfile = profile; + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/response/AuthenticationResponse.java b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/response/AuthenticationResponse.java new file mode 100644 index 000000000..730ef0944 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/response/AuthenticationResponse.java @@ -0,0 +1,32 @@ +package org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.response; + +import org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.GameProfile; + +public class AuthenticationResponse extends Response { + + private String accessToken; + private String clientToken; + private GameProfile selectedProfile; + private GameProfile[] availableProfiles; + private User user; + + public String getAccessToken() { + return this.accessToken; + } + + public String getClientToken() { + return this.clientToken; + } + + public GameProfile[] getAvailableProfiles() { + return this.availableProfiles; + } + + public GameProfile getSelectedProfile() { + return this.selectedProfile; + } + + public User getUser() { + return this.user; + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/response/RefreshResponse.java b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/response/RefreshResponse.java new file mode 100644 index 000000000..4d214136d --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/response/RefreshResponse.java @@ -0,0 +1,32 @@ +package org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.response; + +import org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.GameProfile; + +public class RefreshResponse extends Response { + + private String accessToken; + private String clientToken; + private GameProfile selectedProfile; + private GameProfile[] availableProfiles; + private User user; + + public String getAccessToken() { + return this.accessToken; + } + + public String getClientToken() { + return this.clientToken; + } + + public GameProfile[] getAvailableProfiles() { + return this.availableProfiles; + } + + public GameProfile getSelectedProfile() { + return this.selectedProfile; + } + + public User getUser() { + return this.user; + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/response/Response.java b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/response/Response.java new file mode 100644 index 000000000..8a35bf45a --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/response/Response.java @@ -0,0 +1,20 @@ +package org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.response; + +public class Response { + + private String error; + private String errorMessage; + private String cause; + + public String getError() { + return this.error; + } + + public String getCause() { + return this.cause; + } + + public String getErrorMessage() { + return this.errorMessage; + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/response/User.java b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/response/User.java new file mode 100644 index 000000000..de8d32a4a --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hellominecraft/launcher/utils/auth/yggdrasil/response/User.java @@ -0,0 +1,17 @@ +package org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.response; + +import org.jackhuang.hellominecraft.launcher.utils.auth.yggdrasil.properties.PropertyMap; + +public class User { + + private String id; + private PropertyMap properties; + + public String getId() { + return this.id; + } + + public PropertyMap getProperties() { + return this.properties; + } +}