From 80352a24bf9dc39175ab0ce1c52edfa0a0d4abd5 Mon Sep 17 00:00:00 2001 From: azures04 Date: Tue, 2 Dec 2025 04:19:36 +0100 Subject: [PATCH] first commit --- .settings/org.eclipse.buildship.core.prefs | 13 + .settings/org.eclipse.jdt.core.prefs | 11 + build.gradle | 66 +++++ gradle/libs.versions.toml | 12 + gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 172 ++++++++++++ gradlew.bat | 84 ++++++ settings.gradle | 18 ++ src/main/java/com/mojang/authlib/Agent.java | 25 ++ .../mojang/authlib/AuthenticationService.java | 11 + .../authlib/BaseAuthenticationService.java | 4 + .../authlib/BaseUserAuthentication.java | 245 ++++++++++++++++++ .../java/com/mojang/authlib/GameProfile.java | 78 ++++++ .../mojang/authlib/GameProfileRepository.java | 5 + .../authlib/HttpAuthenticationService.java | 173 +++++++++++++ .../authlib/HttpUserAuthentication.java | 11 + .../mojang/authlib/ProfileLookupCallback.java | 7 + .../mojang/authlib/UserAuthentication.java | 39 +++ .../java/com/mojang/authlib/UserType.java | 30 +++ .../exceptions/AuthenticationException.java | 19 ++ .../AuthenticationUnavailableException.java | 19 ++ .../InvalidCredentialsException.java | 19 ++ .../exceptions/UserMigratedException.java | 19 ++ .../legacy/LegacyAuthenticationService.java | 30 +++ .../legacy/LegacyMinecraftSessionService.java | 65 +++++ .../legacy/LegacyUserAuthentication.java | 94 +++++++ .../BaseMinecraftSessionService.java | 15 ++ .../HttpMinecraftSessionService.java | 13 + .../minecraft/InsecureTextureException.java | 48 ++++ .../minecraft/MinecraftProfileTexture.java | 38 +++ .../minecraft/MinecraftSessionService.java | 16 ++ .../mojang/authlib/properties/Property.java | 57 ++++ .../authlib/properties/PropertyMap.java | 75 ++++++ .../yggdrasil/ProfileIncompleteException.java | 20 ++ .../yggdrasil/ProfileNotFoundException.java | 19 ++ .../YggdrasilAuthenticationService.java | 113 ++++++++ .../YggdrasilGameProfileRepository.java | 105 ++++++++ .../YggdrasilMinecraftSessionService.java | 201 ++++++++++++++ .../YggdrasilUserAuthentication.java | 242 +++++++++++++++++ .../request/AuthenticationRequest.java | 19 ++ .../yggdrasil/request/InvalidateRequest.java | 13 + .../request/JoinMinecraftServerRequest.java | 9 + .../yggdrasil/request/RefreshRequest.java | 21 ++ .../yggdrasil/request/ValidateRequest.java | 13 + .../response/AuthenticationResponse.java | 31 +++ .../HasJoinedMinecraftServerResponse.java | 17 ++ .../MinecraftProfilePropertiesResponse.java | 22 ++ .../response/MinecraftTexturesPayload.java | 33 +++ .../ProfileSearchResultsResponse.java | 41 +++ .../yggdrasil/response/RefreshResponse.java | 31 +++ .../authlib/yggdrasil/response/Response.java | 31 +++ .../authlib/yggdrasil/response/User.java | 16 ++ .../com/mojang/util/QueueLogAppender.java | 87 +++++++ .../java/com/mojang/util/UUIDTypeAdapter.java | 25 ++ .../fr/azures04/modun/EndpointsManager.java | 66 +++++ .../YggdrasilSessionPublicKeyManager.java | 110 ++++++++ .../fr/azures04/modun/dto/PublicKeyEntry.java | 14 + .../modun/dto/PublicKeysResponse.java | 25 ++ .../resources/yggdrasil_session_pubkey.der | Bin 0 -> 550 bytes 59 files changed, 2860 insertions(+) create mode 100644 .settings/org.eclipse.buildship.core.prefs create mode 100644 .settings/org.eclipse.jdt.core.prefs create mode 100644 build.gradle create mode 100644 gradle/libs.versions.toml create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 src/main/java/com/mojang/authlib/Agent.java create mode 100644 src/main/java/com/mojang/authlib/AuthenticationService.java create mode 100644 src/main/java/com/mojang/authlib/BaseAuthenticationService.java create mode 100644 src/main/java/com/mojang/authlib/BaseUserAuthentication.java create mode 100644 src/main/java/com/mojang/authlib/GameProfile.java create mode 100644 src/main/java/com/mojang/authlib/GameProfileRepository.java create mode 100644 src/main/java/com/mojang/authlib/HttpAuthenticationService.java create mode 100644 src/main/java/com/mojang/authlib/HttpUserAuthentication.java create mode 100644 src/main/java/com/mojang/authlib/ProfileLookupCallback.java create mode 100644 src/main/java/com/mojang/authlib/UserAuthentication.java create mode 100644 src/main/java/com/mojang/authlib/UserType.java create mode 100644 src/main/java/com/mojang/authlib/exceptions/AuthenticationException.java create mode 100644 src/main/java/com/mojang/authlib/exceptions/AuthenticationUnavailableException.java create mode 100644 src/main/java/com/mojang/authlib/exceptions/InvalidCredentialsException.java create mode 100644 src/main/java/com/mojang/authlib/exceptions/UserMigratedException.java create mode 100644 src/main/java/com/mojang/authlib/legacy/LegacyAuthenticationService.java create mode 100644 src/main/java/com/mojang/authlib/legacy/LegacyMinecraftSessionService.java create mode 100644 src/main/java/com/mojang/authlib/legacy/LegacyUserAuthentication.java create mode 100644 src/main/java/com/mojang/authlib/minecraft/BaseMinecraftSessionService.java create mode 100644 src/main/java/com/mojang/authlib/minecraft/HttpMinecraftSessionService.java create mode 100644 src/main/java/com/mojang/authlib/minecraft/InsecureTextureException.java create mode 100644 src/main/java/com/mojang/authlib/minecraft/MinecraftProfileTexture.java create mode 100644 src/main/java/com/mojang/authlib/minecraft/MinecraftSessionService.java create mode 100644 src/main/java/com/mojang/authlib/properties/Property.java create mode 100644 src/main/java/com/mojang/authlib/properties/PropertyMap.java create mode 100644 src/main/java/com/mojang/authlib/yggdrasil/ProfileIncompleteException.java create mode 100644 src/main/java/com/mojang/authlib/yggdrasil/ProfileNotFoundException.java create mode 100644 src/main/java/com/mojang/authlib/yggdrasil/YggdrasilAuthenticationService.java create mode 100644 src/main/java/com/mojang/authlib/yggdrasil/YggdrasilGameProfileRepository.java create mode 100644 src/main/java/com/mojang/authlib/yggdrasil/YggdrasilMinecraftSessionService.java create mode 100644 src/main/java/com/mojang/authlib/yggdrasil/YggdrasilUserAuthentication.java create mode 100644 src/main/java/com/mojang/authlib/yggdrasil/request/AuthenticationRequest.java create mode 100644 src/main/java/com/mojang/authlib/yggdrasil/request/InvalidateRequest.java create mode 100644 src/main/java/com/mojang/authlib/yggdrasil/request/JoinMinecraftServerRequest.java create mode 100644 src/main/java/com/mojang/authlib/yggdrasil/request/RefreshRequest.java create mode 100644 src/main/java/com/mojang/authlib/yggdrasil/request/ValidateRequest.java create mode 100644 src/main/java/com/mojang/authlib/yggdrasil/response/AuthenticationResponse.java create mode 100644 src/main/java/com/mojang/authlib/yggdrasil/response/HasJoinedMinecraftServerResponse.java create mode 100644 src/main/java/com/mojang/authlib/yggdrasil/response/MinecraftProfilePropertiesResponse.java create mode 100644 src/main/java/com/mojang/authlib/yggdrasil/response/MinecraftTexturesPayload.java create mode 100644 src/main/java/com/mojang/authlib/yggdrasil/response/ProfileSearchResultsResponse.java create mode 100644 src/main/java/com/mojang/authlib/yggdrasil/response/RefreshResponse.java create mode 100644 src/main/java/com/mojang/authlib/yggdrasil/response/Response.java create mode 100644 src/main/java/com/mojang/authlib/yggdrasil/response/User.java create mode 100644 src/main/java/com/mojang/util/QueueLogAppender.java create mode 100644 src/main/java/com/mojang/util/UUIDTypeAdapter.java create mode 100644 src/main/java/fr/azures04/modun/EndpointsManager.java create mode 100644 src/main/java/fr/azures04/modun/YggdrasilSessionPublicKeyManager.java create mode 100644 src/main/java/fr/azures04/modun/dto/PublicKeyEntry.java create mode 100644 src/main/java/fr/azures04/modun/dto/PublicKeysResponse.java create mode 100644 src/main/resources/yggdrasil_session_pubkey.der diff --git a/.settings/org.eclipse.buildship.core.prefs b/.settings/org.eclipse.buildship.core.prefs new file mode 100644 index 0000000..4fcdc3f --- /dev/null +++ b/.settings/org.eclipse.buildship.core.prefs @@ -0,0 +1,13 @@ +arguments= +auto.sync=false +build.scans.enabled=false +connection.gradle.distribution=GRADLE_DISTRIBUTION(VERSION(8.5)) +connection.project.dir= +eclipse.preferences.version=1 +gradle.user.home= +java.home= +jvm.arguments= +offline.mode=false +override.workspace.settings=false +show.console.view=false +show.executions.view=false diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..dc9c8c7 --- /dev/null +++ b/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,11 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=21 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=21 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=21 diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..aebef3c --- /dev/null +++ b/build.gradle @@ -0,0 +1,66 @@ +buildscript { + repositories { + gradlePluginPortal() + } + dependencies { + classpath "com.github.johnrengelman:shadow:8.1.1" + classpath group: "org.yaml", name: "snakeyaml", version: "1.19" + } +} + +plugins { + id "net.researchgate.release" version "3.0.2" +} + +apply plugin: "java-library" +apply plugin: "eclipse" +apply plugin: "com.github.johnrengelman.shadow" + + +version = "1.5.21.1" +group = "fr.azures04.modun" +archivesBaseName = "authlib" + + +repositories { + jcenter() + maven { + url = "https://hub.spigotmc.org/nexus/content/repositories/snapshots/" + } + maven { url = "https://oss.sonatype.org/content/repositories/snapshots" } + maven { url = "https://oss.sonatype.org/content/repositories/central" } +} + +dependencies { + implementation("org.apache.logging.log4j:log4j-core:2.9.0") + implementation("com.google.code.gson:gson:2.8.6") + implementation("org.apache.commons:commons-lang3:3.3.2") + implementation("commons-io:commons-io:2.4") + implementation("javax.annotation:javax.annotation-api:1.3.2") + implementation("com.google.code.findbugs:jsr305:3.0.2") + implementation("com.google.guava:guava:15.0") + implementation("commons-codec:commons-codec:1.9") +} + +sourceCompatibility = targetCompatibility = '1.8' +compileJava { + sourceCompatibility = targetCompatibility = '1.8' +} + +release { + failOnPublishNeeded = true + failOnSnapshotDependencies = true + failOnUnversionedFiles = true + failOnUpdateNeeded = true + preTagCommitMessage = "[Gradle Release Plugin] - pre tag commit: " + tagCommitMessage = "[Gradle Release Plugin] - creating tag: " + newVersionCommitMessage = "[Gradle Release Plugin] - new version commit: " + tagTemplate = "1.5.21.1" + versionPropertyFile = "gradle.properties" + snapshotSuffix = "-SNAPSHOT" + buildTasks = ["shadowJar"] + + git { + requireBranch.set("main") + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..255af7c --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,12 @@ +# This file was generated by the Gradle 'init' task. +# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format + +[versions] +commons-math3 = "3.6.1" +guava = "32.1.2-jre" +junit-jupiter = "5.10.0" + +[libraries] +commons-math3 = { module = "org.apache.commons:commons-math3", version.ref = "commons-math3" } +guava = { module = "com.google.guava:guava", version.ref = "guava" } +junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..03c0356 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip \ No newline at end of file diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..cccdd3d --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..6d981ac --- /dev/null +++ b/settings.gradle @@ -0,0 +1,18 @@ +/* + * This settings file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * In a single project build this file can be empty or even removed. + * + * Detailed information about configuring a multi-project build in Gradle can be found + * in the user guide at https://docs.gradle.org/4.1/userguide/multi_project_builds.html + */ + +/* +// To declare projects as part of a multi-project build use the 'include' method +include 'shared' +include 'api' +include 'services:webservice' +*/ + +rootProject.name = 'Authlib' diff --git a/src/main/java/com/mojang/authlib/Agent.java b/src/main/java/com/mojang/authlib/Agent.java new file mode 100644 index 0000000..dfb931f --- /dev/null +++ b/src/main/java/com/mojang/authlib/Agent.java @@ -0,0 +1,25 @@ +package com.mojang.authlib; + +public class Agent { + public static final Agent MINECRAFT = new Agent("Minecraft", 1); + public static final Agent SCROLLS = new Agent("Scrolls", 1); + private final String name; + private final int version; + + public Agent(String name, int version) { + this.name = name; + this.version = version; + } + + public String getName() { + return this.name; + } + + public int getVersion() { + return this.version; + } + + public String toString() { + return "Agent{name='" + this.name + '\'' + ", version=" + this.version + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/com/mojang/authlib/AuthenticationService.java b/src/main/java/com/mojang/authlib/AuthenticationService.java new file mode 100644 index 0000000..de16d7a --- /dev/null +++ b/src/main/java/com/mojang/authlib/AuthenticationService.java @@ -0,0 +1,11 @@ +package com.mojang.authlib; + +import com.mojang.authlib.minecraft.MinecraftSessionService; + +public interface AuthenticationService { + UserAuthentication createUserAuthentication(Agent agent); + + MinecraftSessionService createMinecraftSessionService(); + + GameProfileRepository createProfileRepository(); +} \ No newline at end of file diff --git a/src/main/java/com/mojang/authlib/BaseAuthenticationService.java b/src/main/java/com/mojang/authlib/BaseAuthenticationService.java new file mode 100644 index 0000000..6b61ca3 --- /dev/null +++ b/src/main/java/com/mojang/authlib/BaseAuthenticationService.java @@ -0,0 +1,4 @@ +package com.mojang.authlib; + +public abstract class BaseAuthenticationService implements AuthenticationService { +} diff --git a/src/main/java/com/mojang/authlib/BaseUserAuthentication.java b/src/main/java/com/mojang/authlib/BaseUserAuthentication.java new file mode 100644 index 0000000..a76ec16 --- /dev/null +++ b/src/main/java/com/mojang/authlib/BaseUserAuthentication.java @@ -0,0 +1,245 @@ +package com.mojang.authlib; + +import com.mojang.authlib.properties.Property; +import com.mojang.authlib.properties.PropertyMap; +import com.mojang.util.UUIDTypeAdapter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public abstract class BaseUserAuthentication implements UserAuthentication { + private static final Logger LOGGER = LogManager.getLogger(); + 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; + private UserType userType; + + protected BaseUserAuthentication(AuthenticationService authenticationService) { + Validate.notNull(authenticationService); + this.authenticationService = authenticationService; + } + + public boolean canLogIn() { + return !this.canPlayOnline() && StringUtils.isNotBlank(this.getUsername()) && StringUtils.isNotBlank(this.getPassword()); + } + + public void logOut() { + this.password = null; + this.userid = null; + this.setSelectedProfile((GameProfile) null); + this.getModifiableUserProperties().clear(); + this.setUserType((UserType) null); + } + + public boolean isLoggedIn() { + return this.getSelectedProfile() != null; + } + + public void setUsername(String username) { + if (this.isLoggedIn() && this.canPlayOnline()) { + throw new IllegalStateException("Cannot change username whilst logged in & online"); + } else { + this.username = username; + } + } + + public void setPassword(String password) { + if (this.isLoggedIn() && this.canPlayOnline() && StringUtils.isNotBlank(password)) { + throw new IllegalStateException("Cannot set password whilst logged in & online"); + } else { + this.password = password; + } + } + + protected String getUsername() { + return this.username; + } + + protected String getPassword() { + return this.password; + } + + @SuppressWarnings("unchecked") + public void loadFromStorage(Map credentials) { + this.logOut(); + this.setUsername(String.valueOf(credentials.get("username"))); + if (credentials.containsKey("userid")) { + this.userid = String.valueOf(credentials.get("userid")); + } else { + this.userid = this.username; + } + + if (credentials.containsKey("userProperties")) { + try { + for (Map propertyMap : (List > ) credentials.get("userProperties")) { + String name = (String) propertyMap.get("name"); + String value = (String) propertyMap.get("value"); + String signature = (String) propertyMap.get("signature"); + if (signature == null) { + this.getModifiableUserProperties().put(name, new Property(name, value)); + } else { + this.getModifiableUserProperties().put(name, new Property(name, value, signature)); + } + } + } catch (Throwable throwable) { + LOGGER.warn("Couldn't deserialize user properties", throwable); + } + } + + if (credentials.containsKey("displayName") && credentials.containsKey("uuid")) { + GameProfile profile = new GameProfile(UUIDTypeAdapter.fromString(String.valueOf(credentials.get("uuid"))), String.valueOf(credentials.get("displayName"))); + if (credentials.containsKey("profileProperties")) { + try { + for (Map propertyMap : (List > ) credentials.get("profileProperties")) { + 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 throwable) { + LOGGER.warn("Couldn't deserialize profile properties", throwable); + } + } + + this.setSelectedProfile(profile); + } + + } + + public Map saveForStorage() { + Map result = new HashMap<>(); + if (this.getUsername() != null) { + result.put("username", this.getUsername()); + } + + if (this.getUserID() != null) { + result.put("userid", this.getUserID()); + } else if (this.getUsername() != null) { + result.put("username", this.getUsername()); + } + + if (!this.getUserProperties().isEmpty()) { + List > properties = new ArrayList<>(); + + for (Property userProperty : this.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 selectedProfile = this.getSelectedProfile(); + if (selectedProfile != null) { + result.put("displayName", selectedProfile.getName()); + result.put("uuid", selectedProfile.getId()); + List > properties = new ArrayList<>(); + + for (Property profileProperty : selectedProfile.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; + } + + public GameProfile getSelectedProfile() { + return this.selectedProfile; + } + + public String toString() { + StringBuilder result = new StringBuilder(); + result.append(this.getClass().getSimpleName()); + result.append("{"); + if (this.isLoggedIn()) { + result.append("Logged in as "); + result.append(this.getUsername()); + if (this.getSelectedProfile() != null) { + result.append(" / "); + result.append(this.getSelectedProfile()); + result.append(" - "); + if (this.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; + } + + public String getUserID() { + return this.userid; + } + + public PropertyMap getUserProperties() { + if (this.isLoggedIn()) { + PropertyMap result = new PropertyMap(); + result.putAll(this.getModifiableUserProperties()); + return result; + } else { + return new PropertyMap(); + } + } + + protected PropertyMap getModifiableUserProperties() { + return this.userProperties; + } + + public UserType getUserType() { + if (this.isLoggedIn()) { + return this.userType == null ? UserType.LEGACY : this.userType; + } else { + return null; + } + } + + protected void setUserType(UserType userType) { + this.userType = userType; + } + + protected void setUserid(String userid) { + this.userid = userid; + } +} \ No newline at end of file diff --git a/src/main/java/com/mojang/authlib/GameProfile.java b/src/main/java/com/mojang/authlib/GameProfile.java new file mode 100644 index 0000000..0fc4a32 --- /dev/null +++ b/src/main/java/com/mojang/authlib/GameProfile.java @@ -0,0 +1,78 @@ +package com.mojang.authlib; + +import com.mojang.authlib.properties.PropertyMap; +import java.util.UUID; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.builder.ToStringBuilder; + +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 && StringUtils.isBlank(name)) { + throw new IllegalArgumentException("Name and ID cannot both be blank"); + } else { + 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 && StringUtils.isNotBlank(this.getName()); + } + + public boolean equals(Object o) { + if (this == o) { + return true; + } else if (o != null && this.getClass() == o.getClass()) { + GameProfile that = (GameProfile) o; + if (this.id != null) { + if (!this.id.equals(that.id)) { + return false; + } + } else if (that.id != null) { + return false; + } + + if (this.name != null) { + if (!this.name.equals(that.name)) { + return false; + } + } else if (that.name != null) { + return false; + } + + return true; + } else { + return false; + } + } + + public int hashCode() { + int result = this.id != null ? this.id.hashCode() : 0; + return 31 * result + (this.name != null ? this.name.hashCode() : 0); + } + + public String toString() { + return (new ToStringBuilder(this)).append("id", this.id).append("name", this.name).append("properties", this.properties).append("legacy", this.legacy).toString(); + } + + public boolean isLegacy() { + return this.legacy; + } +} \ No newline at end of file diff --git a/src/main/java/com/mojang/authlib/GameProfileRepository.java b/src/main/java/com/mojang/authlib/GameProfileRepository.java new file mode 100644 index 0000000..dd36d8e --- /dev/null +++ b/src/main/java/com/mojang/authlib/GameProfileRepository.java @@ -0,0 +1,5 @@ +package com.mojang.authlib; + +public interface GameProfileRepository { + void findProfilesByNames(String[] args, Agent agent, ProfileLookupCallback callback); +} \ No newline at end of file diff --git a/src/main/java/com/mojang/authlib/HttpAuthenticationService.java b/src/main/java/com/mojang/authlib/HttpAuthenticationService.java new file mode 100644 index 0000000..218f487 --- /dev/null +++ b/src/main/java/com/mojang/authlib/HttpAuthenticationService.java @@ -0,0 +1,173 @@ +package com.mojang.authlib; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.Proxy; +import java.net.URL; +import java.net.URLEncoder; +import java.util.Map; +import org.apache.commons.io.Charsets; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.Validate; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public abstract class HttpAuthenticationService extends BaseAuthenticationService { + private static final Logger LOGGER = LogManager.getLogger(); + private final Proxy proxy; + + protected HttpAuthenticationService(Proxy proxy) { + Validate.notNull(proxy); + this.proxy = proxy; + } + + public Proxy getProxy() { + return this.proxy; + } + + protected HttpURLConnection createUrlConnection(URL url) throws IOException { + Validate.notNull(url); + 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 { + Validate.notNull(url); + Validate.notNull(post); + Validate.notNull(contentType); + HttpURLConnection connection = this.createUrlConnection(url); + byte[] postAsBytes = post.getBytes(Charsets.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; + + String response; + try { + inputStream = connection.getInputStream(); + String result = IOUtils.toString(inputStream, Charsets.UTF_8); + LOGGER.debug("Successful read, server response was " + connection.getResponseCode()); + LOGGER.debug("Response: " + result); + return result; + } catch (IOException exception) { + IOUtils.closeQuietly(inputStream); + inputStream = connection.getErrorStream(); + if (inputStream == null) { + LOGGER.debug("Request failed", exception); + throw exception; + } + + LOGGER.debug("Reading error page from " + url); + String result = IOUtils.toString(inputStream, Charsets.UTF_8); + LOGGER.debug("Successful read, server response was " + connection.getResponseCode()); + LOGGER.debug("Response: " + result); + response = result; + } finally { + IOUtils.closeQuietly(inputStream); + } + + return response; + } + + public String performGetRequest(URL url) throws IOException { + Validate.notNull(url); + HttpURLConnection connection = this.createUrlConnection(url); + LOGGER.debug("Reading data from " + url); + InputStream inputStream = null; + + String response; + try { + inputStream = connection.getInputStream(); + String result = IOUtils.toString(inputStream, Charsets.UTF_8); + LOGGER.debug("Successful read, server response was " + connection.getResponseCode()); + LOGGER.debug("Response: " + result); + return result; + } catch (IOException exception) { + IOUtils.closeQuietly(inputStream); + inputStream = connection.getErrorStream(); + if (inputStream == null) { + LOGGER.debug("Request failed", exception); + throw exception; + } + + LOGGER.debug("Reading error page from " + url); + String result = IOUtils.toString(inputStream, Charsets.UTF_8); + LOGGER.debug("Successful read, server response was " + connection.getResponseCode()); + LOGGER.debug("Response: " + result); + response = result; + } finally { + IOUtils.closeQuietly(inputStream); + } + + return response; + } + + @SuppressWarnings("deprecation") + public static URL constantURL(String url) { + try { + return new URL(url); + } catch (MalformedURLException exception) { + throw new Error("Couldn't create constant for " + url, exception); + } + } + + public static String buildQuery(Map query) { + if (query == null) { + return ""; + } else { + StringBuilder builder = new StringBuilder(); + + for (Map.Entry entry : query.entrySet()) { + if (builder.length()>0) { + builder.append('&'); + } + + try { + builder.append(URLEncoder.encode((String) entry.getKey(), "UTF-8")); + } catch (UnsupportedEncodingException exception) { + LOGGER.error("Unexpected exception building query", exception); + } + + if (entry.getValue() != null) { + builder.append('='); + + try { + builder.append(URLEncoder.encode(entry.getValue().toString(), "UTF-8")); + } catch (UnsupportedEncodingException exception) { + LOGGER.error("Unexpected exception building query", exception); + } + } + } + + return builder.toString(); + } + } + + @SuppressWarnings("deprecation") + public static URL concatenateURL(URL url, String query) { + try { + return url.getQuery() != null && url.getQuery().length()>0 ? new URL(url.getProtocol(), url.getHost(), url.getPort(), url.getFile() + "&" + query) : new URL(url.getProtocol(), url.getHost(), url.getPort(), url.getFile() + "?" + query); + } catch (MalformedURLException exception) { + throw new IllegalArgumentException("Could not concatenate given URL with GET arguments!", exception); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/mojang/authlib/HttpUserAuthentication.java b/src/main/java/com/mojang/authlib/HttpUserAuthentication.java new file mode 100644 index 0000000..64f2659 --- /dev/null +++ b/src/main/java/com/mojang/authlib/HttpUserAuthentication.java @@ -0,0 +1,11 @@ +package com.mojang.authlib; + +public abstract class HttpUserAuthentication extends BaseUserAuthentication { + protected HttpUserAuthentication(HttpAuthenticationService authenticationService) { + super(authenticationService); + } + + public HttpAuthenticationService getAuthenticationService() { + return (HttpAuthenticationService) super.getAuthenticationService(); + } +} \ No newline at end of file diff --git a/src/main/java/com/mojang/authlib/ProfileLookupCallback.java b/src/main/java/com/mojang/authlib/ProfileLookupCallback.java new file mode 100644 index 0000000..05bda14 --- /dev/null +++ b/src/main/java/com/mojang/authlib/ProfileLookupCallback.java @@ -0,0 +1,7 @@ +package com.mojang.authlib; + +public interface ProfileLookupCallback { + void onProfileLookupSucceeded(GameProfile profile); + + void onProfileLookupFailed(GameProfile profile, Exception exception); +} \ No newline at end of file diff --git a/src/main/java/com/mojang/authlib/UserAuthentication.java b/src/main/java/com/mojang/authlib/UserAuthentication.java new file mode 100644 index 0000000..68ca9a9 --- /dev/null +++ b/src/main/java/com/mojang/authlib/UserAuthentication.java @@ -0,0 +1,39 @@ +package com.mojang.authlib; + +import com.mojang.authlib.exceptions.AuthenticationException; +import com.mojang.authlib.properties.PropertyMap; +import java.util.Map; + +public interface UserAuthentication { + boolean canLogIn(); + + void logIn() throws AuthenticationException; + + void logOut(); + + boolean isLoggedIn(); + + boolean canPlayOnline(); + + GameProfile[] getAvailableProfiles(); + + GameProfile getSelectedProfile(); + + void selectGameProfile(GameProfile profile) throws AuthenticationException; + + void loadFromStorage(Map credentials); + + Map saveForStorage(); + + void setUsername(String username); + + void setPassword(String password); + + String getAuthenticatedToken(); + + String getUserID(); + + PropertyMap getUserProperties(); + + UserType getUserType(); +} \ No newline at end of file diff --git a/src/main/java/com/mojang/authlib/UserType.java b/src/main/java/com/mojang/authlib/UserType.java new file mode 100644 index 0000000..e3d5e39 --- /dev/null +++ b/src/main/java/com/mojang/authlib/UserType.java @@ -0,0 +1,30 @@ +package com.mojang.authlib; + +import java.util.HashMap; +import java.util.Map; + +public enum UserType { + LEGACY("legacy"), + MOJANG("mojang"); + + private static final Map BY_NAME = new HashMap <>(); + private final String name; + + private UserType(String name) { + this.name = name; + } + + public static UserType byName(String name) { + return (UserType) BY_NAME.get(name.toLowerCase()); + } + + public String getName() { + return this.name; + } + + static { + for (UserType type : values()) { + BY_NAME.put(type.name, type); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/mojang/authlib/exceptions/AuthenticationException.java b/src/main/java/com/mojang/authlib/exceptions/AuthenticationException.java new file mode 100644 index 0000000..6850457 --- /dev/null +++ b/src/main/java/com/mojang/authlib/exceptions/AuthenticationException.java @@ -0,0 +1,19 @@ +package com.mojang.authlib.exceptions; + +public class AuthenticationException extends Exception { + private static final long serialVersionUID = 4871782997693067748L; + + public AuthenticationException() {} + + public AuthenticationException(String message) { + super(message); + } + + public AuthenticationException(String message, Throwable cause) { + super(message, cause); + } + + public AuthenticationException(Throwable cause) { + super(cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/mojang/authlib/exceptions/AuthenticationUnavailableException.java b/src/main/java/com/mojang/authlib/exceptions/AuthenticationUnavailableException.java new file mode 100644 index 0000000..b972281 --- /dev/null +++ b/src/main/java/com/mojang/authlib/exceptions/AuthenticationUnavailableException.java @@ -0,0 +1,19 @@ +package com.mojang.authlib.exceptions; + +public class AuthenticationUnavailableException extends AuthenticationException { + private static final long serialVersionUID = 3489067028489894211L; + + public AuthenticationUnavailableException() {} + + public AuthenticationUnavailableException(String message) { + super(message); + } + + public AuthenticationUnavailableException(String message, Throwable cause) { + super(message, cause); + } + + public AuthenticationUnavailableException(Throwable cause) { + super(cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/mojang/authlib/exceptions/InvalidCredentialsException.java b/src/main/java/com/mojang/authlib/exceptions/InvalidCredentialsException.java new file mode 100644 index 0000000..6a22570 --- /dev/null +++ b/src/main/java/com/mojang/authlib/exceptions/InvalidCredentialsException.java @@ -0,0 +1,19 @@ +package com.mojang.authlib.exceptions; + +public class InvalidCredentialsException extends AuthenticationException { + private static final long serialVersionUID = -2648308630825738711L; + + public InvalidCredentialsException() {} + + public InvalidCredentialsException(String message) { + super(message); + } + + public InvalidCredentialsException(String message, Throwable cause) { + super(message, cause); + } + + public InvalidCredentialsException(Throwable cause) { + super(cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/mojang/authlib/exceptions/UserMigratedException.java b/src/main/java/com/mojang/authlib/exceptions/UserMigratedException.java new file mode 100644 index 0000000..ab9a4cf --- /dev/null +++ b/src/main/java/com/mojang/authlib/exceptions/UserMigratedException.java @@ -0,0 +1,19 @@ +package com.mojang.authlib.exceptions; + +public class UserMigratedException extends InvalidCredentialsException { + private static final long serialVersionUID = 6537065075102000065L; + + public UserMigratedException() {} + + public UserMigratedException(String message) { + super(message); + } + + public UserMigratedException(String message, Throwable cause) { + super(message, cause); + } + + public UserMigratedException(Throwable cause) { + super(cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/mojang/authlib/legacy/LegacyAuthenticationService.java b/src/main/java/com/mojang/authlib/legacy/LegacyAuthenticationService.java new file mode 100644 index 0000000..92ae707 --- /dev/null +++ b/src/main/java/com/mojang/authlib/legacy/LegacyAuthenticationService.java @@ -0,0 +1,30 @@ +package com.mojang.authlib.legacy; + +import com.mojang.authlib.Agent; +import com.mojang.authlib.GameProfileRepository; +import com.mojang.authlib.HttpAuthenticationService; +import java.net.Proxy; +import org.apache.commons.lang3.Validate; + +public class LegacyAuthenticationService extends HttpAuthenticationService { + protected LegacyAuthenticationService(Proxy proxy) { + super(proxy); + } + + public LegacyUserAuthentication createUserAuthentication(Agent agent) { + Validate.notNull(agent); + if (agent != Agent.MINECRAFT) { + throw new IllegalArgumentException("Legacy authentication cannot handle anything but Minecraft"); + } else { + return new LegacyUserAuthentication(this); + } + } + + public LegacyMinecraftSessionService createMinecraftSessionService() { + return new LegacyMinecraftSessionService(this); + } + + public GameProfileRepository createProfileRepository() { + throw new UnsupportedOperationException("Legacy authentication service has no profile repository"); + } +} \ No newline at end of file diff --git a/src/main/java/com/mojang/authlib/legacy/LegacyMinecraftSessionService.java b/src/main/java/com/mojang/authlib/legacy/LegacyMinecraftSessionService.java new file mode 100644 index 0000000..754bd66 --- /dev/null +++ b/src/main/java/com/mojang/authlib/legacy/LegacyMinecraftSessionService.java @@ -0,0 +1,65 @@ +package com.mojang.authlib.legacy; + +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.HttpAuthenticationService; +import com.mojang.authlib.exceptions.AuthenticationException; +import com.mojang.authlib.exceptions.AuthenticationUnavailableException; +import com.mojang.authlib.minecraft.HttpMinecraftSessionService; +import com.mojang.authlib.minecraft.MinecraftProfileTexture; +import java.io.IOException; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +public class LegacyMinecraftSessionService extends HttpMinecraftSessionService { + private static final String BASE_URL = "http://session.minecraft.net/game/"; + private static final URL JOIN_URL = HttpAuthenticationService.constantURL(BASE_URL + "joinserver.jsp"); + private static final URL CHECK_URL = HttpAuthenticationService.constantURL(BASE_URL + "checkserver.jsp"); + + protected LegacyMinecraftSessionService(LegacyAuthenticationService authenticationService) { + super(authenticationService); + } + + public void joinServer(GameProfile profile, String authenticationToken, String serverId) throws AuthenticationException { + Map arguments = new HashMap <>(); + arguments.put("user", profile.getName()); + arguments.put("sessionId", authenticationToken); + arguments.put("serverId", serverId); + URL url = HttpAuthenticationService.concatenateURL(JOIN_URL, HttpAuthenticationService.buildQuery(arguments)); + + try { + String response = this.getAuthenticationService().performGetRequest(url); + if (!response.equals("OK")) { + throw new AuthenticationException(response); + } + } catch (IOException exception) { + throw new AuthenticationUnavailableException(exception); + } + } + + public GameProfile hasJoinedServer(GameProfile user, String serverId) throws AuthenticationUnavailableException { + Map arguments = new HashMap <>(); + arguments.put("user", user.getName()); + arguments.put("serverId", serverId); + URL url = HttpAuthenticationService.concatenateURL(CHECK_URL, HttpAuthenticationService.buildQuery(arguments)); + + try { + String response = this.getAuthenticationService().performGetRequest(url); + return response.equals("YES") ? user : null; + } catch (IOException exception) { + throw new AuthenticationUnavailableException(exception); + } + } + + public Map getTextures(GameProfile profile, boolean requireSecure) { + return new HashMap <>(); + } + + public GameProfile fillProfileProperties(GameProfile profile, boolean requireSecure) { + return profile; + } + + public LegacyAuthenticationService getAuthenticationService() { + return (LegacyAuthenticationService) super.getAuthenticationService(); + } +} \ No newline at end of file diff --git a/src/main/java/com/mojang/authlib/legacy/LegacyUserAuthentication.java b/src/main/java/com/mojang/authlib/legacy/LegacyUserAuthentication.java new file mode 100644 index 0000000..a7c9774 --- /dev/null +++ b/src/main/java/com/mojang/authlib/legacy/LegacyUserAuthentication.java @@ -0,0 +1,94 @@ +package com.mojang.authlib.legacy; + +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.HttpAuthenticationService; +import com.mojang.authlib.HttpUserAuthentication; +import com.mojang.authlib.UserType; +import com.mojang.authlib.exceptions.AuthenticationException; +import com.mojang.authlib.exceptions.InvalidCredentialsException; +import com.mojang.util.UUIDTypeAdapter; +import java.io.IOException; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import org.apache.commons.lang3.StringUtils; + +public class LegacyUserAuthentication extends HttpUserAuthentication { + private static final URL AUTHENTICATION_URL = HttpAuthenticationService.constantURL("https://login.minecraft.net"); + private static final int AUTHENTICATION_VERSION = 14; + private static final int RESPONSE_PART_PROFILE_NAME = 2; + private static final int RESPONSE_PART_SESSION_TOKEN = 3; + private static final int RESPONSE_PART_PROFILE_ID = 4; + private String sessionToken; + + protected LegacyUserAuthentication(LegacyAuthenticationService authenticationService) { + super(authenticationService); + } + + public void logIn() throws AuthenticationException { + if (StringUtils.isBlank(this.getUsername())) { + throw new InvalidCredentialsException("Invalid username"); + } else if (StringUtils.isBlank(this.getPassword())) { + throw new InvalidCredentialsException("Invalid password"); + } else { + Map args = new HashMap <>(); + args.put("user", this.getUsername()); + args.put("password", this.getPassword()); + args.put("version", AUTHENTICATION_VERSION); + + String response; + try { + response = this.getAuthenticationService().performPostRequest(AUTHENTICATION_URL, HttpAuthenticationService.buildQuery(args), "application/x-www-form-urlencoded").trim(); + } catch (IOException exception) { + throw new AuthenticationException("Authentication server is not responding", exception); + } + + String[] split = response.split(":"); + if (split.length == 5) { + String profileId = split[RESPONSE_PART_PROFILE_ID]; + String profileName = split[RESPONSE_PART_PROFILE_NAME]; + String sessionToken = split[RESPONSE_PART_SESSION_TOKEN]; + if (!StringUtils.isBlank(profileId) && !StringUtils.isBlank(profileName) && !StringUtils.isBlank(sessionToken)) { + this.setSelectedProfile(new GameProfile(UUIDTypeAdapter.fromString(profileId), profileName)); + this.sessionToken = sessionToken; + this.setUserType(UserType.LEGACY); + } else { + throw new AuthenticationException("Unknown response from authentication server: " + response); + } + } else { + throw new InvalidCredentialsException(response); + } + } + } + + public void logOut() { + super.logOut(); + this.sessionToken = null; + } + + public boolean canPlayOnline() { + return this.isLoggedIn() && this.getSelectedProfile() != null && this.getAuthenticatedToken() != null; + } + + public GameProfile[] getAvailableProfiles() { + return this.getSelectedProfile() != null ? new GameProfile[] { + this.getSelectedProfile() + } : new GameProfile[0]; + } + + public void selectGameProfile(GameProfile profile) throws AuthenticationException { + throw new UnsupportedOperationException("Game profiles cannot be changed in the legacy authentication service"); + } + + public String getAuthenticatedToken() { + return this.sessionToken; + } + + public String getUserID() { + return this.getUsername(); + } + + public LegacyAuthenticationService getAuthenticationService() { + return (LegacyAuthenticationService) super.getAuthenticationService(); + } +} \ No newline at end of file diff --git a/src/main/java/com/mojang/authlib/minecraft/BaseMinecraftSessionService.java b/src/main/java/com/mojang/authlib/minecraft/BaseMinecraftSessionService.java new file mode 100644 index 0000000..f187bee --- /dev/null +++ b/src/main/java/com/mojang/authlib/minecraft/BaseMinecraftSessionService.java @@ -0,0 +1,15 @@ +package com.mojang.authlib.minecraft; + +import com.mojang.authlib.AuthenticationService; + +public abstract class BaseMinecraftSessionService implements MinecraftSessionService { + private final AuthenticationService authenticationService; + + protected BaseMinecraftSessionService(AuthenticationService authenticationService) { + this.authenticationService = authenticationService; + } + + public AuthenticationService getAuthenticationService() { + return this.authenticationService; + } +} \ No newline at end of file diff --git a/src/main/java/com/mojang/authlib/minecraft/HttpMinecraftSessionService.java b/src/main/java/com/mojang/authlib/minecraft/HttpMinecraftSessionService.java new file mode 100644 index 0000000..83a0f1d --- /dev/null +++ b/src/main/java/com/mojang/authlib/minecraft/HttpMinecraftSessionService.java @@ -0,0 +1,13 @@ +package com.mojang.authlib.minecraft; + +import com.mojang.authlib.HttpAuthenticationService; + +public abstract class HttpMinecraftSessionService extends BaseMinecraftSessionService { + protected HttpMinecraftSessionService(HttpAuthenticationService authenticationService) { + super(authenticationService); + } + + public HttpAuthenticationService getAuthenticationService() { + return (HttpAuthenticationService) super.getAuthenticationService(); + } +} \ No newline at end of file diff --git a/src/main/java/com/mojang/authlib/minecraft/InsecureTextureException.java b/src/main/java/com/mojang/authlib/minecraft/InsecureTextureException.java new file mode 100644 index 0000000..09f1710 --- /dev/null +++ b/src/main/java/com/mojang/authlib/minecraft/InsecureTextureException.java @@ -0,0 +1,48 @@ +package com.mojang.authlib.minecraft; + +import com.mojang.authlib.GameProfile; +import java.util.Calendar; +import java.util.Date; +import java.util.UUID; + +public class InsecureTextureException extends RuntimeException { + private static final long serialVersionUID = 8583839911134111760L; + + public InsecureTextureException(String message) { + super(message); + } + + public static class MissingTextureException extends InsecureTextureException { + private static final long serialVersionUID = 923263531191208887L; + + public MissingTextureException() { + super("No texture information found"); + } + } + + public static class OutdatedTextureException extends InsecureTextureException { + private static final long serialVersionUID = -599847585679123867L; + private final Date validFrom; + private final Calendar limit; + + public OutdatedTextureException(Date validFrom, Calendar limit) { + super("Decrypted textures payload is too old (" + validFrom + ", but we need it to be at least " + limit + ")"); + this.validFrom = validFrom; + this.limit = limit; + } + } + + public static class WrongTextureOwnerException extends InsecureTextureException { + private static final long serialVersionUID = -5004489121188040013L; + private final GameProfile expected; + private final UUID resultId; + private final String resultName; + + public WrongTextureOwnerException(GameProfile expected, UUID resultId, String resultName) { + super("Decrypted textures payload was for another user (expected " + expected.getId() + "/" + expected.getName() + " but was for " + resultId + "/" + resultName + ")"); + this.expected = expected; + this.resultId = resultId; + this.resultName = resultName; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/mojang/authlib/minecraft/MinecraftProfileTexture.java b/src/main/java/com/mojang/authlib/minecraft/MinecraftProfileTexture.java new file mode 100644 index 0000000..b452153 --- /dev/null +++ b/src/main/java/com/mojang/authlib/minecraft/MinecraftProfileTexture.java @@ -0,0 +1,38 @@ +package com.mojang.authlib.minecraft; + +import java.util.Map; +import javax.annotation.Nullable; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang3.builder.ToStringBuilder; + +public class MinecraftProfileTexture { + private final String url; + private final Map metadata; + + public MinecraftProfileTexture(String url, Map metadata) { + this.url = url; + this.metadata = metadata; + } + + public String getUrl() { + return this.url; + } + + @Nullable + public String getMetadata(String key) { + return this.metadata == null ? null : (String) this.metadata.get(key); + } + + public String getHash() { + return FilenameUtils.getBaseName(this.url); + } + + public String toString() { + return (new ToStringBuilder(this)).append("url", this.url).append("hash", this.getHash()).toString(); + } + + public static enum Type { + SKIN, + CAPE; + } +} \ No newline at end of file diff --git a/src/main/java/com/mojang/authlib/minecraft/MinecraftSessionService.java b/src/main/java/com/mojang/authlib/minecraft/MinecraftSessionService.java new file mode 100644 index 0000000..012f28f --- /dev/null +++ b/src/main/java/com/mojang/authlib/minecraft/MinecraftSessionService.java @@ -0,0 +1,16 @@ +package com.mojang.authlib.minecraft; + +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.exceptions.AuthenticationException; +import com.mojang.authlib.exceptions.AuthenticationUnavailableException; +import java.util.Map; + +public interface MinecraftSessionService { + void joinServer(GameProfile profile, String authenticationToken, String serverId) throws AuthenticationException; + + GameProfile hasJoinedServer(GameProfile profile, String serverId) throws AuthenticationUnavailableException; + + Map getTextures(GameProfile profile, boolean requireSecure); + + GameProfile fillProfileProperties(GameProfile profile, boolean requireSecure); +} \ No newline at end of file diff --git a/src/main/java/com/mojang/authlib/properties/Property.java b/src/main/java/com/mojang/authlib/properties/Property.java new file mode 100644 index 0000000..12f53c2 --- /dev/null +++ b/src/main/java/com/mojang/authlib/properties/Property.java @@ -0,0 +1,57 @@ +package com.mojang.authlib.properties; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import org.apache.commons.codec.binary.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, (String) 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.decodeBase64(this.signature)); + } catch (NoSuchAlgorithmException exception) { + exception.printStackTrace(); + } catch (InvalidKeyException exception) { + exception.printStackTrace(); + } catch (SignatureException exception) { + exception.printStackTrace(); + } + + return false; + } +} \ No newline at end of file diff --git a/src/main/java/com/mojang/authlib/properties/PropertyMap.java b/src/main/java/com/mojang/authlib/properties/PropertyMap.java new file mode 100644 index 0000000..58da5be --- /dev/null +++ b/src/main/java/com/mojang/authlib/properties/PropertyMap.java @@ -0,0 +1,75 @@ +package com.mojang.authlib.properties; + +import com.google.common.collect.ForwardingMultimap; +import com.google.common.collect.LinkedHashMultimap; +import com.google.common.collect.Multimap; +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.Map; + +public class PropertyMap extends ForwardingMultimap { + private final Multimap properties = LinkedHashMultimap.create(); + + protected Multimap delegate() { + return this.properties; + } + + public static class Serializer implements JsonSerializer , + JsonDeserializer { + public PropertyMap deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + PropertyMap result = new PropertyMap(); + if (json instanceof JsonObject) { + JsonObject object = (JsonObject) json; + + for (Map.Entry entry : object.entrySet()) { + 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; + } + + 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/src/main/java/com/mojang/authlib/yggdrasil/ProfileIncompleteException.java b/src/main/java/com/mojang/authlib/yggdrasil/ProfileIncompleteException.java new file mode 100644 index 0000000..4fcf77f --- /dev/null +++ b/src/main/java/com/mojang/authlib/yggdrasil/ProfileIncompleteException.java @@ -0,0 +1,20 @@ +package com.mojang.authlib.yggdrasil; + +public class ProfileIncompleteException extends RuntimeException { + + private static final long serialVersionUID = -3427258556051913114L; + + public ProfileIncompleteException() {} + + public ProfileIncompleteException(String message) { + super(message); + } + + public ProfileIncompleteException(String message, Throwable cause) { + super(message, cause); + } + + public ProfileIncompleteException(Throwable cause) { + super(cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/mojang/authlib/yggdrasil/ProfileNotFoundException.java b/src/main/java/com/mojang/authlib/yggdrasil/ProfileNotFoundException.java new file mode 100644 index 0000000..aa0b139 --- /dev/null +++ b/src/main/java/com/mojang/authlib/yggdrasil/ProfileNotFoundException.java @@ -0,0 +1,19 @@ +package com.mojang.authlib.yggdrasil; + +public class ProfileNotFoundException extends RuntimeException { + private static final long serialVersionUID = -4170318180689190630L; + + public ProfileNotFoundException() {} + + public ProfileNotFoundException(String message) { + super(message); + } + + public ProfileNotFoundException(String message, Throwable cause) { + super(message, cause); + } + + public ProfileNotFoundException(Throwable cause) { + super(cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/mojang/authlib/yggdrasil/YggdrasilAuthenticationService.java b/src/main/java/com/mojang/authlib/yggdrasil/YggdrasilAuthenticationService.java new file mode 100644 index 0000000..45365bb --- /dev/null +++ b/src/main/java/com/mojang/authlib/yggdrasil/YggdrasilAuthenticationService.java @@ -0,0 +1,113 @@ +package com.mojang.authlib.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 com.mojang.authlib.Agent; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.GameProfileRepository; +import com.mojang.authlib.HttpAuthenticationService; +import com.mojang.authlib.UserAuthentication; +import com.mojang.authlib.exceptions.AuthenticationException; +import com.mojang.authlib.exceptions.AuthenticationUnavailableException; +import com.mojang.authlib.exceptions.InvalidCredentialsException; +import com.mojang.authlib.exceptions.UserMigratedException; +import com.mojang.authlib.minecraft.MinecraftSessionService; +import com.mojang.authlib.properties.PropertyMap; +import com.mojang.authlib.yggdrasil.response.ProfileSearchResultsResponse; +import com.mojang.authlib.yggdrasil.response.Response; +import com.mojang.util.UUIDTypeAdapter; +import java.io.IOException; +import java.lang.reflect.Type; +import java.net.Proxy; +import java.net.URL; +import java.util.UUID; +import org.apache.commons.lang3.StringUtils; + +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 YggdrasilAuthenticationService.GameProfileSerializer()); + builder.registerTypeAdapter(PropertyMap.class, new PropertyMap.Serializer()); + builder.registerTypeAdapter(UUID.class, new UUIDTypeAdapter()); + builder.registerTypeAdapter(ProfileSearchResultsResponse.class, new ProfileSearchResultsResponse.Serializer()); + this.gson = builder.create(); + } + + public UserAuthentication createUserAuthentication(Agent agent) { + return new YggdrasilUserAuthentication(this, agent); + } + + public MinecraftSessionService createMinecraftSessionService() { + return new YggdrasilMinecraftSessionService(this); + } + + public GameProfileRepository createProfileRepository() { + return new YggdrasilGameProfileRepository(this); + } + + protected T makeRequest(URL url, Object input, Class classOfT) throws AuthenticationException { + try { + String jsonResult = input == null ? this.performGetRequest(url) : this.performPostRequest(url, this.gson.toJson(input), "application/json"); + T result = (T)(this.gson.fromJson(jsonResult, classOfT)); + if (result == null) { + return (T) null; + } else if (StringUtils.isNotBlank(result.getError())) { + if ("UserMigratedException".equals(result.getCause())) { + throw new UserMigratedException(result.getErrorMessage()); + } else if (result.getError().equals("ForbiddenOperationException")) { + throw new InvalidCredentialsException(result.getErrorMessage()); + } else { + throw new AuthenticationException(result.getErrorMessage()); + } + } else { + return result; + } + } catch (IOException exception) { + throw new AuthenticationUnavailableException("Cannot contact authentication server", exception); + } catch (IllegalStateException exception) { + throw new AuthenticationUnavailableException("Cannot contact authentication server", exception); + } catch (JsonParseException exception) { + throw new AuthenticationUnavailableException("Cannot contact authentication server", exception); + } + } + + public String getClientToken() { + return this.clientToken; + } + + private static class GameProfileSerializer implements JsonSerializer , JsonDeserializer { + private GameProfileSerializer() {} + + 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); + } + + 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/src/main/java/com/mojang/authlib/yggdrasil/YggdrasilGameProfileRepository.java b/src/main/java/com/mojang/authlib/yggdrasil/YggdrasilGameProfileRepository.java new file mode 100644 index 0000000..8f9dea0 --- /dev/null +++ b/src/main/java/com/mojang/authlib/yggdrasil/YggdrasilGameProfileRepository.java @@ -0,0 +1,105 @@ +package com.mojang.authlib.yggdrasil; + +import com.google.common.base.Strings; +import com.google.common.collect.Iterables; +import com.google.common.collect.Sets; +import com.mojang.authlib.Agent; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.GameProfileRepository; +import com.mojang.authlib.HttpAuthenticationService; +import com.mojang.authlib.ProfileLookupCallback; +import com.mojang.authlib.exceptions.AuthenticationException; +import com.mojang.authlib.yggdrasil.response.ProfileSearchResultsResponse; + +import fr.azures04.modun.EndpointsManager; + +import java.util.List; +import java.util.Set; +import java.util.UUID; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class YggdrasilGameProfileRepository implements GameProfileRepository { + private static final Logger LOGGER = LogManager.getLogger(); + private static final String BASE_URL = EndpointsManager.getAPI(); + private static final String SEARCH_PAGE_URL = BASE_URL + "profiles"; + private static final int ENTRIES_PER_PAGE = 2; + private static final int MAX_FAIL_COUNT = 3; + private static final int DELAY_BETWEEN_PAGES = 100; + private static final int DELAY_BETWEEN_FAILURES = 750; + private final YggdrasilAuthenticationService authenticationService; + + public YggdrasilGameProfileRepository(YggdrasilAuthenticationService authenticationService) { + this.authenticationService = authenticationService; + } + + public void findProfilesByNames(String[] names, Agent agent, ProfileLookupCallback callback) { + Set criteria = Sets.newHashSet(); + + for (String name : names) { + if (!Strings.isNullOrEmpty(name)) { + criteria.add(name.toLowerCase()); + } + } + + int page = 0; + + for (List request : Iterables.partition(criteria, ENTRIES_PER_PAGE)) { + int failCount = 0; + + while (true) { + boolean failed = false; + + try { + ProfileSearchResultsResponse response = this.authenticationService.makeRequest(HttpAuthenticationService.constantURL(SEARCH_PAGE_URL + "/" + agent.getName().toLowerCase()), request, ProfileSearchResultsResponse.class); + failCount = 0; + LOGGER.debug("Page {} returned {} results, parsing", new Object[] { + page, + response.getProfiles().length + }); + Set missing = Sets.newHashSet(request); + + for (GameProfile profile : response.getProfiles()) { + LOGGER.debug("Successfully looked up profile {}", new Object[] { + profile + }); + missing.remove(profile.getName().toLowerCase()); + callback.onProfileLookupSucceeded(profile); + } + + for (String name : missing) { + LOGGER.debug("Couldn't find profile {}", new Object[] { + name + }); + callback.onProfileLookupFailed(new GameProfile((UUID) null, name), new ProfileNotFoundException("Server did not find the requested profile")); + } + + try { + Thread.sleep(DELAY_BETWEEN_PAGES); + } catch (InterruptedException exception) {} + } catch (AuthenticationException exception) { + ++failCount; + if (failCount == MAX_FAIL_COUNT) { + for (String name : request) { + LOGGER.debug("Couldn't find profile {} because of a server error", new Object[] { + name + }); + callback.onProfileLookupFailed(new GameProfile((UUID) null, name), exception); + } + } else { + try { + Thread.sleep(DELAY_BETWEEN_FAILURES); + } catch (InterruptedException interruptedException) {} + + failed = true; + } + } + + if (!failed) { + break; + } + } + } + + } +} \ No newline at end of file diff --git a/src/main/java/com/mojang/authlib/yggdrasil/YggdrasilMinecraftSessionService.java b/src/main/java/com/mojang/authlib/yggdrasil/YggdrasilMinecraftSessionService.java new file mode 100644 index 0000000..20d24ed --- /dev/null +++ b/src/main/java/com/mojang/authlib/yggdrasil/YggdrasilMinecraftSessionService.java @@ -0,0 +1,201 @@ +package com.mojang.authlib.yggdrasil; + +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.spec.X509EncodedKeySpec; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.codec.Charsets; +import org.apache.commons.codec.binary.Base64; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.Iterables; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonParseException; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.HttpAuthenticationService; +import com.mojang.authlib.exceptions.AuthenticationException; +import com.mojang.authlib.exceptions.AuthenticationUnavailableException; +import com.mojang.authlib.minecraft.HttpMinecraftSessionService; +import com.mojang.authlib.minecraft.InsecureTextureException; +import com.mojang.authlib.minecraft.MinecraftProfileTexture; +import com.mojang.authlib.properties.Property; +import com.mojang.authlib.yggdrasil.request.JoinMinecraftServerRequest; +import com.mojang.authlib.yggdrasil.response.HasJoinedMinecraftServerResponse; +import com.mojang.authlib.yggdrasil.response.MinecraftProfilePropertiesResponse; +import com.mojang.authlib.yggdrasil.response.MinecraftTexturesPayload; +import com.mojang.authlib.yggdrasil.response.Response; +import com.mojang.util.UUIDTypeAdapter; + +import fr.azures04.modun.EndpointsManager; +import fr.azures04.modun.YggdrasilSessionPublicKeyManager; + +public class YggdrasilMinecraftSessionService extends HttpMinecraftSessionService { + private static final String[] WHITELISTED_DOMAINS = new String[] { + ".minecraft.net", + ".mojang.com", + EndpointsManager.getWhitelistedDomain() + }; + private static final Logger LOGGER = LogManager.getLogger(); + private static final String BASE_URL = EndpointsManager.getSessionServer() + "session/minecraft/"; + private static final URL JOIN_URL = HttpAuthenticationService.constantURL(BASE_URL + "join"); + private static final URL CHECK_URL = HttpAuthenticationService.constantURL(BASE_URL + "hasJoined"); + + private final PublicKey publicKey; + private final Gson gson = (new GsonBuilder()).registerTypeAdapter(UUID.class, new UUIDTypeAdapter()).create(); + private final LoadingCache insecureProfiles = CacheBuilder.newBuilder().expireAfterWrite(6L, TimeUnit.HOURS).build(new CacheLoader () { + public GameProfile load(GameProfile key) throws Exception { + return YggdrasilMinecraftSessionService.this.fillGameProfile(key, false); + } + }); + + protected YggdrasilMinecraftSessionService(YggdrasilAuthenticationService authenticationService) { + super(authenticationService); + + try { + X509EncodedKeySpec spec = new X509EncodedKeySpec(YggdrasilSessionPublicKeyManager.getYggdrasilSessionPublicKey()); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + this.publicKey = keyFactory.generatePublic(spec); + } catch (Exception exception) { + throw new Error("Missing/invalid yggdrasil public key!"); + } + } + + public void joinServer(GameProfile profile, String authenticationToken, String serverId) throws AuthenticationException { + JoinMinecraftServerRequest request = new JoinMinecraftServerRequest(); + request.accessToken = authenticationToken; + request.selectedProfile = profile.getId(); + request.serverId = serverId; + this.getAuthenticationService().makeRequest(JOIN_URL, request, Response.class); + } + + public GameProfile hasJoinedServer(GameProfile user, String serverId) throws AuthenticationUnavailableException { + Map arguments = new HashMap <>(); + arguments.put("username", user.getName()); + arguments.put("serverId", serverId); + URL url = HttpAuthenticationService.concatenateURL(CHECK_URL, HttpAuthenticationService.buildQuery(arguments)); + + try { + HasJoinedMinecraftServerResponse response = this.getAuthenticationService().makeRequest(url, (Object) null, HasJoinedMinecraftServerResponse.class); + if (response != null && response.getId() != null) { + GameProfile result = new GameProfile(response.getId(), user.getName()); + if (response.getProperties() != null) { + result.getProperties().putAll(response.getProperties()); + } + + return result; + } else { + return null; + } + } catch (AuthenticationUnavailableException exception) { + throw exception; + } catch (AuthenticationException exception) { + return null; + } + } + + public MapgetTextures(GameProfile profile, boolean requireSecure) { + Property textureProperty = (Property) Iterables.getFirst(profile.getProperties().get("textures"), (Object) null); + if (textureProperty == null) { + return new HashMap <>(); + } else { + if (requireSecure) { + if (!textureProperty.hasSignature()) { + LOGGER.error("Signature is missing from textures payload"); + throw new InsecureTextureException("Signature is missing from textures payload"); + } + + if (!textureProperty.isSignatureValid(this.publicKey)) { + LOGGER.error("Textures payload has been tampered with (signature invalid)"); + throw new InsecureTextureException("Textures payload has been tampered with (signature invalid)"); + } + } + + MinecraftTexturesPayload result; + try { + String json = new String(Base64.decodeBase64(textureProperty.getValue()), Charsets.UTF_8); + result = (MinecraftTexturesPayload) this.gson.fromJson(json, MinecraftTexturesPayload.class); + } catch (JsonParseException exception) { + LOGGER.error("Could not decode textures payload", exception); + return new HashMap <>(); + } + + if (result.getTextures() == null) { + return new HashMap <>(); + } else { + for (Map.Entry entry : result.getTextures().entrySet()) { + if (!isWhitelistedDomain(((MinecraftProfileTexture) entry.getValue()).getUrl())) { + LOGGER.error("Textures payload has been tampered with (non-whitelisted domain)"); + return new HashMap <>(); + } + } + + return result.getTextures(); + } + } + } + + public GameProfile fillProfileProperties(GameProfile profile, boolean requireSecure) { + if (profile.getId() == null) { + return profile; + } else { + return !requireSecure ? (GameProfile) this.insecureProfiles.getUnchecked(profile) : this.fillGameProfile(profile, true); + } + } + + protected GameProfile fillGameProfile(GameProfile profile, boolean requireSecure) { + try { + URL url = HttpAuthenticationService.constantURL(BASE_URL + "profile/" + UUIDTypeAdapter.fromUUID(profile.getId())); + url = HttpAuthenticationService.concatenateURL(url, "unsigned=" + !requireSecure); + MinecraftProfilePropertiesResponse response = this.getAuthenticationService().makeRequest(url, (Object) null, MinecraftProfilePropertiesResponse.class); + if (response == null) { + LOGGER.debug("Couldn't fetch profile properties for " + profile + " as the profile does not exist"); + return profile; + } else { + GameProfile result = new GameProfile(response.getId(), response.getName()); + result.getProperties().putAll(response.getProperties()); + profile.getProperties().putAll(response.getProperties()); + LOGGER.debug("Successfully fetched profile properties for " + profile); + return result; + } + } catch (AuthenticationException exception) { + LOGGER.warn("Couldn't look up profile properties for " + profile, exception); + return profile; + } + } + + public YggdrasilAuthenticationService getAuthenticationService() { + return (YggdrasilAuthenticationService) super.getAuthenticationService(); + } + + private static boolean isWhitelistedDomain(String url) { + URI uri = null; + + try { + uri = new URI(url); + } catch (URISyntaxException exception) { + throw new IllegalArgumentException("Invalid URL '" + url + "'"); + } + + String domain = uri.getHost(); + + for (int i = 0; i credentials) { + super.loadFromStorage(credentials); + this.accessToken = String.valueOf(credentials.get(STORAGE_KEY_ACCESS_TOKEN)); + } + + public Map saveForStorage() { + Map result = super.saveForStorage(); + if (StringUtils.isNotBlank(this.getAuthenticatedToken())) { + result.put(STORAGE_KEY_ACCESS_TOKEN, this.getAuthenticatedToken()); + } + + return result; + } + + /** @deprecated */ + @Deprecated + public String getSessionToken() { + return this.isLoggedIn() && this.getSelectedProfile() != null && this.canPlayOnline() ? String.format("token:%s:%s", this.getAuthenticatedToken(), this.getSelectedProfile().getId()) : null; + } + + public String getAuthenticatedToken() { + return this.accessToken; + } + + public Agent getAgent() { + return this.agent; + } + + public String toString() { + return "YggdrasilAuthenticationService{agent=" + this.agent + ", profiles=" + Arrays.toString(this.profiles) + ", selectedProfile=" + this.getSelectedProfile() + ", username='" + this.getUsername() + '\'' + ", isLoggedIn=" + this.isLoggedIn() + ", userType=" + this.getUserType() + ", canPlayOnline=" + this.canPlayOnline() + ", accessToken='" + this.accessToken + '\'' + ", clientToken='" + this.getAuthenticationService().getClientToken() + '\'' + '}'; + } + + public YggdrasilAuthenticationService getAuthenticationService() { + return (YggdrasilAuthenticationService) super.getAuthenticationService(); + } +} \ No newline at end of file diff --git a/src/main/java/com/mojang/authlib/yggdrasil/request/AuthenticationRequest.java b/src/main/java/com/mojang/authlib/yggdrasil/request/AuthenticationRequest.java new file mode 100644 index 0000000..9dcf9e5 --- /dev/null +++ b/src/main/java/com/mojang/authlib/yggdrasil/request/AuthenticationRequest.java @@ -0,0 +1,19 @@ +package com.mojang.authlib.yggdrasil.request; + +import com.mojang.authlib.Agent; +import com.mojang.authlib.yggdrasil.YggdrasilUserAuthentication; + +public class AuthenticationRequest { + private Agent agent; + private String username; + private String password; + private String clientToken; + private boolean requestUser = true; + + public AuthenticationRequest(YggdrasilUserAuthentication authenticationService, String username, String password) { + this.agent = authenticationService.getAgent(); + this.username = username; + this.clientToken = authenticationService.getAuthenticationService().getClientToken(); + this.password = password; + } +} \ No newline at end of file diff --git a/src/main/java/com/mojang/authlib/yggdrasil/request/InvalidateRequest.java b/src/main/java/com/mojang/authlib/yggdrasil/request/InvalidateRequest.java new file mode 100644 index 0000000..d85c46d --- /dev/null +++ b/src/main/java/com/mojang/authlib/yggdrasil/request/InvalidateRequest.java @@ -0,0 +1,13 @@ +package com.mojang.authlib.yggdrasil.request; + +import com.mojang.authlib.yggdrasil.YggdrasilUserAuthentication; + +public class InvalidateRequest { + private String accessToken; + private String clientToken; + + public InvalidateRequest(YggdrasilUserAuthentication authenticationService) { + this.accessToken = authenticationService.getAuthenticatedToken(); + this.clientToken = authenticationService.getAuthenticationService().getClientToken(); + } +} \ No newline at end of file diff --git a/src/main/java/com/mojang/authlib/yggdrasil/request/JoinMinecraftServerRequest.java b/src/main/java/com/mojang/authlib/yggdrasil/request/JoinMinecraftServerRequest.java new file mode 100644 index 0000000..4f3ccae --- /dev/null +++ b/src/main/java/com/mojang/authlib/yggdrasil/request/JoinMinecraftServerRequest.java @@ -0,0 +1,9 @@ +package com.mojang.authlib.yggdrasil.request; + +import java.util.UUID; + +public class JoinMinecraftServerRequest { + public String accessToken; + public UUID selectedProfile; + public String serverId; +} \ No newline at end of file diff --git a/src/main/java/com/mojang/authlib/yggdrasil/request/RefreshRequest.java b/src/main/java/com/mojang/authlib/yggdrasil/request/RefreshRequest.java new file mode 100644 index 0000000..07534b0 --- /dev/null +++ b/src/main/java/com/mojang/authlib/yggdrasil/request/RefreshRequest.java @@ -0,0 +1,21 @@ +package com.mojang.authlib.yggdrasil.request; + +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.yggdrasil.YggdrasilUserAuthentication; + +public class RefreshRequest { + private String clientToken; + private String accessToken; + private GameProfile selectedProfile; + private boolean requestUser = true; + + public RefreshRequest(YggdrasilUserAuthentication authenticationService) { + this(authenticationService, (GameProfile) null); + } + + public RefreshRequest(YggdrasilUserAuthentication authenticationService, GameProfile profile) { + this.clientToken = authenticationService.getAuthenticationService().getClientToken(); + this.accessToken = authenticationService.getAuthenticatedToken(); + this.selectedProfile = profile; + } +} \ No newline at end of file diff --git a/src/main/java/com/mojang/authlib/yggdrasil/request/ValidateRequest.java b/src/main/java/com/mojang/authlib/yggdrasil/request/ValidateRequest.java new file mode 100644 index 0000000..546db1c --- /dev/null +++ b/src/main/java/com/mojang/authlib/yggdrasil/request/ValidateRequest.java @@ -0,0 +1,13 @@ +package com.mojang.authlib.yggdrasil.request; + +import com.mojang.authlib.yggdrasil.YggdrasilUserAuthentication; + +public class ValidateRequest { + private String clientToken; + private String accessToken; + + public ValidateRequest(YggdrasilUserAuthentication authenticationService) { + this.clientToken = authenticationService.getAuthenticationService().getClientToken(); + this.accessToken = authenticationService.getAuthenticatedToken(); + } +} \ No newline at end of file diff --git a/src/main/java/com/mojang/authlib/yggdrasil/response/AuthenticationResponse.java b/src/main/java/com/mojang/authlib/yggdrasil/response/AuthenticationResponse.java new file mode 100644 index 0000000..182e547 --- /dev/null +++ b/src/main/java/com/mojang/authlib/yggdrasil/response/AuthenticationResponse.java @@ -0,0 +1,31 @@ +package com.mojang.authlib.yggdrasil.response; + +import com.mojang.authlib.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; + } +} \ No newline at end of file diff --git a/src/main/java/com/mojang/authlib/yggdrasil/response/HasJoinedMinecraftServerResponse.java b/src/main/java/com/mojang/authlib/yggdrasil/response/HasJoinedMinecraftServerResponse.java new file mode 100644 index 0000000..6aa122f --- /dev/null +++ b/src/main/java/com/mojang/authlib/yggdrasil/response/HasJoinedMinecraftServerResponse.java @@ -0,0 +1,17 @@ +package com.mojang.authlib.yggdrasil.response; + +import com.mojang.authlib.properties.PropertyMap; +import java.util.UUID; + +public class HasJoinedMinecraftServerResponse extends Response { + private UUID id; + private PropertyMap properties; + + public UUID getId() { + return this.id; + } + + public PropertyMap getProperties() { + return this.properties; + } +} \ No newline at end of file diff --git a/src/main/java/com/mojang/authlib/yggdrasil/response/MinecraftProfilePropertiesResponse.java b/src/main/java/com/mojang/authlib/yggdrasil/response/MinecraftProfilePropertiesResponse.java new file mode 100644 index 0000000..b2cbfc3 --- /dev/null +++ b/src/main/java/com/mojang/authlib/yggdrasil/response/MinecraftProfilePropertiesResponse.java @@ -0,0 +1,22 @@ +package com.mojang.authlib.yggdrasil.response; + +import com.mojang.authlib.properties.PropertyMap; +import java.util.UUID; + +public class MinecraftProfilePropertiesResponse extends Response { + private UUID id; + private String name; + private PropertyMap properties; + + public UUID getId() { + return this.id; + } + + public String getName() { + return this.name; + } + + public PropertyMap getProperties() { + return this.properties; + } +} \ No newline at end of file diff --git a/src/main/java/com/mojang/authlib/yggdrasil/response/MinecraftTexturesPayload.java b/src/main/java/com/mojang/authlib/yggdrasil/response/MinecraftTexturesPayload.java new file mode 100644 index 0000000..1683f5e --- /dev/null +++ b/src/main/java/com/mojang/authlib/yggdrasil/response/MinecraftTexturesPayload.java @@ -0,0 +1,33 @@ +package com.mojang.authlib.yggdrasil.response; + +import com.mojang.authlib.minecraft.MinecraftProfileTexture; +import java.util.Map; +import java.util.UUID; + +public class MinecraftTexturesPayload { + private long timestamp; + private UUID profileId; + private String profileName; + private boolean isPublic; + private Map textures; + + public long getTimestamp() { + return this.timestamp; + } + + public UUID getProfileId() { + return this.profileId; + } + + public String getProfileName() { + return this.profileName; + } + + public boolean isPublic() { + return this.isPublic; + } + + public Map getTextures() { + return this.textures; + } +} \ No newline at end of file diff --git a/src/main/java/com/mojang/authlib/yggdrasil/response/ProfileSearchResultsResponse.java b/src/main/java/com/mojang/authlib/yggdrasil/response/ProfileSearchResultsResponse.java new file mode 100644 index 0000000..4d431c1 --- /dev/null +++ b/src/main/java/com/mojang/authlib/yggdrasil/response/ProfileSearchResultsResponse.java @@ -0,0 +1,41 @@ +package com.mojang.authlib.yggdrasil.response; + +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.mojang.authlib.GameProfile; +import java.lang.reflect.Type; + +public class ProfileSearchResultsResponse extends Response { + private GameProfile[] profiles; + + public GameProfile[] getProfiles() { + return this.profiles; + } + + public static class Serializer implements JsonDeserializer { + public ProfileSearchResultsResponse deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + ProfileSearchResultsResponse result = new ProfileSearchResultsResponse(); + if (json instanceof JsonObject) { + JsonObject object = (JsonObject) json; + if (object.has("error")) { + result.setError(object.getAsJsonPrimitive("error").getAsString()); + } + + if (object.has("errorMessage")) { + result.setError(object.getAsJsonPrimitive("errorMessage").getAsString()); + } + + if (object.has("cause")) { + result.setError(object.getAsJsonPrimitive("cause").getAsString()); + } + } else { + result.profiles = (GameProfile[]) context.deserialize(json, GameProfile[].class); + } + + return result; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/mojang/authlib/yggdrasil/response/RefreshResponse.java b/src/main/java/com/mojang/authlib/yggdrasil/response/RefreshResponse.java new file mode 100644 index 0000000..78ba891 --- /dev/null +++ b/src/main/java/com/mojang/authlib/yggdrasil/response/RefreshResponse.java @@ -0,0 +1,31 @@ +package com.mojang.authlib.yggdrasil.response; + +import com.mojang.authlib.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; + } +} \ No newline at end of file diff --git a/src/main/java/com/mojang/authlib/yggdrasil/response/Response.java b/src/main/java/com/mojang/authlib/yggdrasil/response/Response.java new file mode 100644 index 0000000..2134832 --- /dev/null +++ b/src/main/java/com/mojang/authlib/yggdrasil/response/Response.java @@ -0,0 +1,31 @@ +package com.mojang.authlib.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; + } + + protected void setError(String error) { + this.error = error; + } + + protected void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + protected void setCause(String cause) { + this.cause = cause; + } +} \ No newline at end of file diff --git a/src/main/java/com/mojang/authlib/yggdrasil/response/User.java b/src/main/java/com/mojang/authlib/yggdrasil/response/User.java new file mode 100644 index 0000000..2a6fc0d --- /dev/null +++ b/src/main/java/com/mojang/authlib/yggdrasil/response/User.java @@ -0,0 +1,16 @@ +package com.mojang.authlib.yggdrasil.response; + +import com.mojang.authlib.properties.PropertyMap; + +public class User { + private String id; + private PropertyMap properties; + + public String getId() { + return this.id; + } + + public PropertyMap getProperties() { + return this.properties; + } +} \ No newline at end of file diff --git a/src/main/java/com/mojang/util/QueueLogAppender.java b/src/main/java/com/mojang/util/QueueLogAppender.java new file mode 100644 index 0000000..736ddad --- /dev/null +++ b/src/main/java/com/mojang/util/QueueLogAppender.java @@ -0,0 +1,87 @@ +package com.mojang.util; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import org.apache.logging.log4j.core.Filter; +import org.apache.logging.log4j.core.Layout; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginElement; +import org.apache.logging.log4j.core.config.plugins.PluginFactory; +import org.apache.logging.log4j.core.layout.PatternLayout; +import org.apache.logging.log4j.core.pattern.RegexReplacement; + +@Plugin( + name = "Queue", + category = "Core", + elementType = "appender", + printObject = true +) +public class QueueLogAppender extends AbstractAppender { + private static final int MAX_CAPACITY = 250; + private static final Map > QUEUES = new HashMap <>(); + private static final ReadWriteLock QUEUE_LOCK = new ReentrantReadWriteLock(); + private final BlockingQueue queue; + + public QueueLogAppender(String name, Filter filter, Layout layout, boolean ignoreExceptions, BlockingQueue queue) { + super(name, filter, layout, ignoreExceptions); + this.queue = queue; + } + + public void append(LogEvent event) { + if (this.queue.size() >= MAX_CAPACITY) { + this.queue.clear(); + } + + this.queue.add(this.getLayout().toSerializable(event).toString()); + } + + @SuppressWarnings("deprecation") + @PluginFactory + public static QueueLogAppender createAppender(@PluginAttribute("name") String name, @PluginAttribute("ignoreExceptions") String ignore, @PluginElement("Layout") Layout layout, @PluginElement("Filters") Filter filter, @PluginAttribute("target") String target) { + boolean ignoreExceptions = Boolean.parseBoolean(ignore); + if (name == null) { + LOGGER.error("No name provided for QueueLogAppender"); + return null; + } else { + if (target == null) { + target = name; + } + + QUEUE_LOCK.writeLock().lock(); + BlockingQueue queue = QUEUES.get(target); + if (queue == null) { + queue = new LinkedBlockingQueue <>(); + QUEUES.put(target, queue); + } + + QUEUE_LOCK.writeLock().unlock(); + if (layout == null) { + layout = PatternLayout.createLayout((String) null, null, (Configuration) null, (RegexReplacement) null, null, ignoreExceptions, ignoreExceptions, (String) null, (String) null); + } + + return new QueueLogAppender(name, filter, layout, ignoreExceptions, queue); + } + } + + public static String getNextLogEvent(String queueName) { + QUEUE_LOCK.readLock().lock(); + BlockingQueue queue = QUEUES.get(queueName); + QUEUE_LOCK.readLock().unlock(); + if (queue != null) { + try { + return queue.take(); + } catch (InterruptedException exception) {} + } + + return null; + } +} \ No newline at end of file diff --git a/src/main/java/com/mojang/util/UUIDTypeAdapter.java b/src/main/java/com/mojang/util/UUIDTypeAdapter.java new file mode 100644 index 0000000..7d44463 --- /dev/null +++ b/src/main/java/com/mojang/util/UUIDTypeAdapter.java @@ -0,0 +1,25 @@ +package com.mojang.util; + +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 { + public void write(JsonWriter out, UUID value) throws IOException { + out.value(fromUUID(value)); + } + + 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")); + } +} \ No newline at end of file diff --git a/src/main/java/fr/azures04/modun/EndpointsManager.java b/src/main/java/fr/azures04/modun/EndpointsManager.java new file mode 100644 index 0000000..13b5277 --- /dev/null +++ b/src/main/java/fr/azures04/modun/EndpointsManager.java @@ -0,0 +1,66 @@ +package fr.azures04.modun; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class EndpointsManager { + + private static final Logger LOGGER = LogManager.getLogger(); + + public static String getAuthServer() { + String property = System.getProperty("minecraft.endpoints.authserver"); + if (property != null && !property.isEmpty()) { + return (property + "/").replaceAll("//", "/"); + } else { + property = "https://authserver.mojang.com/"; + } + LOGGER.log(Level.INFO, "[Modun] Auth server endpoint : " + property); + return property; + } + + public static String getSessionServer() { + String property = System.getProperty("minecraft.endpoints.sessionserver"); + if (property != null && !property.isEmpty()) { + return (property + "/").replaceAll("(? profilePropertyKeys; + private List authenticationKeys; + + public List getProfilePropertyKeys() { + return profilePropertyKeys; + } + + public List getAuthenticationKeys() { + return authenticationKeys; + } + + public void setProfilePropertyKeys(List profilePropertyKeys) { + this.profilePropertyKeys = profilePropertyKeys; + } + + public void setAuthenticationKeys(List authenticationKeys) { + this.authenticationKeys = authenticationKeys; + } +} \ No newline at end of file diff --git a/src/main/resources/yggdrasil_session_pubkey.der b/src/main/resources/yggdrasil_session_pubkey.der new file mode 100644 index 0000000000000000000000000000000000000000..9c79a3aa4771da1f15af37a2af0898f878ad816f GIT binary patch literal 550 zcmV+>0@?jAf&wBi4F(A+hDe6@4FLfG1potr0uKN%f&vNxf&u{m%20R*skxUv>`)yD7%qARQ>bP36CtP}x@~Z9*VkPYUS6pWrZtT#q)8GCbf0*5+4Pyw;#Tq!Aq{bDO|Iw<43LnG*#4;?Z&LM71zXE@J`@_dMl`?qQ1 z@3qR;mm=XG_-{G#4vhRxmZiQslrTtB_&7&0ECnD9)gZ}j`e&Je+s