From 1023083633d10d7fc3c64c7847c392c11ceb9ff0 Mon Sep 17 00:00:00 2001 From: Phu Ngo Date: Thu, 15 Sep 2022 10:30:49 +0700 Subject: [PATCH 1/8] .gitignore --- .gitignore | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 From 5d0ea6195e5dd38e33d18eb4a1e2c3ca48d0ad04 Mon Sep 17 00:00:00 2001 From: Phu Ngo Date: Thu, 15 Sep 2022 10:48:45 +0700 Subject: [PATCH 2/8] ms auth and compat patch up to 1.19 --- .gitattributes | 5 + .gitignore | 28 +++ build.gradle | 7 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59536 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 234 ++++++++++++++++++ gradlew.bat | 89 +++++++ .../reauth/authentication/MsAuthAPI.java | 91 +++++++ .../reauth/authentication/SessionData.java | 26 ++ .../reauth/authentication/YggdrasilAPI.java | 48 ++++ .../authentication/dto/RequestObject.java | 25 ++ .../authentication/dto/ResponseObject.java | 19 ++ .../MicrosoftAuthRefreshRequest.java | 42 ++++ .../dto/microsoft/MicrosoftAuthResponse.java | 55 ++++ .../code/MicrosoftAuthCodeRequest.java | 43 ++++ .../device/MicrosoftAuthDeviceRequest.java | 39 +++ .../device/MicrosoftAuthDeviceResponse.java | 59 +++++ .../MicrosoftAuthDeviceTokenRequest.java | 41 +++ .../dto/mojang/MojangAuthRequest.java | 24 ++ .../dto/mojang/MojangAuthResponse.java | 45 ++++ .../dto/mojang/ProfileResponse.java | 41 +++ .../dto/xbox/XboxAuthResponse.java | 101 ++++++++ .../dto/xbox/XboxLiveAuthRequest.java | 49 ++++ .../dto/xbox/XboxXstsAuthRequest.java | 47 ++++ .../dto/yggdrasil/AuthenticateRequest.java | 52 ++++ .../dto/yggdrasil/AuthenticateResponse.java | 60 +++++ .../dto/yggdrasil/JoinServerRequest.java | 30 +++ .../dto/yggdrasil/JoinServerResponse.java | 31 +++ .../flows/AuthorizationCodeFlow.java | 6 + .../authentication/flows/DeviceCodeFlow.java | 10 + .../reauth/authentication/flows/Flow.java | 33 +++ .../authentication/flows/FlowCallback.java | 21 ++ .../authentication/flows/FlowStage.java | 41 +++ .../reauth/authentication/flows/Flows.java | 35 +++ .../reauth/authentication/flows/Tokens.java | 23 ++ .../authentication/flows/impl/FlowBase.java | 114 +++++++++ .../flows/impl/MicrosoftCodeFlow.java | 95 +++++++ .../flows/impl/MicrosoftDeviceFlow.java | 127 ++++++++++ .../flows/impl/MicrosoftProfileFlow.java | 117 +++++++++ .../flows/impl/MojangAuthenticationFlow.java | 68 +++++ .../flows/impl/UnknownProfileFlow.java | 34 +++ .../flows/impl/XboxAuthenticationFlow.java | 84 +++++++ .../flows/impl/util/AuthBiFunction.java | 27 ++ .../flows/impl/util/AuthFunction.java | 27 ++ .../flows/impl/util/AuthSupplier.java | 27 ++ .../flows/impl/util/Futures.java | 28 +++ .../reauth/authentication/http/HttpUtil.java | 126 ++++++++++ .../http/InvalidResponseException.java | 8 + .../reauth/authentication/http/Response.java | 41 +++ .../http/UnreachableServiceException.java | 11 + .../http/server/AuthenticationCodeServer.java | 65 +++++ .../http/server/CodeHandler.java | 95 +++++++ .../authentication/http/server/Handler.java | 40 +++ .../http/server/HttpStatus.java | 17 ++ .../http/server/PageWriter.java | 81 ++++++ .../http/server/ResourcesHandler.java | 65 +++++ .../authentication/http/server/Response.java | 44 ++++ .../reauth/configuration/ProfileBuilder.java | 38 +++ .../configuration/ProfileConstants.java | 71 ++++++ .../technicianlp/reauth/crypto/Crypto.java | 40 +++ .../reauth/crypto/CryptoException.java | 8 + .../reauth/crypto/EncryptionAutomatic.java | 112 +++++++++ .../reauth/crypto/EncryptionNone.java | 39 +++ .../reauth/crypto/PkceChallenge.java | 40 +++ .../reauth/crypto/ProfileEncryption.java | 21 ++ .../reauth/mojangfix/CertWorkaround.java | 212 ++++++++++++++++ .../reauth/mojangfix/JceWorkaround.java | 78 ++++++ .../reauth/mojangfix/MojangJavaFix.java | 34 +++ .../reauth/session/SessionChecker.java | 58 +++++ .../reauth/session/SessionStatus.java | 19 ++ .../reauth/util/ReflectionUtils.java | 104 ++++++++ .../resources/reauth/certs/amazonrootca1.pem | 20 ++ .../reauth/certs/digicertglobalrootg2.pem | 22 ++ ...crosoftrsarootcertificateauthority2017.pem | 33 +++ .../resources/resources/reauth/icon.png | Bin 0 -> 1598 bytes .../resources/resources/reauth/logo.png | Bin 0 -> 19903 bytes .../resources/resources/reauth/reauth.html | 57 +++++ .../technicianlp/reauth/ReconnectHelper.java | 72 ++++++ .../reauth/configuration/Config.java | 58 +++++ .../reauth/configuration/Profile.java | 30 +++ .../reauth/configuration/ProfileList.java | 119 +++++++++ .../reauth/gui/AbstractScreen.java | 98 ++++++++ .../technicianlp/reauth/gui/FlowScreen.java | 164 ++++++++++++ .../technicianlp/reauth/gui/MainScreen.java | 82 ++++++ .../reauth/gui/OfflineLoginScreen.java | 71 ++++++ .../technicianlp/reauth/gui/SaveButton.java | 27 ++ .../reauth/session/SessionHelper.java | 104 ++++++++ .../resources/assets/reauth/lang/en_us.json | 47 +++- 88 files changed, 4723 insertions(+), 1 deletion(-) create mode 100644 .gitattributes create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 src/common/java/technicianlp/reauth/authentication/MsAuthAPI.java create mode 100644 src/common/java/technicianlp/reauth/authentication/SessionData.java create mode 100644 src/common/java/technicianlp/reauth/authentication/YggdrasilAPI.java create mode 100644 src/common/java/technicianlp/reauth/authentication/dto/RequestObject.java create mode 100644 src/common/java/technicianlp/reauth/authentication/dto/ResponseObject.java create mode 100644 src/common/java/technicianlp/reauth/authentication/dto/microsoft/MicrosoftAuthRefreshRequest.java create mode 100644 src/common/java/technicianlp/reauth/authentication/dto/microsoft/MicrosoftAuthResponse.java create mode 100644 src/common/java/technicianlp/reauth/authentication/dto/microsoft/code/MicrosoftAuthCodeRequest.java create mode 100644 src/common/java/technicianlp/reauth/authentication/dto/microsoft/device/MicrosoftAuthDeviceRequest.java create mode 100644 src/common/java/technicianlp/reauth/authentication/dto/microsoft/device/MicrosoftAuthDeviceResponse.java create mode 100644 src/common/java/technicianlp/reauth/authentication/dto/microsoft/device/MicrosoftAuthDeviceTokenRequest.java create mode 100644 src/common/java/technicianlp/reauth/authentication/dto/mojang/MojangAuthRequest.java create mode 100644 src/common/java/technicianlp/reauth/authentication/dto/mojang/MojangAuthResponse.java create mode 100644 src/common/java/technicianlp/reauth/authentication/dto/mojang/ProfileResponse.java create mode 100644 src/common/java/technicianlp/reauth/authentication/dto/xbox/XboxAuthResponse.java create mode 100644 src/common/java/technicianlp/reauth/authentication/dto/xbox/XboxLiveAuthRequest.java create mode 100644 src/common/java/technicianlp/reauth/authentication/dto/xbox/XboxXstsAuthRequest.java create mode 100644 src/common/java/technicianlp/reauth/authentication/dto/yggdrasil/AuthenticateRequest.java create mode 100644 src/common/java/technicianlp/reauth/authentication/dto/yggdrasil/AuthenticateResponse.java create mode 100644 src/common/java/technicianlp/reauth/authentication/dto/yggdrasil/JoinServerRequest.java create mode 100644 src/common/java/technicianlp/reauth/authentication/dto/yggdrasil/JoinServerResponse.java create mode 100644 src/common/java/technicianlp/reauth/authentication/flows/AuthorizationCodeFlow.java create mode 100644 src/common/java/technicianlp/reauth/authentication/flows/DeviceCodeFlow.java create mode 100644 src/common/java/technicianlp/reauth/authentication/flows/Flow.java create mode 100644 src/common/java/technicianlp/reauth/authentication/flows/FlowCallback.java create mode 100644 src/common/java/technicianlp/reauth/authentication/flows/FlowStage.java create mode 100644 src/common/java/technicianlp/reauth/authentication/flows/Flows.java create mode 100644 src/common/java/technicianlp/reauth/authentication/flows/Tokens.java create mode 100644 src/common/java/technicianlp/reauth/authentication/flows/impl/FlowBase.java create mode 100644 src/common/java/technicianlp/reauth/authentication/flows/impl/MicrosoftCodeFlow.java create mode 100644 src/common/java/technicianlp/reauth/authentication/flows/impl/MicrosoftDeviceFlow.java create mode 100644 src/common/java/technicianlp/reauth/authentication/flows/impl/MicrosoftProfileFlow.java create mode 100644 src/common/java/technicianlp/reauth/authentication/flows/impl/MojangAuthenticationFlow.java create mode 100644 src/common/java/technicianlp/reauth/authentication/flows/impl/UnknownProfileFlow.java create mode 100644 src/common/java/technicianlp/reauth/authentication/flows/impl/XboxAuthenticationFlow.java create mode 100644 src/common/java/technicianlp/reauth/authentication/flows/impl/util/AuthBiFunction.java create mode 100644 src/common/java/technicianlp/reauth/authentication/flows/impl/util/AuthFunction.java create mode 100644 src/common/java/technicianlp/reauth/authentication/flows/impl/util/AuthSupplier.java create mode 100644 src/common/java/technicianlp/reauth/authentication/flows/impl/util/Futures.java create mode 100644 src/common/java/technicianlp/reauth/authentication/http/HttpUtil.java create mode 100644 src/common/java/technicianlp/reauth/authentication/http/InvalidResponseException.java create mode 100644 src/common/java/technicianlp/reauth/authentication/http/Response.java create mode 100644 src/common/java/technicianlp/reauth/authentication/http/UnreachableServiceException.java create mode 100644 src/common/java/technicianlp/reauth/authentication/http/server/AuthenticationCodeServer.java create mode 100644 src/common/java/technicianlp/reauth/authentication/http/server/CodeHandler.java create mode 100644 src/common/java/technicianlp/reauth/authentication/http/server/Handler.java create mode 100644 src/common/java/technicianlp/reauth/authentication/http/server/HttpStatus.java create mode 100644 src/common/java/technicianlp/reauth/authentication/http/server/PageWriter.java create mode 100644 src/common/java/technicianlp/reauth/authentication/http/server/ResourcesHandler.java create mode 100644 src/common/java/technicianlp/reauth/authentication/http/server/Response.java create mode 100644 src/common/java/technicianlp/reauth/configuration/ProfileBuilder.java create mode 100644 src/common/java/technicianlp/reauth/configuration/ProfileConstants.java create mode 100644 src/common/java/technicianlp/reauth/crypto/Crypto.java create mode 100644 src/common/java/technicianlp/reauth/crypto/CryptoException.java create mode 100644 src/common/java/technicianlp/reauth/crypto/EncryptionAutomatic.java create mode 100644 src/common/java/technicianlp/reauth/crypto/EncryptionNone.java create mode 100644 src/common/java/technicianlp/reauth/crypto/PkceChallenge.java create mode 100644 src/common/java/technicianlp/reauth/crypto/ProfileEncryption.java create mode 100644 src/common/java/technicianlp/reauth/mojangfix/CertWorkaround.java create mode 100644 src/common/java/technicianlp/reauth/mojangfix/JceWorkaround.java create mode 100644 src/common/java/technicianlp/reauth/mojangfix/MojangJavaFix.java create mode 100644 src/common/java/technicianlp/reauth/session/SessionChecker.java create mode 100644 src/common/java/technicianlp/reauth/session/SessionStatus.java create mode 100644 src/common/java/technicianlp/reauth/util/ReflectionUtils.java create mode 100644 src/common/resources/resources/reauth/certs/amazonrootca1.pem create mode 100644 src/common/resources/resources/reauth/certs/digicertglobalrootg2.pem create mode 100644 src/common/resources/resources/reauth/certs/microsoftrsarootcertificateauthority2017.pem create mode 100644 src/common/resources/resources/reauth/icon.png create mode 100644 src/common/resources/resources/reauth/logo.png create mode 100644 src/common/resources/resources/reauth/reauth.html create mode 100644 src/main/java/technicianlp/reauth/ReconnectHelper.java create mode 100644 src/main/java/technicianlp/reauth/configuration/Config.java create mode 100644 src/main/java/technicianlp/reauth/configuration/Profile.java create mode 100644 src/main/java/technicianlp/reauth/configuration/ProfileList.java create mode 100644 src/main/java/technicianlp/reauth/gui/AbstractScreen.java create mode 100644 src/main/java/technicianlp/reauth/gui/FlowScreen.java create mode 100644 src/main/java/technicianlp/reauth/gui/MainScreen.java create mode 100644 src/main/java/technicianlp/reauth/gui/OfflineLoginScreen.java create mode 100644 src/main/java/technicianlp/reauth/gui/SaveButton.java create mode 100644 src/main/java/technicianlp/reauth/session/SessionHelper.java diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f811f6a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# Disable autocrlf on generated files, they always generate with LF +# Add any extra files or paths here to make git stop saying they +# are changed when only line endings change. +src/generated/**/.cache/cache text eol=lf +src/generated/**/*.json text eol=lf diff --git a/.gitignore b/.gitignore index e69de29..6ab9de8 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1,28 @@ +# eclipse +bin +*.launch +.settings +.metadata +.classpath +.project + +# idea +out +*.ipr +*.iws +*.iml +.idea + +# gradle +build +.gradle + +# other +eclipse +run +annotations +logs +scrapped + +# Files from Forge MDK +forge*changelog.txt diff --git a/build.gradle b/build.gradle index e80d504..7b1460a 100644 --- a/build.gradle +++ b/build.gradle @@ -27,6 +27,13 @@ dependencies { // You may need to force-disable transitiveness on them. } +sourceSets { + main { + java.srcDirs += "src/common/java" + resources.srcDirs += "src/common/resources" + } +} + processResources { inputs.property "version", project.version diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..7454180f2ae8848c63b8b4dea2cb829da983f2fa GIT binary patch literal 59536 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 QuEJmfE|#`Dqx|px0L@K;Y5)KL literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..b1159fc --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..c53aefa --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${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='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# 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 ;; #( + MSYS* | 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" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@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 Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@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="-Xmx64m" "-Xms64m" + +@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 execute + +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 execute + +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 + +: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 %* + +: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/src/common/java/technicianlp/reauth/authentication/MsAuthAPI.java b/src/common/java/technicianlp/reauth/authentication/MsAuthAPI.java new file mode 100644 index 0000000..a567aa4 --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/MsAuthAPI.java @@ -0,0 +1,91 @@ +package technicianlp.reauth.authentication; + +import technicianlp.reauth.authentication.dto.microsoft.MicrosoftAuthRefreshRequest; +import technicianlp.reauth.authentication.dto.microsoft.MicrosoftAuthResponse; +import technicianlp.reauth.authentication.dto.microsoft.code.MicrosoftAuthCodeRequest; +import technicianlp.reauth.authentication.dto.microsoft.device.MicrosoftAuthDeviceRequest; +import technicianlp.reauth.authentication.dto.microsoft.device.MicrosoftAuthDeviceResponse; +import technicianlp.reauth.authentication.dto.microsoft.device.MicrosoftAuthDeviceTokenRequest; +import technicianlp.reauth.authentication.dto.mojang.MojangAuthRequest; +import technicianlp.reauth.authentication.dto.mojang.MojangAuthResponse; +import technicianlp.reauth.authentication.dto.mojang.ProfileResponse; +import technicianlp.reauth.authentication.dto.xbox.XboxAuthResponse; +import technicianlp.reauth.authentication.dto.xbox.XboxLiveAuthRequest; +import technicianlp.reauth.authentication.dto.xbox.XboxXstsAuthRequest; +import technicianlp.reauth.authentication.http.HttpUtil; +import technicianlp.reauth.authentication.http.InvalidResponseException; +import technicianlp.reauth.authentication.http.Response; +import technicianlp.reauth.authentication.http.UnreachableServiceException; + + +public final class MsAuthAPI { + + public static final String clientId = "fa861065-c46c-4ac9-a4da-59a7d40b8a72"; + + public static final int port = 52371; + public static final String redirectUri = "http://127.0.0.1:" + port; + private static final String redirectUriEncoded = "http%3A%2F%2F127%2E0%2E0%2E1%3A" + port; + + private static final String scopeBasic = "XboxLive.signin"; + private static final String scopePersist = "XboxLive.signin XboxLive.offline_access"; + private static final String scopePersistUrl = "XboxLive.signin+XboxLive.offline_access"; + + private static final String urlMicrosoftAuthorize = "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize"; + private static final String urlMicrosoftToken = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"; + private static final String urlMicrosoftDevice = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode"; + private static final String urlXboxLive = "https://user.auth.xboxlive.com/user/authenticate"; + private static final String urlXsts = "https://xsts.auth.xboxlive.com/xsts/authorize"; + private static final String urlMojangLogin = "https://api.minecraftservices.com/authentication/login_with_xbox"; + private static final String urlMojangProfile = "https://api.minecraftservices.com/minecraft/profile"; + + public static String getLoginUrl(boolean persist, String pkceChallenge) { + return urlMicrosoftAuthorize + + "?client_id=" + clientId + + "&redirect_uri=" + redirectUriEncoded + + "&scope=" + (persist ? scopePersistUrl : scopeBasic) + + "&response_type=code" + + "&response_mode=form_post" + + "&prompt=select_account" + + "&code_challenge=" + pkceChallenge + + "&code_challenge_method=S256"; + } + + public static MicrosoftAuthResponse redeemAuthorizationCode(String code, String pkceVerifier) throws UnreachableServiceException, InvalidResponseException { + MicrosoftAuthCodeRequest request = new MicrosoftAuthCodeRequest(code, pkceVerifier); + return HttpUtil.performFormRequest(urlMicrosoftToken, request); + } + + public static MicrosoftAuthDeviceResponse requestDeviceCode(boolean persist) throws UnreachableServiceException, InvalidResponseException { + MicrosoftAuthDeviceRequest request = new MicrosoftAuthDeviceRequest(persist ? scopePersist : scopeBasic); + return HttpUtil.performFormRequest(urlMicrosoftDevice, request); + } + + public static Response redeemDeviceCode(String deviceCode) throws UnreachableServiceException { + MicrosoftAuthDeviceTokenRequest request = new MicrosoftAuthDeviceTokenRequest(deviceCode); + return HttpUtil.performWrappedFormRequest(urlMicrosoftToken, request); + } + + public static MicrosoftAuthResponse redeemRefreshToken(String refreshToken) throws UnreachableServiceException, InvalidResponseException { + MicrosoftAuthRefreshRequest request = new MicrosoftAuthRefreshRequest(refreshToken); + return HttpUtil.performFormRequest(urlMicrosoftToken, request); + } + + public static XboxAuthResponse authenticateXASU(String token) throws UnreachableServiceException, InvalidResponseException { + XboxLiveAuthRequest request = new XboxLiveAuthRequest(token); + return HttpUtil.performJsonRequest(urlXboxLive, request); + } + + public static Response authenticateXSTS(String xblToken) throws UnreachableServiceException { + XboxXstsAuthRequest request = new XboxXstsAuthRequest(xblToken); + return HttpUtil.performWrappedJsonRequest(urlXsts, request); + } + + public static MojangAuthResponse authenticateMojang(String xstsToken, String uhs) throws UnreachableServiceException, InvalidResponseException { + MojangAuthRequest request = new MojangAuthRequest(xstsToken, uhs); + return HttpUtil.performJsonRequest(urlMojangLogin, request); + } + + public static ProfileResponse fetchProfile(String accessToken) throws UnreachableServiceException, InvalidResponseException { + return HttpUtil.performGetRequest(urlMojangProfile, accessToken, ProfileResponse.class); + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/SessionData.java b/src/common/java/technicianlp/reauth/authentication/SessionData.java new file mode 100644 index 0000000..d95190c --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/SessionData.java @@ -0,0 +1,26 @@ +package technicianlp.reauth.authentication; + +public final class SessionData { + + public final String username; + public final String uuid; + public final String accessToken; + public final String type; + + public SessionData(String username, String uuid, String accessToken, String type) { + this.username = username; + this.uuid = uuid; + this.accessToken = accessToken; + this.type = type; + } + + @Override + public final String toString() { + return "SessionData{" + + "username='" + this.username + '\'' + + ", uuid='" + this.uuid + '\'' + + ", accessToken='" + this.accessToken + '\'' + + ", type='" + this.type + '\'' + + '}'; + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/YggdrasilAPI.java b/src/common/java/technicianlp/reauth/authentication/YggdrasilAPI.java new file mode 100644 index 0000000..0cba051 --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/YggdrasilAPI.java @@ -0,0 +1,48 @@ +package technicianlp.reauth.authentication; + +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService; +import com.mojang.authlib.yggdrasil.YggdrasilUserAuthentication; +import technicianlp.reauth.authentication.dto.yggdrasil.AuthenticateRequest; +import technicianlp.reauth.authentication.dto.yggdrasil.AuthenticateResponse; +import technicianlp.reauth.authentication.dto.yggdrasil.JoinServerRequest; +import technicianlp.reauth.authentication.dto.yggdrasil.JoinServerResponse; +import technicianlp.reauth.authentication.http.HttpUtil; +import technicianlp.reauth.authentication.http.InvalidResponseException; +import technicianlp.reauth.authentication.http.Response; +import technicianlp.reauth.authentication.http.UnreachableServiceException; +import technicianlp.reauth.crypto.Crypto; + +import java.math.BigInteger; +import java.util.UUID; + +/** + * cut down reimplementation version of {@link YggdrasilUserAuthentication} + */ +public final class YggdrasilAPI { + + private static final String urlAuthenticate = "https://authserver.mojang.com/authenticate"; + private static final String urlJoin = "https://sessionserver.mojang.com/session/minecraft/join"; + + /** + * reimplementation of {@link YggdrasilUserAuthentication#logInWithPassword()} + */ + public static SessionData login(String username, String password) throws UnreachableServiceException, InvalidResponseException { + AuthenticateRequest request = new AuthenticateRequest(username, password, UUID.randomUUID().toString()); + AuthenticateResponse response = HttpUtil.performJsonRequest(urlAuthenticate, request); + return response != null ? response.getSession() : null; + } + + /** + * checks validity of accessToken by invoking the joinServer endpoint + *

+ * reimplementation of {@link YggdrasilMinecraftSessionService#joinServer(GameProfile, String, String)} + * Server hash is generated like during standard login sequence + */ + public static boolean validate(String accessToken, String uuid) throws UnreachableServiceException { + String hash = new BigInteger(Crypto.randomBytes(20)).toString(16); + JoinServerRequest request = new JoinServerRequest(accessToken, uuid, hash); + Response response = HttpUtil.performWrappedJsonRequest(urlJoin, request); + return response.isValid(); + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/dto/RequestObject.java b/src/common/java/technicianlp/reauth/authentication/dto/RequestObject.java new file mode 100644 index 0000000..623ebb0 --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/dto/RequestObject.java @@ -0,0 +1,25 @@ +package technicianlp.reauth.authentication.dto; + +import java.util.Map; + +/** + * Base Interface for request payloads + */ +public interface RequestObject { + + Class getResponseClass(); + + /** + * Interface for form request payloads + */ + interface Form extends RequestObject { + + Map getFields(); + } + + /** + * Interface for json request payloads + */ + interface JSON extends RequestObject { + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/dto/ResponseObject.java b/src/common/java/technicianlp/reauth/authentication/dto/ResponseObject.java new file mode 100644 index 0000000..dd74a12 --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/dto/ResponseObject.java @@ -0,0 +1,19 @@ +package technicianlp.reauth.authentication.dto; + +import org.jetbrains.annotations.Nullable; + +/** + * Interface for response payloads + */ +public interface ResponseObject { + + /** + * checks whether the request was successful and all required fields have been sent + */ + boolean isValid(); + + /** + * returns the errormessage returned by the service for a failed request + */ + @Nullable String getError(); +} diff --git a/src/common/java/technicianlp/reauth/authentication/dto/microsoft/MicrosoftAuthRefreshRequest.java b/src/common/java/technicianlp/reauth/authentication/dto/microsoft/MicrosoftAuthRefreshRequest.java new file mode 100644 index 0000000..005d487 --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/dto/microsoft/MicrosoftAuthRefreshRequest.java @@ -0,0 +1,42 @@ +package technicianlp.reauth.authentication.dto.microsoft; + +import com.google.common.collect.ImmutableMap; +import technicianlp.reauth.authentication.MsAuthAPI; +import technicianlp.reauth.authentication.dto.RequestObject; + +import java.util.HashMap; +import java.util.Map; + +/** + * Request Payload for the /token Endpoint of the Microsoft Identity Platform + * Payload is used for refreshing the oauth tokens. + * + * @see Microsoft Authentication Scheme on wiki.vg + * @see Microsoft Auth Code Flow + * @see Microsoft Device Code Flow + */ +public final class MicrosoftAuthRefreshRequest implements RequestObject.Form { + + private final Map fields; + + public MicrosoftAuthRefreshRequest(String refreshToken) { + Map map = new HashMap<>(); + + map.put("client_id", MsAuthAPI.clientId); + map.put("redirect_uri", MsAuthAPI.redirectUri); + map.put("grant_type", "refresh_token"); + map.put("refresh_token", refreshToken); + + this.fields = ImmutableMap.copyOf(map); + } + + @Override + public final Class getResponseClass() { + return MicrosoftAuthResponse.class; + } + + @Override + public final Map getFields() { + return this.fields; + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/dto/microsoft/MicrosoftAuthResponse.java b/src/common/java/technicianlp/reauth/authentication/dto/microsoft/MicrosoftAuthResponse.java new file mode 100644 index 0000000..c13ae72 --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/dto/microsoft/MicrosoftAuthResponse.java @@ -0,0 +1,55 @@ +package technicianlp.reauth.authentication.dto.microsoft; + +import com.google.gson.annotations.SerializedName; +import org.jetbrains.annotations.Nullable; +import technicianlp.reauth.authentication.dto.ResponseObject; + +/** + * Response Payload for the /token Endpoint of the Microsoft Identity Platform + *
+ * Only relevant fields are deserialized + * + * @see Microsoft Authentication Scheme on wiki.vg + * @see Microsoft Auth Code Flow + * @see Microsoft Device Code Flow + */ +public final class MicrosoftAuthResponse implements ResponseObject { + + // Normal Handling + @SerializedName("expires_in") + public final String expires_in; + @SerializedName("access_token") + public final String accessToken; + @SerializedName("refresh_token") + public final @Nullable String refreshToken; + + // Error Handling + @SerializedName("error") + public final @Nullable String error; + + private MicrosoftAuthResponse() { + this.expires_in = null; + this.accessToken = null; + this.refreshToken = null; + + this.error = null; + } + + @Override + public final boolean isValid() { + return this.error == null && this.expires_in != null && this.accessToken != null; + } + + @Override + public final @Nullable String getError() { + return this.error; + } + + public final String getAccessToken() { + return this.accessToken; + } + + public final String getRefreshToken() { + return this.refreshToken; + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/dto/microsoft/code/MicrosoftAuthCodeRequest.java b/src/common/java/technicianlp/reauth/authentication/dto/microsoft/code/MicrosoftAuthCodeRequest.java new file mode 100644 index 0000000..8f6cbb9 --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/dto/microsoft/code/MicrosoftAuthCodeRequest.java @@ -0,0 +1,43 @@ +package technicianlp.reauth.authentication.dto.microsoft.code; + +import com.google.common.collect.ImmutableMap; +import technicianlp.reauth.authentication.MsAuthAPI; +import technicianlp.reauth.authentication.dto.RequestObject; +import technicianlp.reauth.authentication.dto.microsoft.MicrosoftAuthResponse; + +import java.util.HashMap; +import java.util.Map; + +/** + * Request Payload for the /token Endpoint of the Microsoft Identity Platform. + * Payload is used for redeeming the code received in the auth code grant flow. + * + * @see Microsoft Authentication Scheme on wiki.vg + * @see Microsoft Auth Code Flow + */ +public final class MicrosoftAuthCodeRequest implements RequestObject.Form { + + private final Map fields; + + public MicrosoftAuthCodeRequest(String authCode, String pkceVerifier) { + Map map = new HashMap<>(); + + map.put("client_id", MsAuthAPI.clientId); + map.put("redirect_uri", MsAuthAPI.redirectUri); + map.put("grant_type", "authorization_code"); + map.put("code", authCode); + map.put("code_verifier", pkceVerifier); + + this.fields = ImmutableMap.copyOf(map); + } + + @Override + public final Class getResponseClass() { + return MicrosoftAuthResponse.class; + } + + @Override + public final Map getFields() { + return this.fields; + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/dto/microsoft/device/MicrosoftAuthDeviceRequest.java b/src/common/java/technicianlp/reauth/authentication/dto/microsoft/device/MicrosoftAuthDeviceRequest.java new file mode 100644 index 0000000..1fe75cc --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/dto/microsoft/device/MicrosoftAuthDeviceRequest.java @@ -0,0 +1,39 @@ +package technicianlp.reauth.authentication.dto.microsoft.device; + +import com.google.common.collect.ImmutableMap; +import technicianlp.reauth.authentication.MsAuthAPI; +import technicianlp.reauth.authentication.dto.RequestObject; + +import java.util.HashMap; +import java.util.Map; + +/** + * Request Payload for the /devicecode Endpoint of the Microsoft Identity Platform. + * Payload is used for requesting a devicecode for the device code flow. + * + * @see Microsoft Authentication Scheme on wiki.vg + * @see Microsoft Device Code Flow + */ +public final class MicrosoftAuthDeviceRequest implements RequestObject.Form { + + private final Map fields; + + public MicrosoftAuthDeviceRequest(String scope) { + Map map = new HashMap<>(); + + map.put("client_id", MsAuthAPI.clientId); + map.put("scope", scope); + + this.fields = ImmutableMap.copyOf(map); + } + + @Override + public final Class getResponseClass() { + return MicrosoftAuthDeviceResponse.class; + } + + @Override + public final Map getFields() { + return this.fields; + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/dto/microsoft/device/MicrosoftAuthDeviceResponse.java b/src/common/java/technicianlp/reauth/authentication/dto/microsoft/device/MicrosoftAuthDeviceResponse.java new file mode 100644 index 0000000..76544f0 --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/dto/microsoft/device/MicrosoftAuthDeviceResponse.java @@ -0,0 +1,59 @@ +package technicianlp.reauth.authentication.dto.microsoft.device; + +import com.google.gson.annotations.SerializedName; +import org.jetbrains.annotations.Nullable; +import technicianlp.reauth.authentication.dto.ResponseObject; + +/** + * Response Payload for the /devicecode Endpoint of the Microsoft Identity Platform + *
+ * Only relevant fields are deserialized + * + * @see Microsoft Authentication Scheme on wiki.vg + * @see Microsoft Device Code Flow + */ +public final class MicrosoftAuthDeviceResponse implements ResponseObject { + + // Normal Handling + @SerializedName("device_code") + public final String deviceCode; + @SerializedName("user_code") + public final String userCode; + @SerializedName("verification_uri") + public final String verificationUri; + + @SerializedName("interval") + public final int interval; + + // Error Handling + @SerializedName("error") + public final @Nullable String error; + + private MicrosoftAuthDeviceResponse() { + this.deviceCode = null; + this.userCode = null; + this.verificationUri = null; + + this.interval = 5; + + this.error = null; + } + + @Override + public final boolean isValid() { + return this.error == null && this.deviceCode != null && this.userCode != null && this.verificationUri != null; + } + + @Override + public final @Nullable String getError() { + return this.error; + } + + public final String getUserCode() { + return this.userCode; + } + + public final String getVerificationUri() { + return this.verificationUri; + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/dto/microsoft/device/MicrosoftAuthDeviceTokenRequest.java b/src/common/java/technicianlp/reauth/authentication/dto/microsoft/device/MicrosoftAuthDeviceTokenRequest.java new file mode 100644 index 0000000..d825387 --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/dto/microsoft/device/MicrosoftAuthDeviceTokenRequest.java @@ -0,0 +1,41 @@ +package technicianlp.reauth.authentication.dto.microsoft.device; + +import com.google.common.collect.ImmutableMap; +import technicianlp.reauth.authentication.MsAuthAPI; +import technicianlp.reauth.authentication.dto.RequestObject; +import technicianlp.reauth.authentication.dto.microsoft.MicrosoftAuthResponse; + +import java.util.HashMap; +import java.util.Map; + +/** + * Request Payload for the /token Endpoint of the Microsoft Identity Platform. + * Payload is used for polling the endpoint as described in the device code flow. + * + * @see Microsoft Authentication Scheme on wiki.vg + * @see Microsoft Device Code Flow + */ +public final class MicrosoftAuthDeviceTokenRequest implements RequestObject.Form { + + private final Map fields; + + public MicrosoftAuthDeviceTokenRequest(String deviceCode) { + Map map = new HashMap<>(); + + map.put("client_id", MsAuthAPI.clientId); + map.put("device_code", deviceCode); + map.put("grant_type", "urn:ietf:params:oauth:grant-type:device_code"); + + this.fields = ImmutableMap.copyOf(map); + } + + @Override + public final Class getResponseClass() { + return MicrosoftAuthResponse.class; + } + + @Override + public final Map getFields() { + return this.fields; + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/dto/mojang/MojangAuthRequest.java b/src/common/java/technicianlp/reauth/authentication/dto/mojang/MojangAuthRequest.java new file mode 100644 index 0000000..97a30f5 --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/dto/mojang/MojangAuthRequest.java @@ -0,0 +1,24 @@ +package technicianlp.reauth.authentication.dto.mojang; + +import com.google.gson.annotations.SerializedName; +import technicianlp.reauth.authentication.dto.RequestObject; + +/** + * Request Payload for the /authentication/login_with_xbox Endpoint of the MinecraftServices API + * + * @see https://wiki.vg/Microsoft_Authentication_Scheme + */ +public final class MojangAuthRequest implements RequestObject.JSON { + + @SerializedName("identityToken") + private final String token; + + public MojangAuthRequest(String xToken, String userHash) { + this.token = "XBL3.0 x=" + userHash + ";" + xToken; + } + + @Override + public final Class getResponseClass() { + return MojangAuthResponse.class; + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/dto/mojang/MojangAuthResponse.java b/src/common/java/technicianlp/reauth/authentication/dto/mojang/MojangAuthResponse.java new file mode 100644 index 0000000..d23ff71 --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/dto/mojang/MojangAuthResponse.java @@ -0,0 +1,45 @@ +package technicianlp.reauth.authentication.dto.mojang; + +import com.google.gson.annotations.SerializedName; +import org.jetbrains.annotations.Nullable; +import technicianlp.reauth.authentication.dto.ResponseObject; + +/** + * Response Payload for the /authentication/login_with_xbox Endpoint of the MinecraftServices API + *
+ * Only relevant fields are deserialized + * + * @see https://wiki.vg/Microsoft_Authentication_Scheme + */ +public final class MojangAuthResponse implements ResponseObject { + + // Normal Handling + @SerializedName("access_token") + public final String token; + @SerializedName("expires_in") + public final String expiry; + + // Error Handling + @SerializedName("error") + public final @Nullable String error; + + private MojangAuthResponse() { + this.token = null; + this.expiry = null; + this.error = null; + } + + @Override + public final boolean isValid() { + return this.error == null && this.token != null && this.expiry != null; + } + + @Override + public final @Nullable String getError() { + return this.error; + } + + public final String getToken() { + return this.token; + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/dto/mojang/ProfileResponse.java b/src/common/java/technicianlp/reauth/authentication/dto/mojang/ProfileResponse.java new file mode 100644 index 0000000..e493f54 --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/dto/mojang/ProfileResponse.java @@ -0,0 +1,41 @@ +package technicianlp.reauth.authentication.dto.mojang; + +import com.google.gson.annotations.SerializedName; +import org.jetbrains.annotations.Nullable; +import technicianlp.reauth.authentication.dto.ResponseObject; + +/** + * Response Payload for the /minecraft/profile Endpoint of the MinecraftServices API. + *
+ * Only relevant fields are deserialized + * + * @see https://wiki.vg/Microsoft_Authentication_Scheme + */ +public final class ProfileResponse implements ResponseObject { + + // Normal Handling + @SerializedName("id") + public final String uuid; + @SerializedName("name") + public final String name; + + // Error Handling + @SerializedName("error") + public final @Nullable String error; + + private ProfileResponse() { + this.uuid = null; + this.name = null; + this.error = null; + } + + @Override + public final boolean isValid() { + return this.error == null && this.uuid != null && this.name != null; + } + + @Override + public final @Nullable String getError() { + return this.error; + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/dto/xbox/XboxAuthResponse.java b/src/common/java/technicianlp/reauth/authentication/dto/xbox/XboxAuthResponse.java new file mode 100644 index 0000000..92186cd --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/dto/xbox/XboxAuthResponse.java @@ -0,0 +1,101 @@ +package technicianlp.reauth.authentication.dto.xbox; + +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.JsonPrimitive; +import org.jetbrains.annotations.Nullable; +import technicianlp.reauth.authentication.dto.ResponseObject; + +import java.lang.reflect.Type; + +/** + * Response Payload for: + * the /user/authenticate Endpoint of the Xbox Live user service
+ * the /xsts/authorize Endpoint of the Xbox Live xsts service
+ *
+ * Only relevant fields are deserialized + * + * @see https://wiki.vg/Microsoft_Authentication_Scheme + */ +public final class XboxAuthResponse implements ResponseObject { + + // Normal Handling + public final String validUntil; + public final String token; + public final String userHash; + + // Error Handling + public final @Nullable String error; + + private XboxAuthResponse(String validUntil, String token, String userHash, String error) { + this.validUntil = validUntil; + this.token = token; + this.userHash = userHash; + this.error = error; + } + + @Override + public final boolean isValid() { + return this.error == null && this.validUntil != null && this.token != null && this.userHash != null; + } + + @Override + public final @Nullable String getError() { + return this.error; + } + + public final String getToken() { + return this.token; + } + + /** + * Custom deserializer that searches the DisplayClaims object for the uhs field. + */ + public static final class Deserializer implements JsonDeserializer { + + @Override + public final XboxAuthResponse deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + try { + JsonObject root = json.getAsJsonObject(); + + String validUntil = this.getString(root, "NotAfter"); + String token = this.getString(root, "Token"); + String userHash = this.extractUserHash(root.getAsJsonObject("DisplayClaims")); + + String error = this.getString(root, "XErr"); + + return new XboxAuthResponse(validUntil, token, userHash, error); + } catch (IllegalStateException | ClassCastException e) { + throw new JsonParseException("invalid format", e); + } + } + + private String getString(JsonObject root, String name) { + JsonPrimitive primitive = root.getAsJsonPrimitive(name); + if (primitive != null) { + return primitive.getAsString(); + } else { + return null; + } + } + + private String extractUserHash(JsonObject displayClaims) { + if (displayClaims != null) { + JsonArray xui = displayClaims.getAsJsonArray("xui"); + if (xui != null) { + for (JsonElement claim : xui) { + JsonPrimitive uhs = claim.getAsJsonObject().getAsJsonPrimitive("uhs"); + if (uhs != null) { + return uhs.getAsString(); + } + } + } + } + return null; + } + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/dto/xbox/XboxLiveAuthRequest.java b/src/common/java/technicianlp/reauth/authentication/dto/xbox/XboxLiveAuthRequest.java new file mode 100644 index 0000000..cd3524a --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/dto/xbox/XboxLiveAuthRequest.java @@ -0,0 +1,49 @@ +package technicianlp.reauth.authentication.dto.xbox; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import technicianlp.reauth.authentication.dto.RequestObject; + +import java.lang.reflect.Type; + +/** + * Request Payload for the /user/authenticate Endpoint of the Xbox Live user services
+ * + * @see https://wiki.vg/Microsoft_Authentication_Scheme + */ +public final class XboxLiveAuthRequest implements RequestObject.JSON { + + private final String token; + + public XboxLiveAuthRequest(String token) { + this.token = token; + } + + @Override + public final Class getResponseClass() { + return XboxAuthResponse.class; + } + + /** + * Custom Serializer for {@link XboxLiveAuthRequest} for static data to improve readability + */ + public static final class Serializer implements JsonSerializer { + + @Override + public final JsonElement serialize(XboxLiveAuthRequest src, Type typeOfSrc, JsonSerializationContext context) { + JsonObject properties = new JsonObject(); + properties.addProperty("AuthMethod", "RPS"); + properties.addProperty("SiteName", "user.auth.xboxlive.com"); + properties.addProperty("RpsTicket", "d=" + src.token); + + JsonObject root = new JsonObject(); + root.add("Properties", properties); + //noinspection HttpUrlsUsage + root.addProperty("RelyingParty", "http://auth.xboxlive.com"); + root.addProperty("TokenType", "JWT"); + return root; + } + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/dto/xbox/XboxXstsAuthRequest.java b/src/common/java/technicianlp/reauth/authentication/dto/xbox/XboxXstsAuthRequest.java new file mode 100644 index 0000000..9fd8e7f --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/dto/xbox/XboxXstsAuthRequest.java @@ -0,0 +1,47 @@ +package technicianlp.reauth.authentication.dto.xbox; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import technicianlp.reauth.authentication.dto.RequestObject; + +import java.lang.reflect.Type; + +/** + * Request Payload for the /user/authenticate Endpoint of the Xbox Live user services
+ * + * @see https://wiki.vg/Microsoft_Authentication_Scheme + */ +public final class XboxXstsAuthRequest implements RequestObject.JSON { + + private final String token; + + public XboxXstsAuthRequest(String token) { + this.token = token; + } + + @Override + public final Class getResponseClass() { + return XboxAuthResponse.class; + } + + /** + * Custom Serializer for {@link XboxXstsAuthRequest} for static data to improve readability + */ + public static final class Serializer implements JsonSerializer { + + @Override + public final JsonElement serialize(XboxXstsAuthRequest src, Type typeOfSrc, JsonSerializationContext context) { + JsonObject properties = new JsonObject(); + properties.addProperty("SandboxId", "RETAIL"); + properties.add("UserTokens", context.serialize(new String[]{src.token})); + + JsonObject root = new JsonObject(); + root.add("Properties", properties); + root.addProperty("RelyingParty", "rp://api.minecraftservices.com/"); + root.addProperty("TokenType", "JWT"); + return root; + } + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/dto/yggdrasil/AuthenticateRequest.java b/src/common/java/technicianlp/reauth/authentication/dto/yggdrasil/AuthenticateRequest.java new file mode 100644 index 0000000..b1d0b5b --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/dto/yggdrasil/AuthenticateRequest.java @@ -0,0 +1,52 @@ +package technicianlp.reauth.authentication.dto.yggdrasil; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import technicianlp.reauth.authentication.dto.RequestObject; + +import java.lang.reflect.Type; + +/** + * Request Payload for the /authenticate Endpoint of the Mojang Yggdrasil API + * + * @see https://wiki.vg/Authentication + */ +public final class AuthenticateRequest implements RequestObject.JSON { + + private final String username; + private final String password; + private final String clientToken; + + public AuthenticateRequest(String username, String password, String clientToken) { + this.username = username; + this.password = password; + this.clientToken = clientToken; + } + + @Override + public final Class getResponseClass() { + return AuthenticateResponse.class; + } + + /** + * Custom Serializer for {@link AuthenticateRequest} for static data to improve readability + */ + public static final class Serializer implements JsonSerializer { + + @Override + public final JsonElement serialize(AuthenticateRequest src, Type typeOfSrc, JsonSerializationContext context) { + JsonObject agent = new JsonObject(); + agent.addProperty("name", "Minecraft"); + agent.addProperty("version", 1); + + JsonObject root = new JsonObject(); + root.add("agent", agent); + root.addProperty("username", src.username); + root.addProperty("password", src.password); + root.addProperty("clientToken", src.clientToken); + return root; + } + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/dto/yggdrasil/AuthenticateResponse.java b/src/common/java/technicianlp/reauth/authentication/dto/yggdrasil/AuthenticateResponse.java new file mode 100644 index 0000000..12587d2 --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/dto/yggdrasil/AuthenticateResponse.java @@ -0,0 +1,60 @@ +package technicianlp.reauth.authentication.dto.yggdrasil; + +import com.google.gson.annotations.SerializedName; +import org.jetbrains.annotations.Nullable; +import technicianlp.reauth.authentication.SessionData; +import technicianlp.reauth.authentication.dto.ResponseObject; + +/** + * Response Payload for the /authenticate of the Mojang Yggdrasil API + *
+ * Only relevant fields are deserialized + * + * @see https://wiki.vg/Authentication + */ +public final class AuthenticateResponse implements ResponseObject { + + @SerializedName("accessToken") + public final String accessToken; + @SerializedName("selectedProfile") + public final Profile profile; + + @SerializedName("error") + public final @Nullable String error; + + private AuthenticateResponse() { + this.accessToken = null; + this.profile = null; + this.error = null; + } + + @Override + public final boolean isValid() { + return this.error == null && this.accessToken != null && this.profile != null && this.profile.name != null && this.profile.uuid != null; + } + + @Override + public final @Nullable String getError() { + return this.error; + } + + public final SessionData getSession() { + if (this.profile == null) { + return null; + } + return new SessionData(this.profile.name, this.profile.uuid, this.accessToken, "mojang"); + } + + public static final class Profile { + + @SerializedName("name") + public final String name; + @SerializedName("id") + public final String uuid; + + private Profile() { + this.name = null; + this.uuid = null; + } + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/dto/yggdrasil/JoinServerRequest.java b/src/common/java/technicianlp/reauth/authentication/dto/yggdrasil/JoinServerRequest.java new file mode 100644 index 0000000..5db9a2d --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/dto/yggdrasil/JoinServerRequest.java @@ -0,0 +1,30 @@ +package technicianlp.reauth.authentication.dto.yggdrasil; + +import com.google.gson.annotations.SerializedName; +import technicianlp.reauth.authentication.dto.RequestObject; + +/** + * Request Payload for the /session/minecraft/join of the Mojang Session API + * + * @see https://wiki.vg/Authentication + */ +public final class JoinServerRequest implements RequestObject.JSON { + + @SerializedName("accessToken") + private final String accessToken; + @SerializedName("selectedProfile") + private final String uuid; + @SerializedName("serverId") + private final String hash; + + public JoinServerRequest(String accessToken, String uuid, String hash) { + this.accessToken = accessToken; + this.uuid = uuid; + this.hash = hash; + } + + @Override + public final Class getResponseClass() { + return JoinServerResponse.class; + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/dto/yggdrasil/JoinServerResponse.java b/src/common/java/technicianlp/reauth/authentication/dto/yggdrasil/JoinServerResponse.java new file mode 100644 index 0000000..8ececfc --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/dto/yggdrasil/JoinServerResponse.java @@ -0,0 +1,31 @@ +package technicianlp.reauth.authentication.dto.yggdrasil; + +import com.google.gson.annotations.SerializedName; +import org.jetbrains.annotations.Nullable; +import technicianlp.reauth.authentication.dto.ResponseObject; + +/** + * Response Payload for the /session/minecraft/join of the Mojang Session API + * Payload is only returned in case of error + * + * @see https://wiki.vg/Authentication + */ +public final class JoinServerResponse implements ResponseObject { + + @SerializedName("error") + public final @Nullable String error; + + private JoinServerResponse() { + this.error = null; + } + + @Override + public final boolean isValid() { + return this.error == null; + } + + @Override + public final @Nullable String getError() { + return this.error; + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/flows/AuthorizationCodeFlow.java b/src/common/java/technicianlp/reauth/authentication/flows/AuthorizationCodeFlow.java new file mode 100644 index 0000000..d38823a --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/flows/AuthorizationCodeFlow.java @@ -0,0 +1,6 @@ +package technicianlp.reauth.authentication.flows; + +public interface AuthorizationCodeFlow extends Flow { + + String getLoginUrl(); +} diff --git a/src/common/java/technicianlp/reauth/authentication/flows/DeviceCodeFlow.java b/src/common/java/technicianlp/reauth/authentication/flows/DeviceCodeFlow.java new file mode 100644 index 0000000..3a80d64 --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/flows/DeviceCodeFlow.java @@ -0,0 +1,10 @@ +package technicianlp.reauth.authentication.flows; + +import java.util.concurrent.CompletableFuture; + +public interface DeviceCodeFlow extends Flow { + + CompletableFuture getLoginUrl(); + + CompletableFuture getCode(); +} diff --git a/src/common/java/technicianlp/reauth/authentication/flows/Flow.java b/src/common/java/technicianlp/reauth/authentication/flows/Flow.java new file mode 100644 index 0000000..3d920bb --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/flows/Flow.java @@ -0,0 +1,33 @@ +package technicianlp.reauth.authentication.flows; + +import technicianlp.reauth.authentication.SessionData; +import technicianlp.reauth.configuration.Profile; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +public interface Flow { + + void cancel(); + + CompletableFuture getSession(); + + boolean hasProfile(); + + /** + * Returns a {@link Profile} containing all required profile information. + * + * @throws IllegalStateException if a profile cannot be created + */ + CompletableFuture getProfile(); + + default void thenRunAsync(Runnable runnable, Executor executor) { + CompletableFuture target; + if (!this.hasProfile()) { + target = this.getSession(); + } else { + target = CompletableFuture.allOf(this.getSession(), this.getProfile()); + } + target.thenRunAsync(runnable, executor); + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/flows/FlowCallback.java b/src/common/java/technicianlp/reauth/authentication/flows/FlowCallback.java new file mode 100644 index 0000000..addd13d --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/flows/FlowCallback.java @@ -0,0 +1,21 @@ +package technicianlp.reauth.authentication.flows; + +import technicianlp.reauth.authentication.SessionData; +import technicianlp.reauth.configuration.Profile; + +import java.util.concurrent.Executor; + +public interface FlowCallback { + + /** + * transition displayed stage & log + */ + void transitionStage(FlowStage newStage); + + void onSessionComplete(SessionData session, Throwable throwable); + + void onProfileComplete(Profile profile, Throwable throwable); + + Executor getExecutor(); + +} diff --git a/src/common/java/technicianlp/reauth/authentication/flows/FlowStage.java b/src/common/java/technicianlp/reauth/authentication/flows/FlowStage.java new file mode 100644 index 0000000..833d3a8 --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/flows/FlowStage.java @@ -0,0 +1,41 @@ +package technicianlp.reauth.authentication.flows; + +public enum FlowStage { + + INITIAL("reauth.msauth.step.initial", "Starting Flow"), + FINISHED("reauth.msauth.step.finished", "Login successful"), + PROFILE("reauth.msauth.step.profile", "Saving Profile"), + FAILED("reauth.msauth.step.failed", "Login failed"), + CRYPTO_INIT("reauth.msauth.step.crypto", "Initializing Encryption"), + + YGG_AUTH("reauth.auth.step.yggdrasil", "Authenticating with Mojang Yggdrasil"), + + MS_AWAIT_AUTH_CODE("reauth.msauth.step.microsoft.code.await", "Waiting for Authentication"), + MS_REDEEM_AUTH_CODE("reauth.msauth.step.microsoft.code.redeem", "Authenticating with Microsoft"), + + MS_REQUEST_DEVICE_CODE("reauth.msauth.step.microsoft.device.request", "Requesting Device Code from Microsoft"), + MS_POLL_DEVICE_CODE("reauth.msauth.step.microsoft.device.poll", "Starting to poll Microsoft for token"), + + MS_REDEEM_REFRESH_TOKEN("reauth.msauth.step.microsoft.refresh", "Refreshing Authentication with Microsoft"), + + MS_AUTH_XASU("reauth.msauth.step.xbox", "Authenticating with Xbox Live"), + MS_AUTH_XSTS("reauth.msauth.step.xsts", "Authenticating with XSTS"), + MS_AUTH_MOJANG("reauth.msauth.step.mojang", "Authenticating with Mojang"), + MS_FETCH_PROFILE("reauth.msauth.step.fetch", "Retrieving Profile"); + + private final String rawName; + private final String logLine; + + FlowStage(String rawName, String logLine) { + this.rawName = rawName; + this.logLine = logLine; + } + + public final String getRawName() { + return this.rawName; + } + + public final String getLogLine() { + return this.logLine; + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/flows/Flows.java b/src/common/java/technicianlp/reauth/authentication/flows/Flows.java new file mode 100644 index 0000000..56b27f9 --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/flows/Flows.java @@ -0,0 +1,35 @@ +package technicianlp.reauth.authentication.flows; + +import technicianlp.reauth.authentication.flows.impl.MicrosoftCodeFlow; +import technicianlp.reauth.authentication.flows.impl.MicrosoftDeviceFlow; +import technicianlp.reauth.authentication.flows.impl.MicrosoftProfileFlow; +import technicianlp.reauth.authentication.flows.impl.MojangAuthenticationFlow; +import technicianlp.reauth.authentication.flows.impl.UnknownProfileFlow; +import technicianlp.reauth.configuration.Profile; +import technicianlp.reauth.configuration.ProfileConstants; + +public final class Flows { + + public static Flow loginWithProfile(Profile profile, FlowCallback callback) { + switch (profile.getValue(ProfileConstants.PROFILE_TYPE)) { + case ProfileConstants.PROFILE_TYPE_MICROSOFT: + return new MicrosoftProfileFlow(profile, callback); + case ProfileConstants.PROFILE_TYPE_MOJANG: + return new MojangAuthenticationFlow(profile, callback); + default: + return new UnknownProfileFlow(callback); + } + } + + public static AuthorizationCodeFlow loginWithAuthCode(boolean persist, FlowCallback callback) { + return new MicrosoftCodeFlow(persist, callback); + } + + public static DeviceCodeFlow loginWithDeviceCode(boolean persist, FlowCallback callback) { + return new MicrosoftDeviceFlow(persist, callback); + } + + public static Flow loginWithMojang(String username, String password, boolean persist, FlowCallback callback) { + return new MojangAuthenticationFlow(username, password, persist, callback); + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/flows/Tokens.java b/src/common/java/technicianlp/reauth/authentication/flows/Tokens.java new file mode 100644 index 0000000..144c70f --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/flows/Tokens.java @@ -0,0 +1,23 @@ +package technicianlp.reauth.authentication.flows; + +import technicianlp.reauth.authentication.dto.microsoft.MicrosoftAuthResponse; +import technicianlp.reauth.authentication.dto.xbox.XboxAuthResponse; + +public final class Tokens { + + private final String xblToken; + private final String refreshToken; + + public Tokens(MicrosoftAuthResponse microsoft, XboxAuthResponse xasu) { + this.xblToken = xasu.getToken(); + this.refreshToken = microsoft.getRefreshToken(); + } + + public final String getXblToken() { + return this.xblToken; + } + + public final String getRefreshToken() { + return this.refreshToken; + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/flows/impl/FlowBase.java b/src/common/java/technicianlp/reauth/authentication/flows/impl/FlowBase.java new file mode 100644 index 0000000..38f9b49 --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/flows/impl/FlowBase.java @@ -0,0 +1,114 @@ +package technicianlp.reauth.authentication.flows.impl; + +import technicianlp.reauth.authentication.SessionData; +import technicianlp.reauth.authentication.flows.Flow; +import technicianlp.reauth.authentication.flows.FlowCallback; +import technicianlp.reauth.authentication.flows.FlowStage; +import technicianlp.reauth.authentication.flows.impl.util.AuthBiFunction; +import technicianlp.reauth.authentication.flows.impl.util.AuthFunction; +import technicianlp.reauth.authentication.flows.impl.util.AuthSupplier; +import technicianlp.reauth.configuration.Profile; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Executor; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; + +abstract class FlowBase implements Flow { + + private final List> stages = new ArrayList<>(); + private final List flows = new ArrayList<>(); + + private final FlowCallback callback; + final Executor executor; + + protected FlowBase(FlowCallback callback) { + this.callback = callback; + this.executor = callback.getExecutor(); + } + + /** + * Register dependant {@link CompletionStage} for cancellation handling. + * Every major stage should be registered for handling as cancelling a completed stage does no propagate to its dependants. + */ + final void registerDependantStages(CompletableFuture... stages) { + Collections.addAll(this.stages, stages); + } + + /** + * Register a dependant {@link FlowBase} for cancellation handling. + * Used to handle potential cleanup operations as the flows stages should already have been cancelled + */ + final void registerDependantFlow(FlowBase flow) { + this.stages.add(flow.getSession()); + this.flows.add(flow); + } + + /** + * Cancel the flow and its dependants. + * Cancels registered dependant stages in order. + * Requests cancellation of dependant flows. + */ + @Override + public void cancel() { + this.stages.forEach(stage -> stage.cancel(true)); + this.flows.forEach(FlowBase::cancel); + this.getSession().cancel(true); + } + + final void onSessionComplete(SessionData data, Throwable throwable) { + if (throwable == null) { + if (this.hasProfile() && !this.getProfile().isDone()) { + this.step(FlowStage.PROFILE); + } else { + this.step(FlowStage.FINISHED); + } + } else { + this.step(FlowStage.FAILED); + } + this.callback.onSessionComplete(data, throwable); + } + + final void onProfileComplete(Profile profile, Throwable throwable) { + if (throwable == null) { + if (this.getSession().isDone() && !this.getSession().isCompletedExceptionally()) { + this.step(FlowStage.FINISHED); + } + } + this.callback.onProfileComplete(profile, throwable); + } + + final BiFunction wrapStep(FlowStage stage, AuthBiFunction step) { + return (t, u) -> { + this.step(stage); + return step.apply(t, u); + }; + } + + final Function wrapStep(FlowStage stage, AuthFunction step) { + return (t) -> { + this.step(stage); + return step.apply(t); + }; + } + + final Supplier wrapStep(FlowStage stage, AuthSupplier step) { + return () -> { + this.step(stage); + return step.get(); + }; + } + + final Function wrap(AuthFunction step) { + return step; + } + + final void step(FlowStage stage) { + this.callback.transitionStage(stage); + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/flows/impl/MicrosoftCodeFlow.java b/src/common/java/technicianlp/reauth/authentication/flows/impl/MicrosoftCodeFlow.java new file mode 100644 index 0000000..dd58df3 --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/flows/impl/MicrosoftCodeFlow.java @@ -0,0 +1,95 @@ +package technicianlp.reauth.authentication.flows.impl; + +import technicianlp.reauth.authentication.MsAuthAPI; +import technicianlp.reauth.authentication.SessionData; +import technicianlp.reauth.authentication.dto.microsoft.MicrosoftAuthResponse; +import technicianlp.reauth.authentication.dto.xbox.XboxAuthResponse; +import technicianlp.reauth.authentication.flows.AuthorizationCodeFlow; +import technicianlp.reauth.authentication.flows.FlowCallback; +import technicianlp.reauth.authentication.flows.FlowStage; +import technicianlp.reauth.authentication.flows.Tokens; +import technicianlp.reauth.authentication.http.server.AuthenticationCodeServer; +import technicianlp.reauth.configuration.Profile; +import technicianlp.reauth.configuration.ProfileBuilder; +import technicianlp.reauth.crypto.Crypto; +import technicianlp.reauth.crypto.PkceChallenge; +import technicianlp.reauth.crypto.ProfileEncryption; + +import java.io.IOException; +import java.util.concurrent.CompletableFuture; + +public final class MicrosoftCodeFlow extends FlowBase implements AuthorizationCodeFlow { + + private final String loginUrl; + + private final CompletableFuture session; + private final CompletableFuture profile; + + private AuthenticationCodeServer codeServer; + + public MicrosoftCodeFlow(boolean persist, FlowCallback callback) { + super(callback); + PkceChallenge pkceChallenge = Crypto.createPkceChallenge(); + this.loginUrl = MsAuthAPI.getLoginUrl(persist, pkceChallenge.getChallenge()); + + CompletableFuture codeStage = new CompletableFuture<>(); + CompletableFuture pkceVerifier = CompletableFuture.completedFuture(pkceChallenge.getVerifier()); + CompletableFuture ms = codeStage.thenCombineAsync(pkceVerifier, this.wrapStep(FlowStage.MS_REDEEM_AUTH_CODE, MsAuthAPI::redeemAuthorizationCode), this.executor); + CompletableFuture xasu = ms.thenApply(MicrosoftAuthResponse::getAccessToken) + .thenApplyAsync(this.wrapStep(FlowStage.MS_AUTH_XASU, MsAuthAPI::authenticateXASU), this.executor); + XboxAuthenticationFlow flow = new XboxAuthenticationFlow(xasu.thenApply(XboxAuthResponse::getToken), callback); + this.session = flow.getSession(); + this.session.whenComplete(this::onSessionComplete); + this.registerDependantStages(codeStage, ms, xasu, this.session); + this.registerDependantFlow(flow); + + if (persist) { + CompletableFuture tokens = ms.thenCombine(xasu, Tokens::new); + CompletableFuture encryption = CompletableFuture.supplyAsync(Crypto::newEncryption, this.executor); + CompletableFuture builder = this.session.thenCombine(encryption, ProfileBuilder::new); + this.profile = builder.thenCombine(tokens, ProfileBuilder::buildMicrosoft); + this.profile.whenComplete(this::onProfileComplete); + } else { + this.profile = null; + } + + this.executor.execute(() -> { + try { + this.codeServer = new AuthenticationCodeServer(MsAuthAPI.port, this.loginUrl, codeStage, this.executor); + this.step(FlowStage.MS_AWAIT_AUTH_CODE); + } catch (IOException | NoClassDefFoundError exception) { + codeStage.completeExceptionally(exception); + } + }); + } + + @Override + public final CompletableFuture getSession() { + return this.session; + } + + @Override + public boolean hasProfile() { + return this.profile != null; + } + + @Override + public final CompletableFuture getProfile() { + if (this.profile != null) { + return this.profile; + } else { + throw new IllegalStateException("Persistence not requested"); + } + } + + @Override + public final String getLoginUrl() { + return this.loginUrl; + } + + @Override + public final void cancel() { + super.cancel(); + this.executor.execute(() -> this.codeServer.stop(true)); + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/flows/impl/MicrosoftDeviceFlow.java b/src/common/java/technicianlp/reauth/authentication/flows/impl/MicrosoftDeviceFlow.java new file mode 100644 index 0000000..d67cc0b --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/flows/impl/MicrosoftDeviceFlow.java @@ -0,0 +1,127 @@ +package technicianlp.reauth.authentication.flows.impl; + +import technicianlp.reauth.ReAuth; +import technicianlp.reauth.authentication.MsAuthAPI; +import technicianlp.reauth.authentication.SessionData; +import technicianlp.reauth.authentication.dto.microsoft.MicrosoftAuthResponse; +import technicianlp.reauth.authentication.dto.microsoft.device.MicrosoftAuthDeviceResponse; +import technicianlp.reauth.authentication.dto.xbox.XboxAuthResponse; +import technicianlp.reauth.authentication.flows.DeviceCodeFlow; +import technicianlp.reauth.authentication.flows.FlowCallback; +import technicianlp.reauth.authentication.flows.FlowStage; +import technicianlp.reauth.authentication.flows.Tokens; +import technicianlp.reauth.authentication.http.InvalidResponseException; +import technicianlp.reauth.authentication.http.Response; +import technicianlp.reauth.authentication.http.UnreachableServiceException; +import technicianlp.reauth.configuration.Profile; +import technicianlp.reauth.configuration.ProfileBuilder; +import technicianlp.reauth.crypto.Crypto; +import technicianlp.reauth.crypto.ProfileEncryption; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.TimeUnit; + +public final class MicrosoftDeviceFlow extends FlowBase implements DeviceCodeFlow { + + private final CompletableFuture session; + private final CompletableFuture profile; + + private final CompletableFuture url; + private final CompletableFuture code; + + private final CompletableFuture auth; + + public MicrosoftDeviceFlow(boolean persist, FlowCallback callback) { + super(callback); + + CompletableFuture deviceResponse = CompletableFuture.completedFuture(persist) + .thenApplyAsync(this.wrapStep(FlowStage.MS_REQUEST_DEVICE_CODE, MsAuthAPI::requestDeviceCode), this.executor); + this.url = deviceResponse.thenApply(MicrosoftAuthDeviceResponse::getVerificationUri); + this.code = deviceResponse.thenApply(MicrosoftAuthDeviceResponse::getUserCode); + this.auth = deviceResponse.thenApplyAsync(this::pollForCode, this.executor); + CompletableFuture xasu = this.auth.thenApply(MicrosoftAuthResponse::getAccessToken) + .thenApplyAsync(this.wrapStep(FlowStage.MS_AUTH_XASU, MsAuthAPI::authenticateXASU), this.executor); + + XboxAuthenticationFlow flow = new XboxAuthenticationFlow(xasu.thenApply(XboxAuthResponse::getToken), callback); + this.session = flow.getSession(); + this.session.whenComplete(this::onSessionComplete); + this.registerDependantStages(deviceResponse, this.auth, xasu, this.session); + this.registerDependantFlow(flow); + + if (persist) { + CompletableFuture tokens = this.auth.thenCombine(xasu, Tokens::new); + CompletableFuture encryption = CompletableFuture.supplyAsync(Crypto::newEncryption, this.executor); + CompletableFuture builder = this.session.thenCombine(encryption, ProfileBuilder::new); + this.profile = builder.thenCombine(tokens, ProfileBuilder::buildMicrosoft); + this.profile.whenComplete(this::onProfileComplete); + } else { + this.profile = null; + } + } + + /** + * Poll the Microsoft Token Endpoint until the user has completed authentication. + * Interval between Polls is specified by {@link MicrosoftAuthDeviceResponse#interval}. + */ + private MicrosoftAuthResponse pollForCode(MicrosoftAuthDeviceResponse deviceResponse) { + this.step(FlowStage.MS_POLL_DEVICE_CODE); + while (!this.auth.isDone()) { + ReAuth.log.debug("Polling Microsoft for token"); + try { + Response response = MsAuthAPI.redeemDeviceCode(deviceResponse.deviceCode); + if (response.isValid()) { + ReAuth.log.info("Authorization received"); + return response.get(); + } else { + MicrosoftAuthResponse responseError = response.getUnchecked(); + if ("authorization_pending".equals(responseError.getError())) { + ReAuth.log.debug("Authorization is still pending - continue polling"); + } else { + ReAuth.log.info("Authorization failed: " + responseError.getError()); + // will throw InvalidResponseException + return response.get(); + } + } + } catch (UnreachableServiceException | InvalidResponseException e) { + throw new CompletionException(e); + } + try { + TimeUnit.SECONDS.sleep(deviceResponse.interval); + } catch (InterruptedException e) { + throw new CompletionException(e); + } + } + // Polling cancelled + return null; + } + + @Override + public final CompletableFuture getSession() { + return this.session; + } + + @Override + public final boolean hasProfile() { + return this.profile != null; + } + + @Override + public final CompletableFuture getProfile() { + if (this.profile != null) { + return this.profile; + } else { + throw new IllegalStateException("Persistence not requested"); + } + } + + @Override + public final CompletableFuture getLoginUrl() { + return this.url; + } + + @Override + public final CompletableFuture getCode() { + return this.code; + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/flows/impl/MicrosoftProfileFlow.java b/src/common/java/technicianlp/reauth/authentication/flows/impl/MicrosoftProfileFlow.java new file mode 100644 index 0000000..e76a763 --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/flows/impl/MicrosoftProfileFlow.java @@ -0,0 +1,117 @@ +package technicianlp.reauth.authentication.flows.impl; + +import technicianlp.reauth.authentication.MsAuthAPI; +import technicianlp.reauth.authentication.SessionData; +import technicianlp.reauth.authentication.dto.microsoft.MicrosoftAuthResponse; +import technicianlp.reauth.authentication.dto.xbox.XboxAuthResponse; +import technicianlp.reauth.authentication.flows.FlowCallback; +import technicianlp.reauth.authentication.flows.FlowStage; +import technicianlp.reauth.authentication.flows.Tokens; +import technicianlp.reauth.authentication.flows.impl.util.Futures; +import technicianlp.reauth.authentication.http.Response; +import technicianlp.reauth.configuration.Profile; +import technicianlp.reauth.configuration.ProfileBuilder; +import technicianlp.reauth.configuration.ProfileConstants; +import technicianlp.reauth.crypto.Crypto; +import technicianlp.reauth.crypto.ProfileEncryption; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.function.BiConsumer; +import java.util.function.Function; + +public final class MicrosoftProfileFlow extends FlowBase { + + private final CompletableFuture encryption; + private final CompletableFuture session; + + private final XboxAuthenticationFlow xboxFlow1; + + private final CompletableFuture refreshRequired; + private final CompletableFuture profileFuture; + + /** + * tries to login using the stored accessToken. + * When authentication fails due to an expired token, the stored refreshToken is used to acquire a new accessToken followed by a second login attempt + * + * @see XboxAuthenticationFlow#isExpiredToken(Response, Throwable) + */ + public MicrosoftProfileFlow(Profile profile, FlowCallback callback) { + super(callback); + + this.session = new CompletableFuture<>(); + this.session.whenComplete(this::onSessionComplete); + CompletableFuture profileFuture = CompletableFuture.completedFuture(profile); + this.encryption = profileFuture.thenApplyAsync(this.wrapStep(FlowStage.CRYPTO_INIT, Crypto::getProfileEncryption), this.executor); + this.refreshRequired = new CompletableFuture<>(); + + CompletableFuture xblTokenDec = this.encryption.thenCombineAsync(profile.get(ProfileConstants.XBL_TOKEN), ProfileEncryption::decryptFieldOne, this.executor); + this.xboxFlow1 = new XboxAuthenticationFlow(xblTokenDec, callback); + this.xboxFlow1.getSession().whenComplete(this::onComplete); + this.registerDependantStages(this.encryption, xblTokenDec); + this.registerDependantFlow(this.xboxFlow1); + + CompletableFuture refreshTokenEnc = this.refreshRequired.thenCompose(Futures.conditional(profile.get(ProfileConstants.REFRESH_TOKEN), Futures.cancelled())); + CompletableFuture refreshTokenDec = this.encryption.thenCombineAsync(refreshTokenEnc, ProfileEncryption::decryptFieldTwo, this.executor); + CompletableFuture auth = refreshTokenDec.thenApplyAsync(this.wrapStep(FlowStage.MS_REDEEM_REFRESH_TOKEN, MsAuthAPI::redeemRefreshToken), this.executor); + CompletableFuture xasu = auth.thenApply(MicrosoftAuthResponse::getAccessToken) + .thenApplyAsync(this.wrapStep(FlowStage.MS_AUTH_XASU, MsAuthAPI::authenticateXASU), this.executor); + CompletableFuture xblToken = xasu.thenApply(XboxAuthResponse::getToken); + XboxAuthenticationFlow xboxFlow2 = new XboxAuthenticationFlow(xblToken, callback); + xboxFlow2.getSession().whenComplete(this::onComplete); + this.registerDependantStages(this.refreshRequired, refreshTokenDec, auth); + this.registerDependantFlow(xboxFlow2); + + CompletableFuture tokens = auth.thenCombine(xasu, Tokens::new); + this.profileFuture = this.refreshRequired.thenComposeAsync(Futures.conditional(() -> this.constructProfile(tokens), CompletableFuture.completedFuture(profile)), this.executor); + this.profileFuture.whenComplete(this::onProfileComplete); + } + + /** + * A reimagined version of {@link CompletableFuture#applyToEither(CompletionStage, Function)} that makes guarantees on exceptional behaviour and triggers the fallback computation if required. + *

+ * completes {@link MicrosoftProfileFlow#session} with the provided session if completed normally (throwable = null)
+ * if the first {@link XboxAuthenticationFlow} completed exceptionally caused by an expired token the refresh-flow is triggered by completing {@link MicrosoftProfileFlow#refreshRequired}
+ * otherwise {@link MicrosoftProfileFlow#session} is completed exceptionally with the supplied throwable. + * + * @see CompletionStage#whenComplete(BiConsumer) + * @see CompletableFuture#applyToEither(CompletionStage, Function) + */ + private void onComplete(SessionData sessionData, Throwable throwable) { + if (throwable == null) { + this.session.complete(sessionData); + this.refreshRequired.complete(false); + } else { + if (this.refreshRequired.isDone()) { + this.session.completeExceptionally(throwable); + } + if (this.xboxFlow1.hasExpiredTokenError()) { + this.refreshRequired.complete(true); + } else { + this.session.completeExceptionally(throwable); + this.refreshRequired.complete(false); + } + } + } + + @Override + public final CompletableFuture getSession() { + return this.session; + } + + @Override + public final boolean hasProfile() { + return true; + } + + @Override + public final CompletableFuture getProfile() { + return this.profileFuture; + } + + private CompletableFuture constructProfile(CompletableFuture tokens) { + CompletableFuture encryption = this.encryption.thenApplyAsync(ProfileEncryption::randomizedCopy, this.executor); + CompletableFuture builder = this.session.thenCombine(encryption, ProfileBuilder::new); + return builder.thenCombineAsync(tokens, ProfileBuilder::buildMicrosoft, this.executor); + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/flows/impl/MojangAuthenticationFlow.java b/src/common/java/technicianlp/reauth/authentication/flows/impl/MojangAuthenticationFlow.java new file mode 100644 index 0000000..1e4f9c7 --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/flows/impl/MojangAuthenticationFlow.java @@ -0,0 +1,68 @@ +package technicianlp.reauth.authentication.flows.impl; + +import technicianlp.reauth.authentication.SessionData; +import technicianlp.reauth.authentication.YggdrasilAPI; +import technicianlp.reauth.authentication.flows.FlowCallback; +import technicianlp.reauth.authentication.flows.FlowStage; +import technicianlp.reauth.configuration.Profile; +import technicianlp.reauth.configuration.ProfileBuilder; +import technicianlp.reauth.configuration.ProfileConstants; +import technicianlp.reauth.crypto.Crypto; +import technicianlp.reauth.crypto.ProfileEncryption; + +import java.util.concurrent.CompletableFuture; + +public final class MojangAuthenticationFlow extends FlowBase { + + private final CompletableFuture session; + + private final CompletableFuture profile; + + public MojangAuthenticationFlow(String username, String password, boolean save, FlowCallback callback) { + super(callback); + this.session = CompletableFuture.supplyAsync(this.wrapStep(FlowStage.YGG_AUTH, () -> YggdrasilAPI.login(username, password)), this.executor); + this.session.whenComplete(this::onSessionComplete); + this.registerDependantStages(this.session); + + if (save) { + CompletableFuture encryption = CompletableFuture.supplyAsync(Crypto::newEncryption, this.executor); + CompletableFuture builder = this.session.thenCombine(encryption, ProfileBuilder::new); + this.profile = builder.thenApply(b -> b.buildMojang(username, password)); + } else { + this.profile = null; + } + } + + public MojangAuthenticationFlow(Profile profile, FlowCallback callback) { + super(callback); + CompletableFuture profileFuture = CompletableFuture.completedFuture(profile); + CompletableFuture encryption = profileFuture.thenApplyAsync(this.wrapStep(FlowStage.CRYPTO_INIT, Crypto::getProfileEncryption), this.executor); + CompletableFuture usernameDec = encryption.thenCombineAsync(profile.get(ProfileConstants.USERNAME), ProfileEncryption::decryptFieldOne, this.executor); + CompletableFuture passwordDec = encryption.thenCombineAsync(profile.get(ProfileConstants.PASSWORD), ProfileEncryption::decryptFieldTwo, this.executor); + + this.session = usernameDec.thenCombineAsync(passwordDec, this.wrapStep(FlowStage.YGG_AUTH, YggdrasilAPI::login), this.executor); + this.registerDependantStages(encryption, usernameDec, passwordDec, this.session); + + this.profile = CompletableFuture.completedFuture(profile); + this.profile.whenComplete(this::onProfileComplete); + } + + @Override + public final CompletableFuture getSession() { + return this.session; + } + + @Override + public final boolean hasProfile() { + return this.profile != null; + } + + @Override + public final CompletableFuture getProfile() { + if (this.profile != null) { + return this.profile; + } else { + throw new IllegalStateException("Persistence not requested"); + } + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/flows/impl/UnknownProfileFlow.java b/src/common/java/technicianlp/reauth/authentication/flows/impl/UnknownProfileFlow.java new file mode 100644 index 0000000..73339d8 --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/flows/impl/UnknownProfileFlow.java @@ -0,0 +1,34 @@ +package technicianlp.reauth.authentication.flows.impl; + +import technicianlp.reauth.authentication.SessionData; +import technicianlp.reauth.authentication.flows.FlowCallback; +import technicianlp.reauth.authentication.flows.impl.util.Futures; +import technicianlp.reauth.configuration.Profile; + +import java.util.concurrent.CompletableFuture; + +public final class UnknownProfileFlow extends FlowBase { + + private final CompletableFuture future; + + public UnknownProfileFlow(FlowCallback callback) { + super(callback); + this.future = Futures.failed(new IllegalArgumentException("Unknown Profile Type")); + this.future.whenComplete(this::onSessionComplete); + } + + @Override + public final CompletableFuture getSession() { + return this.future; + } + + @Override + public final boolean hasProfile() { + return false; + } + + @Override + public final CompletableFuture getProfile() { + throw new IllegalStateException("Profile creation not supported"); + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/flows/impl/XboxAuthenticationFlow.java b/src/common/java/technicianlp/reauth/authentication/flows/impl/XboxAuthenticationFlow.java new file mode 100644 index 0000000..40ab405 --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/flows/impl/XboxAuthenticationFlow.java @@ -0,0 +1,84 @@ +package technicianlp.reauth.authentication.flows.impl; + +import technicianlp.reauth.ReAuth; +import technicianlp.reauth.authentication.MsAuthAPI; +import technicianlp.reauth.authentication.SessionData; +import technicianlp.reauth.authentication.dto.mojang.MojangAuthResponse; +import technicianlp.reauth.authentication.dto.mojang.ProfileResponse; +import technicianlp.reauth.authentication.dto.xbox.XboxAuthResponse; +import technicianlp.reauth.authentication.flows.FlowCallback; +import technicianlp.reauth.authentication.flows.FlowStage; +import technicianlp.reauth.authentication.http.InvalidResponseException; +import technicianlp.reauth.authentication.http.Response; +import technicianlp.reauth.authentication.http.UnreachableServiceException; +import technicianlp.reauth.configuration.Profile; + +import java.util.concurrent.CompletableFuture; + +final class XboxAuthenticationFlow extends FlowBase { + + private static final String XSTS_ERR_TOKEN_EXPIRED = Integer.toUnsignedString(0x8015DC22); + private static final String XSTS_ERR_TOKEN_INVALID = Integer.toUnsignedString(0x8015DC26); + + private final CompletableFuture session; + private final CompletableFuture> xstsAuthResponse; + + public XboxAuthenticationFlow(CompletableFuture xblToken, FlowCallback callback) { + super(callback); + + this.xstsAuthResponse = xblToken.thenApplyAsync(this.wrapStep(FlowStage.MS_AUTH_XSTS, MsAuthAPI::authenticateXSTS), this.executor); + CompletableFuture xsts = this.xstsAuthResponse.thenApply(this.wrap(Response::get)); + CompletableFuture mojang = xsts.thenApplyAsync(this.wrapStep(FlowStage.MS_AUTH_MOJANG, this::authenticateMojang), this.executor); + CompletableFuture token = mojang.thenApply(MojangAuthResponse::getToken); + CompletableFuture profile = token.thenApplyAsync(this.wrapStep(FlowStage.MS_FETCH_PROFILE, MsAuthAPI::fetchProfile), this.executor); + this.session = token.thenCombine(profile, this::makeSession); + + this.registerDependantStages(this.xstsAuthResponse, xsts, mojang, profile, this.session); + } + + private MojangAuthResponse authenticateMojang(XboxAuthResponse xsts) throws UnreachableServiceException, InvalidResponseException { + return MsAuthAPI.authenticateMojang(xsts.token, xsts.userHash); + } + + private SessionData makeSession(String token, ProfileResponse profile) { + return new SessionData(profile.name, profile.uuid, token, "msa"); + } + + @Override + public final CompletableFuture getSession() { + return this.session; + } + + /** + * checks whether the passed response contains an error caused expired/invalid XASU-Token. + * If a valid response is passed or the request failed with an exception false is returned. + */ + final boolean hasExpiredTokenError() { + if (!this.xstsAuthResponse.isDone()) { + ReAuth.log.warn("Cant determine token expiration on unfinished request"); + return false; + } + if (this.xstsAuthResponse.isCompletedExceptionally()) { + return false; + } + Response response = this.xstsAuthResponse.join(); + if (response.isValid()) { + return false; + } + XboxAuthResponse rawResponse = response.getUnchecked(); + if (rawResponse != null) { + return XSTS_ERR_TOKEN_EXPIRED.equals(rawResponse.error) || XSTS_ERR_TOKEN_INVALID.equals(rawResponse.error); + } + return false; + } + + @Override + public final boolean hasProfile() { + return false; + } + + @Override + public final CompletableFuture getProfile() { + throw new IllegalStateException("Profile creation not supported"); + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/flows/impl/util/AuthBiFunction.java b/src/common/java/technicianlp/reauth/authentication/flows/impl/util/AuthBiFunction.java new file mode 100644 index 0000000..31b022d --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/flows/impl/util/AuthBiFunction.java @@ -0,0 +1,27 @@ +package technicianlp.reauth.authentication.flows.impl.util; + +import technicianlp.reauth.authentication.http.InvalidResponseException; +import technicianlp.reauth.authentication.http.UnreachableServiceException; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.function.BiFunction; + +/** + * Functional interface that allows a method reference to throw {@link UnreachableServiceException} or {@link InvalidResponseException}. + * Thrown Exceptions will be wrapped with a {@link CompletionException} for use with {@link CompletableFuture}. + */ +@FunctionalInterface +public interface AuthBiFunction extends BiFunction { + + R applyAuthStep(T t, U u) throws UnreachableServiceException, InvalidResponseException; + + @Override + default R apply(T t, U u) { + try { + return this.applyAuthStep(t, u); + } catch (UnreachableServiceException | InvalidResponseException e) { + throw new CompletionException(e); + } + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/flows/impl/util/AuthFunction.java b/src/common/java/technicianlp/reauth/authentication/flows/impl/util/AuthFunction.java new file mode 100644 index 0000000..9a243fc --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/flows/impl/util/AuthFunction.java @@ -0,0 +1,27 @@ +package technicianlp.reauth.authentication.flows.impl.util; + +import technicianlp.reauth.authentication.http.InvalidResponseException; +import technicianlp.reauth.authentication.http.UnreachableServiceException; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.function.Function; + +/** + * Functional interface that allows a method reference to throw {@link UnreachableServiceException} or {@link InvalidResponseException}. + * Thrown Exceptions will be wrapped with a {@link CompletionException} for use with {@link CompletableFuture}. + */ +@FunctionalInterface +public interface AuthFunction extends Function { + + R applyAuthStep(T t) throws UnreachableServiceException, InvalidResponseException; + + @Override + default R apply(T t) { + try { + return this.applyAuthStep(t); + } catch (UnreachableServiceException | InvalidResponseException e) { + throw new CompletionException(e); + } + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/flows/impl/util/AuthSupplier.java b/src/common/java/technicianlp/reauth/authentication/flows/impl/util/AuthSupplier.java new file mode 100644 index 0000000..ace2003 --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/flows/impl/util/AuthSupplier.java @@ -0,0 +1,27 @@ +package technicianlp.reauth.authentication.flows.impl.util; + +import technicianlp.reauth.authentication.http.InvalidResponseException; +import technicianlp.reauth.authentication.http.UnreachableServiceException; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.function.Supplier; + +/** + * Functional interface that allows a method reference to throw {@link UnreachableServiceException} or {@link InvalidResponseException}. + * Thrown Exceptions will be wrapped with a {@link CompletionException} for use with {@link CompletableFuture}. + */ +@FunctionalInterface +public interface AuthSupplier extends Supplier { + + T getAuthStep() throws UnreachableServiceException, InvalidResponseException; + + @Override + default T get() { + try { + return this.getAuthStep(); + } catch (UnreachableServiceException | InvalidResponseException e) { + throw new CompletionException(e); + } + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/flows/impl/util/Futures.java b/src/common/java/technicianlp/reauth/authentication/flows/impl/util/Futures.java new file mode 100644 index 0000000..83257e2 --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/flows/impl/util/Futures.java @@ -0,0 +1,28 @@ +package technicianlp.reauth.authentication.flows.impl.util; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.util.function.Supplier; + +public final class Futures { + + public static Function> conditional(CompletableFuture value, CompletableFuture fallback) { + return condition -> condition ? value : fallback; + } + + public static Function> conditional(Supplier> value, CompletableFuture fallback) { + return condition -> condition ? value.get() : fallback; + } + + public static CompletableFuture failed(Throwable throwable) { + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(throwable); + return future; + } + + public static CompletableFuture cancelled() { + CompletableFuture future = new CompletableFuture<>(); + future.cancel(true); + return future; + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/http/HttpUtil.java b/src/common/java/technicianlp/reauth/authentication/http/HttpUtil.java new file mode 100644 index 0000000..79a5325 --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/http/HttpUtil.java @@ -0,0 +1,126 @@ +package technicianlp.reauth.authentication.http; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonParseException; +import technicianlp.reauth.authentication.dto.RequestObject; +import technicianlp.reauth.authentication.dto.ResponseObject; +import technicianlp.reauth.authentication.dto.xbox.XboxAuthResponse; +import technicianlp.reauth.authentication.dto.xbox.XboxLiveAuthRequest; +import technicianlp.reauth.authentication.dto.xbox.XboxXstsAuthRequest; +import technicianlp.reauth.authentication.dto.yggdrasil.AuthenticateRequest; +import technicianlp.reauth.mojangfix.CertWorkaround; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLSocketFactory; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.Reader; +import java.io.UnsupportedEncodingException; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.stream.Collectors; + +public final class HttpUtil { + + private static final Gson GSON; + + static { + GsonBuilder builder = new GsonBuilder(); + builder.registerTypeAdapter(AuthenticateRequest.class, new AuthenticateRequest.Serializer()); + builder.registerTypeAdapter(XboxLiveAuthRequest.class, new XboxLiveAuthRequest.Serializer()); + builder.registerTypeAdapter(XboxXstsAuthRequest.class, new XboxXstsAuthRequest.Serializer()); + builder.registerTypeAdapter(XboxAuthResponse.class, new XboxAuthResponse.Deserializer()); + GSON = builder.create(); + } + + public static R performFormRequest(String url, RequestObject.Form form) throws UnreachableServiceException, InvalidResponseException { + return performWrappedFormRequest(url, form).get(); + } + + public static Response performWrappedFormRequest(String url, RequestObject.Form form) throws UnreachableServiceException { + String body = form.getFields().entrySet().stream().map(HttpUtil::urlEncode).collect(Collectors.joining("&")); + return performRequest(url, body, "application/x-www-form-urlencoded", null, form.getResponseClass()); + } + + public static R performJsonRequest(String url, RequestObject.JSON payload) throws UnreachableServiceException, InvalidResponseException { + return performWrappedJsonRequest(url, payload).get(); + } + + public static Response performWrappedJsonRequest(String url, RequestObject.JSON payload) throws UnreachableServiceException { + return performRequest(url, GSON.toJson(payload), "application/json", null, payload.getResponseClass()); + } + + public static R performGetRequest(String url, String bearer, Class responseType) throws UnreachableServiceException, InvalidResponseException { + return performWrappedGetRequest(url, bearer, responseType).get(); + } + + public static Response performWrappedGetRequest(String url, String bearer, Class responseType) throws UnreachableServiceException { + return performRequest(url, null, null, bearer, responseType); + } + + /** + * Executes the Request and parses the returned Response. + *

+ * {@link IllegalStateException} can occur in GSON when type-mismatches are encountered + */ + private static Response performRequest(String url, String body, String contentType, String token, Class responseType) throws UnreachableServiceException { + try { + HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection(); + + SSLSocketFactory socketFactory = CertWorkaround.getSocketFactory(); + if (socketFactory != null) { + connection.setSSLSocketFactory(socketFactory); + } + + connection.setConnectTimeout(10000); + connection.setReadTimeout(10000); + connection.setInstanceFollowRedirects(true); + connection.setUseCaches(false); + + connection.setRequestProperty("Accept", "application/json"); + if (token != null) { + connection.setRequestProperty("Authorization", "Bearer " + token); + } + if (body != null) { + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", contentType); + connection.setDoOutput(true); + try (OutputStream dataOut = connection.getOutputStream()) { + dataOut.write(body.getBytes(StandardCharsets.UTF_8)); + dataOut.flush(); + } + } + + connection.connect(); + + int responseCode = connection.getResponseCode(); + InputStream inputStream = responseCode < 400 ? connection.getInputStream() : connection.getErrorStream(); + + R response = null; + if (inputStream.available() > 0) { + try (Reader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) { + response = GSON.fromJson(reader, responseType); + } + } + + return new Response<>(responseCode, response); + } catch (ClassCastException | IOException e) { + throw new UnreachableServiceException("Cannot reach server", e); + } catch (IllegalStateException | JsonParseException e) { + throw new UnreachableServiceException("Server is talking nonsense", e); + } + } + + private static String urlEncode(Map.Entry entry) { + try { + return URLEncoder.encode(entry.getKey(), "UTF-8") + "=" + URLEncoder.encode(entry.getValue(), "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("UTF-8 unsupported", e); + } + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/http/InvalidResponseException.java b/src/common/java/technicianlp/reauth/authentication/http/InvalidResponseException.java new file mode 100644 index 0000000..14b4197 --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/http/InvalidResponseException.java @@ -0,0 +1,8 @@ +package technicianlp.reauth.authentication.http; + +public final class InvalidResponseException extends Exception { + + public InvalidResponseException(String message) { + super(message); + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/http/Response.java b/src/common/java/technicianlp/reauth/authentication/http/Response.java new file mode 100644 index 0000000..9e8e18d --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/http/Response.java @@ -0,0 +1,41 @@ +package technicianlp.reauth.authentication.http; + +import technicianlp.reauth.authentication.dto.ResponseObject; + +import java.net.HttpURLConnection; + +public final class Response { + + private final int statusCode; + private final R response; + + public Response(int statusCode, R response) { + this.statusCode = statusCode; + this.response = response; + } + + public final boolean isValid() { + if (!(200 <= this.statusCode && this.statusCode < 300)) { + return false; + } + if (this.response != null) { + return this.response.isValid(); + } else { + return this.statusCode == HttpURLConnection.HTTP_NO_CONTENT; + } + } + + public final R get() throws InvalidResponseException { + if (this.isValid()) { + return this.response; + } else if (this.response != null) { + throw new InvalidResponseException("Error received with code " + this.statusCode + ": " + this.response.getError()); + } else { + throw new InvalidResponseException("Received error code " + this.statusCode); + } + } + + public final R getUnchecked() { + return this.response; + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/http/UnreachableServiceException.java b/src/common/java/technicianlp/reauth/authentication/http/UnreachableServiceException.java new file mode 100644 index 0000000..fea0c91 --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/http/UnreachableServiceException.java @@ -0,0 +1,11 @@ +package technicianlp.reauth.authentication.http; + +/** + * Wrapper Exception for use when services cannot be reached or answers cannot be decoded + */ +public final class UnreachableServiceException extends Exception { + + UnreachableServiceException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/http/server/AuthenticationCodeServer.java b/src/common/java/technicianlp/reauth/authentication/http/server/AuthenticationCodeServer.java new file mode 100644 index 0000000..7692ff1 --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/http/server/AuthenticationCodeServer.java @@ -0,0 +1,65 @@ +package technicianlp.reauth.authentication.http.server; + +import com.sun.net.httpserver.HttpServer; +import technicianlp.reauth.ReAuth; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +/** + * Authentication with Microsoft requires a webserver on localhost to receive the issued authentication code. + *
+ * Class provides encapsulation of the jdk-builtin {@link HttpServer}. + * The API provided by the {@link com.sun.net.httpserver} package is an official and documented API since Java 6. + * While the API should be available in every major java distribution, it is an optional API and may therefore be missing in rare cases. + * This is denoted by both the {@link jdk.Exported} Annotation and online documentation. + *
+ * The HttpServer is started on the supplied Executor. + * After the code has been received the future is completed synchronously and the server automatically {@link #stop(boolean) stops} asynchronously. + */ +public final class AuthenticationCodeServer { + + private final Consumer stopServer; + + private boolean running = true; + + public AuthenticationCodeServer(int port, String loginUrl, CompletableFuture codeFuture, Executor executor) throws IOException, NoClassDefFoundError { + InetSocketAddress localAddress = new InetSocketAddress(InetAddress.getLoopbackAddress(), port); + HttpServer server = HttpServer.create(localAddress, 0); + server.setExecutor(executor); + + PageWriter writer = new PageWriter(loginUrl); + server.createContext("/", new CodeHandler(writer, codeFuture)); + server.createContext("/res/", new ResourcesHandler(writer)); + + codeFuture.whenCompleteAsync((v, exception) -> this.stop(exception != null), executor); + + ReAuth.log.info("Starting local endpoint"); + server.start(); + ReAuth.log.info("Started local endpoint"); + + this.stopServer = (immediate) -> server.stop(immediate ? 0 : 1); + } + + public final synchronized void stop(boolean immediate) { + if (this.running) { + this.running = false; + if (!immediate) { + ReAuth.log.info("About to stop local endpoint"); + try { + TimeUnit.SECONDS.sleep(1); + } catch (InterruptedException exception) { + ReAuth.log.warn("Interrupted while waiting to stop local endpoint", exception); + } + } + ReAuth.log.info("Stopping local endpoint"); + this.stopServer.accept(immediate); + ReAuth.log.info("Stopped local endpoint"); + } + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/http/server/CodeHandler.java b/src/common/java/technicianlp/reauth/authentication/http/server/CodeHandler.java new file mode 100644 index 0000000..59df3a9 --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/http/server/CodeHandler.java @@ -0,0 +1,95 @@ +package technicianlp.reauth.authentication.http.server; + +import com.sun.net.httpserver.HttpExchange; +import org.apache.commons.io.IOUtils; +import technicianlp.reauth.ReAuth; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +final class CodeHandler extends Handler { + + private final CompletableFuture codeFuture; + + CodeHandler(PageWriter writer, CompletableFuture codeFuture) { + super(writer); + this.codeFuture = codeFuture; + } + + @Override + public final void handle(HttpExchange exchange) throws IOException { + try { + + String method = exchange.getRequestMethod().toUpperCase(Locale.ROOT); + Response response; + if ("POST".equals(method)) { + response = this.handlePostRequest(exchange); + } else if ("HEAD".equals(method) || "GET".equals(method)) { + response = new Response(HttpStatus.Method_Not_Allowed).setHeader("Allow", "POST"); + } else { + response = new Response(HttpStatus.Not_Implemented); + } + + this.sendResponse(exchange, response); + } catch (Exception exception) { + ReAuth.log.error("Exception while answering request", exception); + throw exception; + } + } + + private Response handlePostRequest(HttpExchange exchange) throws IOException { + String contentType = exchange.getRequestHeaders().getFirst("Content-Type"); + if (!contentType.startsWith("application/x-www-form-urlencoded")) { + return new Response(HttpStatus.Unsupported_Media_Type); + } + + String body = IOUtils.toString(exchange.getRequestBody(), StandardCharsets.UTF_8); + Map formFields = (this.parseFormFields(body)); + + if (formFields.containsKey("code")) { + ReAuth.log.info("Received Microsoft Authentication Code"); + this.codeFuture.complete(formFields.get("code")); + return new Response(HttpStatus.OK).setContent(CONTENT_TYPE_HTML, this.pageWriter.createSuccessResponsePage()); + } else { + String error = formFields.getOrDefault("error", "unknown"); + ReAuth.log.error("Received Error from Microsoft Authentication: " + error); + return new Response(HttpStatus.Bad_Request).setContent(CONTENT_TYPE_HTML, this.pageWriter.createErrorResponsePage(error)); + } + } + + /** + * Decodes the Contents of a application/x-www-form-urlencoded request. + * Duplicate field names are discarded. + */ + private Map parseFormFields(String formUrlEncoded) { + Map formFields = new HashMap<>(); + String[] fields = formUrlEncoded.split("&"); + + try { + for (String field : fields) { + if (field.isEmpty()) { + continue; + } + String key = field; + String value = ""; + + int delimiter = field.indexOf('='); + if (delimiter != -1) { + key = field.substring(0, delimiter); + value = field.substring(delimiter + 1); + } + + formFields.putIfAbsent(URLDecoder.decode(key, "UTF-8"), URLDecoder.decode(value, "UTF-8")); + } + } catch (UnsupportedEncodingException exception) { + throw new RuntimeException("UTF-8 unsupported", exception); + } + return formFields; + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/http/server/Handler.java b/src/common/java/technicianlp/reauth/authentication/http/server/Handler.java new file mode 100644 index 0000000..0251925 --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/http/server/Handler.java @@ -0,0 +1,40 @@ +package technicianlp.reauth.authentication.http.server; + +import com.sun.net.httpserver.Headers; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; + +abstract class Handler implements HttpHandler { + + static final int BUFFER_SIZE = 8192; + static final String CONTENT_TYPE_HTML = "text/html; charset=UTF-8"; + + final PageWriter pageWriter; + + Handler(PageWriter pageWriter) { + this.pageWriter = pageWriter; + } + + final void sendResponse(HttpExchange exchange, Response response) throws IOException { + Headers responseHeaders = exchange.getResponseHeaders(); + response.getHeaders().forEach(responseHeaders::set); + + if (exchange.getRequestMethod().equals("HEAD")) { + exchange.sendResponseHeaders(response.getHttpStatus().code, -1); + } else { + if (!response.hasContent()) { + ByteBuffer buffer = this.pageWriter.createHttpErrorResponsePage(response.getHttpStatus()); + response.setContent(CONTENT_TYPE_HTML, buffer); + } + ByteBuffer pageContent = response.getPageContent(); + pageContent.flip(); + exchange.sendResponseHeaders(response.getHttpStatus().code, pageContent.limit()); + Channels.newChannel(exchange.getResponseBody()).write(pageContent); + } + exchange.close(); + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/http/server/HttpStatus.java b/src/common/java/technicianlp/reauth/authentication/http/server/HttpStatus.java new file mode 100644 index 0000000..68121ee --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/http/server/HttpStatus.java @@ -0,0 +1,17 @@ +package technicianlp.reauth.authentication.http.server; + +enum HttpStatus { + + OK(200), + Bad_Request(400), + Not_Found(404), + Method_Not_Allowed(405), + Unsupported_Media_Type(415), + Not_Implemented(501); + + final int code; + + HttpStatus(int code) { + this.code = code; + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/http/server/PageWriter.java b/src/common/java/technicianlp/reauth/authentication/http/server/PageWriter.java new file mode 100644 index 0000000..ffbb397 --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/http/server/PageWriter.java @@ -0,0 +1,81 @@ +package technicianlp.reauth.authentication.http.server; + +import org.apache.commons.io.IOUtils; +import technicianlp.reauth.ReAuth; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +final class PageWriter { + + private static final int BUFFER_SIZE = 8192; + + private final String loginUrl; + + PageWriter(String loginUrl) { + this.loginUrl = loginUrl; + } + + final ByteBuffer createSuccessResponsePage() throws IOException { + String successMessage = this.formatAndEscape("reauth.msauth.code.success"); + String closeMessage = this.formatAndEscape("reauth.msauth.code.success.close"); + return this.createPage(successMessage, closeMessage); + } + + final ByteBuffer createErrorResponsePage(String errorCode) throws IOException { + String errorMessage = this.formatAndEscape(this.getErrorMessage(errorCode)); + String retryMessage = this.createLink(this.formatAndEscape("reauth.msauth.code.retry"), this.loginUrl); + return this.createPage(errorMessage, retryMessage); + } + + final ByteBuffer createHttpErrorResponsePage(HttpStatus error) throws IOException { + String errorMessage = this.formatAndEscape("reauth.msauth.code.error.http" + error.code); + String retryMessage = this.createLink(this.formatAndEscape("reauth.msauth.code.retry"), this.loginUrl); + return this.createPage(errorMessage, retryMessage); + } + + private ByteBuffer createPage(String text1, String text2) throws IOException { + try (InputStream is = AuthenticationCodeServer.class.getResourceAsStream("/resources/reauth/reauth.html")) { + if (is != null) { + String page = IOUtils.toString(is, StandardCharsets.UTF_8); + page = page.replace("$text1", text1).replace("$text2", text2); + ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE); + buffer.put(page.getBytes(StandardCharsets.UTF_8)); + return buffer; + } else { + throw new FileNotFoundException("Resource /resources/reauth/reauth.html is unavailable"); + } + } + } + + private String createLink(String content, String href) { + return "" + content + ""; + } + + private String getErrorMessage(String authError) { + String type = "unknown"; + switch (authError) { + case "access_denied": + type = "cancelled"; + break; + case "server_error": + case "temporarily_unavailable": + type = "server"; + break; + } + return "reauth.msauth.code.fail." + type; + } + + private String formatAndEscape(String key, Object... arguments) { + String text = ReAuth.i18n.apply(key, arguments); + text = text.replaceAll("&", "&"); + text = text.replaceAll("<", "<"); + text = text.replaceAll(">", ">"); + text = text.replaceAll("\"", "&qout;"); + text = text.replaceAll("'", "'"); + return text; + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/http/server/ResourcesHandler.java b/src/common/java/technicianlp/reauth/authentication/http/server/ResourcesHandler.java new file mode 100644 index 0000000..e4d819e --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/http/server/ResourcesHandler.java @@ -0,0 +1,65 @@ +package technicianlp.reauth.authentication.http.server; + +import com.sun.net.httpserver.HttpExchange; +import technicianlp.reauth.ReAuth; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.util.Locale; + +final class ResourcesHandler extends Handler { + + ResourcesHandler(PageWriter writer) { + super(writer); + } + + @Override + public final void handle(HttpExchange exchange) throws IOException { + try { + String method = exchange.getRequestMethod().toUpperCase(Locale.ROOT); + + Response response; + if (method.equals("GET") || method.equals("HEAD")) { + response = this.handleResourceGet(exchange.getRequestURI().getPath()); + } else if (method.equals("POST")) { + response = new Response(HttpStatus.Method_Not_Allowed).setHeader("Allow", "GET, HEAD"); + } else { + response = new Response(HttpStatus.Not_Implemented); + } + + this.sendResponse(exchange, response); + } catch (Exception exception) { + ReAuth.log.error("Exception while answering request", exception); + throw exception; + } + } + + private Response handleResourceGet(String path) throws IOException { + String contentType = null; + String resource = null; + if ("/res/icon.png".equals(path)) { + contentType = "image/png"; + resource = "/resources/reauth/icon.png"; + } else if ("/res/logo.png".equals(path)) { + contentType = "image/png"; + resource = "/resources/reauth/logo.png"; + } + + if (resource != null) { + try (InputStream is = AuthenticationCodeServer.class.getResourceAsStream(resource)) { + if (is != null) { + ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE * 4); + Channels.newChannel(is).read(buffer); + return new Response(HttpStatus.OK).setContent(contentType, buffer); + } else { + throw new FileNotFoundException("Resource " + resource + " is unavailable"); + } + } + } else { + return new Response(HttpStatus.Not_Found); + } + } +} diff --git a/src/common/java/technicianlp/reauth/authentication/http/server/Response.java b/src/common/java/technicianlp/reauth/authentication/http/server/Response.java new file mode 100644 index 0000000..ae041e5 --- /dev/null +++ b/src/common/java/technicianlp/reauth/authentication/http/server/Response.java @@ -0,0 +1,44 @@ +package technicianlp.reauth.authentication.http.server; + +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; + +final class Response { + + private final HttpStatus httpStatus; + private final Map headers; + private ByteBuffer pageContent; + + Response(HttpStatus httpStatus) { + this.httpStatus = httpStatus; + this.headers = new HashMap<>(); + } + + final Response setContent(String contentType, ByteBuffer content) { + this.setHeader("Content-Type", contentType); + this.pageContent = content; + return this; + } + + final Response setHeader(String name, String value) { + this.getHeaders().put(name, value); + return this; + } + + final HttpStatus getHttpStatus() { + return this.httpStatus; + } + + final boolean hasContent() { + return this.pageContent != null; + } + + final ByteBuffer getPageContent() { + return this.pageContent; + } + + final Map getHeaders() { + return this.headers; + } +} diff --git a/src/common/java/technicianlp/reauth/configuration/ProfileBuilder.java b/src/common/java/technicianlp/reauth/configuration/ProfileBuilder.java new file mode 100644 index 0000000..50b1cbb --- /dev/null +++ b/src/common/java/technicianlp/reauth/configuration/ProfileBuilder.java @@ -0,0 +1,38 @@ +package technicianlp.reauth.configuration; + +import technicianlp.reauth.ReAuth; +import technicianlp.reauth.authentication.SessionData; +import technicianlp.reauth.authentication.flows.Tokens; +import technicianlp.reauth.crypto.ProfileEncryption; + +import java.util.HashMap; +import java.util.Map; + +public final class ProfileBuilder { + + private final Map profile = new HashMap<>(); + private final ProfileEncryption encryption; + + public ProfileBuilder(SessionData session, ProfileEncryption encryption) { + this.encryption = encryption; + this.profile.put(ProfileConstants.NAME, session.username); + this.profile.put(ProfileConstants.UUID, session.uuid); + encryption.saveToProfile(this.profile); + } + + public final Profile buildMicrosoft(Tokens tokens) { + this.profile.put(ProfileConstants.PROFILE_TYPE, ProfileConstants.PROFILE_TYPE_MICROSOFT); + this.profile.put(ProfileConstants.XBL_TOKEN, this.encryption.encryptFieldOne(tokens.getXblToken())); + this.profile.put(ProfileConstants.REFRESH_TOKEN, this.encryption.encryptFieldTwo(tokens.getRefreshToken())); + + return ReAuth.profiles.createProfile(this.profile); + } + + public final Profile buildMojang(String username, String password) { + this.profile.put(ProfileConstants.PROFILE_TYPE, ProfileConstants.PROFILE_TYPE_MOJANG); + this.profile.put(ProfileConstants.USERNAME, this.encryption.encryptFieldOne(username)); + this.profile.put(ProfileConstants.PASSWORD, this.encryption.encryptFieldTwo(password)); + + return ReAuth.profiles.createProfile(this.profile); + } +} diff --git a/src/common/java/technicianlp/reauth/configuration/ProfileConstants.java b/src/common/java/technicianlp/reauth/configuration/ProfileConstants.java new file mode 100644 index 0000000..78703ac --- /dev/null +++ b/src/common/java/technicianlp/reauth/configuration/ProfileConstants.java @@ -0,0 +1,71 @@ +package technicianlp.reauth.configuration; + +import com.google.common.collect.ImmutableList; + +import java.util.ArrayList; +import java.util.List; + +public final class ProfileConstants { + + // profile type + public static final String PROFILE_TYPE = "type"; + public static final String PROFILE_TYPE_NONE = "none"; + public static final String PROFILE_TYPE_MOJANG = "mojang"; + public static final String PROFILE_TYPE_MICROSOFT = "microsoft"; + + // common fields + public static final String NAME = "name"; + public static final String UUID = "uuid"; + + // mojang profiles + public static final String USERNAME = "username"; + public static final String PASSWORD = "password"; + + // microsoft profiles + public static final String REFRESH_TOKEN = "refresh-token"; + public static final String XBL_TOKEN = "xbl-token"; + + // encryption + public static final String KEY = "key"; + public static final String KEY_AUTO = "auto"; + public static final String KEY_NONE = "none"; + public static final String SALT = "salt"; + + /** + * Used to provide consistent ordering for config entries. + */ + private static final List propertyOrder = ImmutableList.of( + ProfileConstants.PROFILE_TYPE, + ProfileConstants.NAME, + ProfileConstants.UUID, + ProfileConstants.USERNAME, + ProfileConstants.XBL_TOKEN, + ProfileConstants.PASSWORD, + ProfileConstants.REFRESH_TOKEN, + ProfileConstants.KEY, + ProfileConstants.SALT); + + static List getOrderedProfileKeys() { + return new ArrayList<>(propertyOrder); + } + + static int compareProfileKeys(String key1, String key2) { + if (key1.equals(key2)) { + return 0; + } else { + if (propertyOrder.contains(key1)) { + if (propertyOrder.contains(key2)) { + return Integer.compare(propertyOrder.indexOf(key1), propertyOrder.indexOf(key2)); + } else { + return -1; + } + } else { + if (propertyOrder.contains(key2)) { + return 1; + } else { + return key1.compareTo(key2); + } + } + } + } +} diff --git a/src/common/java/technicianlp/reauth/crypto/Crypto.java b/src/common/java/technicianlp/reauth/crypto/Crypto.java new file mode 100644 index 0000000..a61da5f --- /dev/null +++ b/src/common/java/technicianlp/reauth/crypto/Crypto.java @@ -0,0 +1,40 @@ +package technicianlp.reauth.crypto; + +import technicianlp.reauth.configuration.Profile; +import technicianlp.reauth.configuration.ProfileConstants; + +import java.security.SecureRandom; + +public final class Crypto { + + private static String configPath = ""; + + public static ProfileEncryption getProfileEncryption(Profile profile) { + switch (profile.getValue(ProfileConstants.KEY)) { + case ProfileConstants.KEY_AUTO: + return new EncryptionAutomatic(configPath, profile.getValue(ProfileConstants.SALT)); + case ProfileConstants.KEY_NONE: + return new EncryptionNone(); + default: + throw new IllegalArgumentException("Unknown Encryption Type"); + } + } + + public static ProfileEncryption newEncryption() { + return new EncryptionAutomatic(configPath); + } + + public static byte[] randomBytes(int length) { + byte[] salt = new byte[length]; + new SecureRandom().nextBytes(salt); + return salt; + } + + public static PkceChallenge createPkceChallenge() { + return new PkceChallenge(); + } + + public static void updateConfigPath(String configPath) { + Crypto.configPath = configPath; + } +} diff --git a/src/common/java/technicianlp/reauth/crypto/CryptoException.java b/src/common/java/technicianlp/reauth/crypto/CryptoException.java new file mode 100644 index 0000000..809ad5b --- /dev/null +++ b/src/common/java/technicianlp/reauth/crypto/CryptoException.java @@ -0,0 +1,8 @@ +package technicianlp.reauth.crypto; + +public final class CryptoException extends RuntimeException { + + public CryptoException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/common/java/technicianlp/reauth/crypto/EncryptionAutomatic.java b/src/common/java/technicianlp/reauth/crypto/EncryptionAutomatic.java new file mode 100644 index 0000000..9978038 --- /dev/null +++ b/src/common/java/technicianlp/reauth/crypto/EncryptionAutomatic.java @@ -0,0 +1,112 @@ +package technicianlp.reauth.crypto; + +import technicianlp.reauth.configuration.Profile; +import technicianlp.reauth.configuration.ProfileConstants; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.util.Base64; +import java.util.Map; + +final class EncryptionAutomatic implements ProfileEncryption { + + private static final int PBE_ROUNDS = 250_000; + private static final int IV1_OFFSET = 32; + private static final int IV2_OFFSET = 48; + + private final byte[] keyData; + + private final String path; + private final byte[] salt; + + public EncryptionAutomatic(String path) { + this(path, Crypto.randomBytes(16)); + } + + EncryptionAutomatic(String path, String salt) throws CryptoException { + this(path, Base64.getDecoder().decode(salt)); + } + + EncryptionAutomatic(String path, byte[] salt) throws CryptoException { + try { + SecretKeyFactory pbkdf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512"); + SecretKey key = pbkdf.generateSecret(new PBEKeySpec(path.toCharArray(), salt, PBE_ROUNDS, 512)); + this.keyData = key.getEncoded(); + } catch (InvalidKeySpecException | NoSuchAlgorithmException e) { + throw new CryptoException("Failed to derive encryption key", e); + } + this.path = path; + this.salt = salt; + } + + @Override + public final String decryptFieldOne(String encrypted) throws CryptoException { + return this.decrypt(encrypted, IV1_OFFSET); + } + + @Override + public final String decryptFieldTwo(String encrypted) throws CryptoException { + return this.decrypt(encrypted, IV2_OFFSET); + } + + private String decrypt(String encrypted, int ivOffset) throws CryptoException { + try { + byte[] raw = Base64.getDecoder().decode(encrypted); + byte[] dec = this.crypt(raw, Cipher.DECRYPT_MODE, this.keyData, ivOffset); + return new String(dec, StandardCharsets.UTF_8); + } catch (GeneralSecurityException e) { + throw new CryptoException("Decryption failed", e); + } + } + + @Override + public final String encryptFieldOne(String value) throws CryptoException { + return this.encrypt(value, IV1_OFFSET); + } + + @Override + public final String encryptFieldTwo(String value) throws CryptoException { + return this.encrypt(value, IV2_OFFSET); + } + + private String encrypt(String value, int ivOffset) throws CryptoException { + try { + byte[] raw = value.getBytes(StandardCharsets.UTF_8); + byte[] enc = this.crypt(raw, Cipher.ENCRYPT_MODE, this.keyData, ivOffset); + return Base64.getEncoder().encodeToString(enc); + } catch (GeneralSecurityException e) { + throw new CryptoException("Encryption failed", e); + } + } + + /** + * Encrypt or decrypt the supplied data with the given Key and IV + */ + private byte[] crypt(byte[] data, int mode, byte[] keyData, int ivOffset) throws GeneralSecurityException { + SecretKeySpec secretKey = new SecretKeySpec(keyData, 0, 32, "AES"); + IvParameterSpec ivParameterSpec = new IvParameterSpec(keyData, ivOffset, 16); + + Cipher aes = Cipher.getInstance("AES/CBC/PKCS5Padding"); + aes.init(mode, secretKey, ivParameterSpec); + return aes.doFinal(data); + } + + @Override + public final void saveToProfile(Map profile) { + profile.put(ProfileConstants.KEY, ProfileConstants.KEY_AUTO); + profile.put(ProfileConstants.SALT, Base64.getEncoder().encodeToString(this.salt)); + } + + @Override + public final ProfileEncryption randomizedCopy() { + return new EncryptionAutomatic(this.path); + } +} diff --git a/src/common/java/technicianlp/reauth/crypto/EncryptionNone.java b/src/common/java/technicianlp/reauth/crypto/EncryptionNone.java new file mode 100644 index 0000000..1c4327c --- /dev/null +++ b/src/common/java/technicianlp/reauth/crypto/EncryptionNone.java @@ -0,0 +1,39 @@ +package technicianlp.reauth.crypto; + +import technicianlp.reauth.configuration.Profile; +import technicianlp.reauth.configuration.ProfileConstants; + +import java.util.Map; + +final class EncryptionNone implements ProfileEncryption { + + @Override + public final String decryptFieldOne(String value) { + return value; + } + + @Override + public final String decryptFieldTwo(String value) { + return value; + } + + @Override + public final String encryptFieldOne(String value) { + return value; + } + + @Override + public final String encryptFieldTwo(String value) { + return value; + } + + @Override + public final void saveToProfile(Map profile) { + profile.put(ProfileConstants.KEY, ProfileConstants.KEY_NONE); + } + + @Override + public final ProfileEncryption randomizedCopy() { + return new EncryptionNone(); + } +} diff --git a/src/common/java/technicianlp/reauth/crypto/PkceChallenge.java b/src/common/java/technicianlp/reauth/crypto/PkceChallenge.java new file mode 100644 index 0000000..017d077 --- /dev/null +++ b/src/common/java/technicianlp/reauth/crypto/PkceChallenge.java @@ -0,0 +1,40 @@ +package technicianlp.reauth.crypto; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Base64; + +/** + * Utility for generating a PKCE Verifier and Challenge as specified by RFC7636 + */ +public final class PkceChallenge { + + final String challenge; + final String verifier; + + PkceChallenge() { + Base64.Encoder base64url = Base64.getUrlEncoder().withoutPadding(); + + byte[] verifierCode = new byte[32]; + new SecureRandom().nextBytes(verifierCode); + this.verifier = base64url.encodeToString(verifierCode); + + try { + MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); + byte[] challengeCode = sha256.digest(this.verifier.getBytes(StandardCharsets.US_ASCII)); + this.challenge = base64url.encodeToString(challengeCode); + } catch (NoSuchAlgorithmException e) { + throw new CryptoException("SHA-256 unavailable", e); + } + } + + public final String getChallenge() { + return this.challenge; + } + + public final String getVerifier() { + return this.verifier; + } +} diff --git a/src/common/java/technicianlp/reauth/crypto/ProfileEncryption.java b/src/common/java/technicianlp/reauth/crypto/ProfileEncryption.java new file mode 100644 index 0000000..b88a4bc --- /dev/null +++ b/src/common/java/technicianlp/reauth/crypto/ProfileEncryption.java @@ -0,0 +1,21 @@ +package technicianlp.reauth.crypto; + +import java.util.Map; + +public interface ProfileEncryption { + + String decryptFieldOne(String encrypted) throws CryptoException; + + String decryptFieldTwo(String encrypted) throws CryptoException; + + String encryptFieldOne(String value) throws CryptoException; + + String encryptFieldTwo(String value) throws CryptoException; + + void saveToProfile(Map profile); + + /** + * creates a new Instance of this {@link ProfileEncryption} with new, random parameters + */ + ProfileEncryption randomizedCopy(); +} diff --git a/src/common/java/technicianlp/reauth/mojangfix/CertWorkaround.java b/src/common/java/technicianlp/reauth/mojangfix/CertWorkaround.java new file mode 100644 index 0000000..2faba9b --- /dev/null +++ b/src/common/java/technicianlp/reauth/mojangfix/CertWorkaround.java @@ -0,0 +1,212 @@ +package technicianlp.reauth.mojangfix; + +import technicianlp.reauth.ReAuth; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLContextSpi; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509ExtendedTrustManager; +import javax.net.ssl.X509TrustManager; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.Socket; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Deque; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +public final class CertWorkaround { + + private static final String MICROSOFT2017 = "microsoftrsarootcertificateauthority2017"; + private static final String AMAZON1 = "amazonrootca1"; + private static final String DIGICERT2 = "digicertglobalrootg2"; + + private static SSLSocketFactory socketFactory = null; + + public static SSLSocketFactory getSocketFactory() { + return socketFactory; + } + + /** + * The default truststore is missing some CA-Certificates required during authentication with Microsoft/XBox/Mojang + * because Mojang for some insane reason ships the 7 years old Java 8 Update 51 (July 14, 2015). + *

+ * The following Certificates are installed if they are missing: + * - Microsoft RSA Root Certificate Authority 2017 + * - DigiCert Global Root G2 + * - Amazon Root CA 1 + */ + static void checkCertificates() { + try { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + X509Certificate microsoft2017 = loadCertificate(cf, MICROSOFT2017); + X509Certificate amazon1 = loadCertificate(cf, AMAZON1); + X509Certificate digicert2 = loadCertificate(cf, DIGICERT2); + + TrustManagerFactory defaultTrust = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + defaultTrust.init((KeyStore) null); + + List trustedCerts = getTrustedCerts(defaultTrust); + Map missingCerts = new HashMap<>(); + if (!trustedCerts.contains(microsoft2017)) { + missingCerts.put(MICROSOFT2017, microsoft2017); + } + if (!trustedCerts.contains(amazon1)) { + missingCerts.put(AMAZON1, amazon1); + } + if (!trustedCerts.contains(digicert2)) { + missingCerts.put(DIGICERT2, digicert2); + } + + if (missingCerts.isEmpty()) { + // no additional certificates required + return; + } else { + ReAuth.log.warn("Some Certificates required for authentication are untrusted by default"); + } + + X509ExtendedTrustManager defaultTrustManager = findX509ExtendedTrustManager(defaultTrust); + X509ExtendedTrustManager missingTrustManager = findX509ExtendedTrustManager(createTrustFactory(missingCerts)); + X509ExtendedTrustManager combinedTrustManager = new CombinedX509ExtendedTrustManager(defaultTrustManager, missingTrustManager); + + SSLContext context = SSLContext.getInstance("TLS"); + context.init(null, new X509ExtendedTrustManager[]{combinedTrustManager}, null); + + CertWorkaround.socketFactory = context.getSocketFactory(); + ReAuth.log.info("Successfully built SSLSocketFactory with required Certificates"); + } catch (GeneralSecurityException | IOException e) { + throw new RuntimeException(e); + } + } + + private static X509Certificate loadCertificate(CertificateFactory certFactory, String name) throws CertificateException, IOException { + try (InputStream is = CertWorkaround.class.getResourceAsStream("/resources/reauth/certs/" + name + ".pem")) { + if (is != null) { + return (X509Certificate) certFactory.generateCertificate(is); + } else { + throw new FileNotFoundException("Certificate " + name + " is unavailable"); + } + } + } + + private static List getTrustedCerts(TrustManagerFactory trustManagerFactory) { + X509ExtendedTrustManager trustManager = findX509ExtendedTrustManager(trustManagerFactory); + if (trustManager != null) { + return new ArrayList<>(Arrays.asList(trustManager.getAcceptedIssuers())); + } else { + return new ArrayList<>(); + } + } + + private static TrustManagerFactory createTrustFactory(Map certificates) throws KeyStoreException, CertificateException, IOException, NoSuchAlgorithmException { + KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); + ks.load(null); + for (Map.Entry certificate : certificates.entrySet()) { + ReAuth.log.info("Adding Certificate {} to trust", certificate.getKey()); + ks.setCertificateEntry(certificate.getKey(), certificate.getValue()); + } + TrustManagerFactory trustFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustFactory.init(ks); + return trustFactory; + } + + private static X509ExtendedTrustManager findX509ExtendedTrustManager(TrustManagerFactory trustManagerFactory) { + return Arrays.stream(trustManagerFactory.getTrustManagers()) + .filter(X509ExtendedTrustManager.class::isInstance) + .map(X509ExtendedTrustManager.class::cast) + .findFirst() + .orElse(null); + } + + /** + * required in order to combine multiple TrustManagers. + * The default Implementation of {@link SSLContextSpi} only considers the first {@link X509TrustManager} + */ + private static final class CombinedX509ExtendedTrustManager extends X509ExtendedTrustManager { + + private final List trustManagers; + + private CombinedX509ExtendedTrustManager(X509ExtendedTrustManager... trustManagers) { + this.trustManagers = new ArrayList<>(Arrays.asList(trustManagers)); + if (this.trustManagers.isEmpty()) { + throw new IllegalArgumentException("At least one X509ExtendedTrustManager is required"); + } + if (this.trustManagers.contains(null)) { + throw new IllegalArgumentException("X509ExtendedTrustManager cannot be null"); + } + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { + this.check(tm -> tm.checkClientTrusted(chain, authType, socket)); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { + this.check(tm -> tm.checkServerTrusted(chain, authType, socket)); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException { + this.check(tm -> tm.checkClientTrusted(chain, authType, engine)); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException { + this.check(tm -> tm.checkServerTrusted(chain, authType, engine)); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + this.check(tm -> tm.checkClientTrusted(chain, authType)); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + this.check(tm -> tm.checkServerTrusted(chain, authType)); + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return this.trustManagers.stream().map(X509TrustManager::getAcceptedIssuers).flatMap(Arrays::stream).toArray(X509Certificate[]::new); + } + + /** + * checks if any of the trustManagers accepts the supplied operation. + * + * @throws CertificateException if all trustManagers refuse the operation + */ + private void check(CertificateCheckConsumer checkFunction) throws CertificateException { + Deque exceptions = new LinkedList<>(); + for (X509ExtendedTrustManager trustManager : this.trustManagers) { + try { + checkFunction.check(trustManager); + return; // accepted by TrustManager + } catch (CertificateException e) { + exceptions.add(e); + } + } + CertificateException last = exceptions.removeLast(); + exceptions.forEach(last::addSuppressed); + throw last; + } + } + + @FunctionalInterface + private interface CertificateCheckConsumer { + void check(X509ExtendedTrustManager trustManager) throws CertificateException; + } +} diff --git a/src/common/java/technicianlp/reauth/mojangfix/JceWorkaround.java b/src/common/java/technicianlp/reauth/mojangfix/JceWorkaround.java new file mode 100644 index 0000000..9ad0ef6 --- /dev/null +++ b/src/common/java/technicianlp/reauth/mojangfix/JceWorkaround.java @@ -0,0 +1,78 @@ +package technicianlp.reauth.mojangfix; + +import technicianlp.reauth.ReAuth; +import technicianlp.reauth.crypto.CryptoException; +import technicianlp.reauth.util.ReflectionUtils; + +import javax.crypto.Cipher; +import java.lang.reflect.Field; +import java.security.NoSuchAlgorithmException; +import java.security.PermissionCollection; +import java.util.Map; + +final class JceWorkaround { + + /** + * check if CryptoAllPermission is in effect and try to remove Jce restrictions otherwise + */ + static void ensureUnlimitedCryptography() { + try { + if (Cipher.getMaxAllowedKeyLength("AES") != Integer.MAX_VALUE) { + ReAuth.log.warn("Cryptography is restricted in this Java installation"); + if (!MojangJavaFix.java8) { + ReAuth.log.warn("Cryptography is likely deliberately restricted!"); + } + removeCryptographyRestrictions(); + if (Cipher.getMaxAllowedKeyLength("AES") != Integer.MAX_VALUE) { + ReAuth.log.error("Failed to remove cryptography restriction"); + } else { + ReAuth.log.info("Cryptography restriction removed successfully"); + } + } + } catch (NoSuchAlgorithmException e) { + throw new CryptoException("AES unavailable", e); + } + } + + /** + * Java had for legal reasons limited the allowed strength of cryptographic algorithms. + * Historically to disable this restriction the so called "Java Cryptography Extension (JCE) Unlimited Strength + * Jurisdiction Policy Files" have to be installed within the JRE directory. + * Since update 151 (October 17, 2017) these restrictions can be disabled programmatically + * and have since been disabled by default in update 161 (January 16, 2018). + *

+ * Since Mojang for some insane reason ships the 7 years old update 51 (July 14, 2015), installation of the policy files + * would be necessary. Since installation of those files cannot be required of the user, a workaround has been found in + * https://stackoverflow.com/questions/1179672 and is used to disable this restriction at runtime: + *

+ * JceSecurity.isRestricted = false; + * JceSecurity.defaultPolicy.perms.clear(); + * JceSecurity.defaultPolicy.add(CryptoAllPermission.INSTANCE); + *

+ * The alternative to this workaround would have been to drop the AES key-length from 256 bits to 128 bits. + */ + private static void removeCryptographyRestrictions() { + try { + final Class jceSecurity = Class.forName("javax.crypto.JceSecurity"); + final Field isRestricted = ReflectionUtils.findField(jceSecurity, "isRestricted"); + ReflectionUtils.unlockFinalField(isRestricted); + final Field defaultPolicy = ReflectionUtils.findField(jceSecurity, "defaultPolicy"); + + final Class cryptoPermissions = Class.forName("javax.crypto.CryptoPermissions"); + final Field perms = ReflectionUtils.findField(cryptoPermissions, "perms"); + + final Class cryptoAllPermission = Class.forName("javax.crypto.CryptoAllPermission"); + final Field instance = ReflectionUtils.findField(cryptoAllPermission, "INSTANCE"); + + + ReflectionUtils.setField(isRestricted, null, false); + + final PermissionCollection permissionCollection = ReflectionUtils.getField(defaultPolicy, null); + ((Map) ReflectionUtils.getField(perms, permissionCollection)).clear(); + + permissionCollection.add(ReflectionUtils.getField(instance, null)); + } catch (final Exception e) { + ReAuth.log.error("Exception removing cryptography restrictions", e); + } + } +} diff --git a/src/common/java/technicianlp/reauth/mojangfix/MojangJavaFix.java b/src/common/java/technicianlp/reauth/mojangfix/MojangJavaFix.java new file mode 100644 index 0000000..bc97720 --- /dev/null +++ b/src/common/java/technicianlp/reauth/mojangfix/MojangJavaFix.java @@ -0,0 +1,34 @@ +package technicianlp.reauth.mojangfix; + +import technicianlp.reauth.ReAuth; + +import java.time.LocalDate; +import java.time.Month; +import java.time.Period; +import java.time.ZoneOffset; + +public final class MojangJavaFix { + + public static final boolean mojangJava; + public static final boolean java8; + + static { + String javaVersion = System.getProperty("java.version"); + mojangJava = "1.8.0_51".equals(javaVersion); + java8 = javaVersion.startsWith("1.8"); + } + + public static void fixMojangJava() { + if (mojangJava) { + Period age = Period.between(LocalDate.of(2015, Month.JULY, 14), LocalDate.now(ZoneOffset.UTC)); + ReAuth.log.warn("+------------------------------------------------------------------+"); + ReAuth.log.warn("| Please complain to Mojang for shipping an ancient Java version |"); + ReAuth.log.warn("| Java 8 Update 51 is {} years {} months and {} days old |", age.getYears(), age.getMonths(), age.getDays()); + ReAuth.log.warn("| Updating would avoid several issues and vulnerabilities |"); + ReAuth.log.warn("+------------------------------------------------------------------+"); + } + JceWorkaround.ensureUnlimitedCryptography(); + CertWorkaround.checkCertificates(); + } + +} diff --git a/src/common/java/technicianlp/reauth/session/SessionChecker.java b/src/common/java/technicianlp/reauth/session/SessionChecker.java new file mode 100644 index 0000000..c312806 --- /dev/null +++ b/src/common/java/technicianlp/reauth/session/SessionChecker.java @@ -0,0 +1,58 @@ +package technicianlp.reauth.session; + +import technicianlp.reauth.ReAuth; +import technicianlp.reauth.authentication.YggdrasilAPI; +import technicianlp.reauth.authentication.http.UnreachableServiceException; + +import java.util.concurrent.CompletableFuture; + +public final class SessionChecker { + + /** + * Time for which the Validity gets cached (5 Minutes) + */ + private static final long cacheTime = 5 * 1000 * 60L; + + /** + * Current cached Session Validity + */ + private static SessionStatus status = SessionStatus.UNKNOWN; + private static long lastCheck = 0; + + /** + * Get the cached Validity Status of the accessToken + * Re-Validation is done if the cache expires + */ + public static SessionStatus getSessionStatus(String token, String uuid) { + if (lastCheck + cacheTime < System.currentTimeMillis()) + status = SessionStatus.UNKNOWN; + + if (status == SessionStatus.UNKNOWN) { + status = SessionStatus.REFRESHING; + lastCheck = System.currentTimeMillis(); + + CompletableFuture tokenFuture = CompletableFuture.completedFuture(token); + CompletableFuture uuidFuture = CompletableFuture.completedFuture(uuid); + tokenFuture.thenCombineAsync(uuidFuture, SessionChecker::getSessionStatus0, ReAuth.executor) + .thenAccept(SessionChecker::setStatus); + } + return status; + } + + public static void invalidate() { + status = SessionStatus.UNKNOWN; + } + + private static SessionStatus getSessionStatus0(String accessToken, String uuid) { + try { + return YggdrasilAPI.validate(accessToken, uuid) ? SessionStatus.VALID : SessionStatus.INVALID; + } catch (UnreachableServiceException e) { + ReAuth.log.error("Failed to check session validity", e); + return SessionStatus.ERROR; + } + } + + private static void setStatus(SessionStatus newStatus) { + SessionChecker.status = newStatus; + } +} diff --git a/src/common/java/technicianlp/reauth/session/SessionStatus.java b/src/common/java/technicianlp/reauth/session/SessionStatus.java new file mode 100644 index 0000000..b81e739 --- /dev/null +++ b/src/common/java/technicianlp/reauth/session/SessionStatus.java @@ -0,0 +1,19 @@ +package technicianlp.reauth.session; + +public enum SessionStatus { + VALID("valid"), + INVALID("invalid"), + UNKNOWN("unknown"), + REFRESHING("refreshing"), + ERROR("error"); + + private final String translationKey; + + SessionStatus(String translationKey) { + this.translationKey = "reauth.status." + translationKey; + } + + public final String getTranslationKey() { + return this.translationKey; + } +} diff --git a/src/common/java/technicianlp/reauth/util/ReflectionUtils.java b/src/common/java/technicianlp/reauth/util/ReflectionUtils.java new file mode 100644 index 0000000..3a9e9a3 --- /dev/null +++ b/src/common/java/technicianlp/reauth/util/ReflectionUtils.java @@ -0,0 +1,104 @@ +package technicianlp.reauth.util; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +public final class ReflectionUtils { + + private static Method findMethodInternal(Class clz, String name, Class... parameterTypes) throws NoSuchMethodException { + Method method = clz.getDeclaredMethod(name, parameterTypes); + method.setAccessible(true); + return method; + } + + public static Method findMethod(Class clz, String name, Class... parameterTypes) { + try { + return findMethodInternal(clz, name, parameterTypes); + } catch (ReflectiveOperationException exception) { + throw new UncheckedReflectiveOperationException("Unable to find Method: " + name, exception); + } + } + + public static Method findObfuscatedMethod(Class clz, String obfName, String name, Class... parameterTypes) { + try { + return findMethodInternal(clz, obfName, parameterTypes); + } catch (NoSuchMethodException suppressed) { + try { + return findMethodInternal(clz, name, parameterTypes); + } catch (NoSuchMethodException exception) { + exception.addSuppressed(suppressed); + throw new UncheckedReflectiveOperationException("Unable to find Obfuscated Method: " + name, exception); + } + } + } + + public static T callMethod(Method method, Object target, Object... args) { + try { + //noinspection unchecked + return (T) method.invoke(target, args); + } catch (ReflectiveOperationException exception) { + throw new UncheckedReflectiveOperationException("Failed reflective Method call", exception); + } + } + + private static Field findFieldInternal(Class clz, String name) throws NoSuchFieldException { + Field field = clz.getDeclaredField(name); + field.setAccessible(true); + return field; + } + + public static Field findField(Class clz, String name) { + try { + return findFieldInternal(clz, name); + } catch (ReflectiveOperationException exception) { + throw new UncheckedReflectiveOperationException("Unable to find Field: " + name, exception); + } + } + + public static Field findObfuscatedField(Class clz, String obfName, String name) { + try { + return findFieldInternal(clz, obfName); + } catch (NoSuchFieldException suppressed) { + try { + return findFieldInternal(clz, name); + } catch (NoSuchFieldException exception) { + exception.addSuppressed(suppressed); + throw new UncheckedReflectiveOperationException("Unable to find Obfuscated Field: " + name, exception); + } + } + } + + public static void unlockFinalField(Field field) { + try { + Field fieldModifiers = findField(Field.class, "modifiers"); + fieldModifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL); + } catch (ReflectiveOperationException exception) { + throw new UncheckedReflectiveOperationException("Unable to unlock final field", exception); + } + } + + public static void setField(Field field, Object target, Object value) { + try { + field.set(target, value); + } catch (ReflectiveOperationException exception) { + throw new UncheckedReflectiveOperationException("Failed Reflective set", exception); + } + } + + public static T getField(Field field, Object target) { + try { + //noinspection unchecked + return (T) field.get(target); + } catch (ReflectiveOperationException exception) { + throw new UncheckedReflectiveOperationException("Failed Reflective get", exception); + } + } + + public static class UncheckedReflectiveOperationException extends RuntimeException { + + public UncheckedReflectiveOperationException(String message, ReflectiveOperationException cause) { + super(message, cause); + } + } +} diff --git a/src/common/resources/resources/reauth/certs/amazonrootca1.pem b/src/common/resources/resources/reauth/certs/amazonrootca1.pem new file mode 100644 index 0000000..a6f3e92 --- /dev/null +++ b/src/common/resources/resources/reauth/certs/amazonrootca1.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj +ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM +9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw +IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 +VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L +93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm +jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA +A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI +U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs +N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv +o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU +5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy +rqXRfboQnoZsG4q5WTP468SQvvG5 +-----END CERTIFICATE----- diff --git a/src/common/resources/resources/reauth/certs/digicertglobalrootg2.pem b/src/common/resources/resources/reauth/certs/digicertglobalrootg2.pem new file mode 100644 index 0000000..798e002 --- /dev/null +++ b/src/common/resources/resources/reauth/certs/digicertglobalrootg2.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH +MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI +2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx +1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ +q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz +tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ +vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV +5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY +1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4 +NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG +Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91 +8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe +pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl +MrY= +-----END CERTIFICATE----- diff --git a/src/common/resources/resources/reauth/certs/microsoftrsarootcertificateauthority2017.pem b/src/common/resources/resources/reauth/certs/microsoftrsarootcertificateauthority2017.pem new file mode 100644 index 0000000..3bdfb23 --- /dev/null +++ b/src/common/resources/resources/reauth/certs/microsoftrsarootcertificateauthority2017.pem @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +MIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBl +MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw +NAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 +IDIwMTcwHhcNMTkxMjE4MjI1MTIyWhcNNDIwNzE4MjMwMDIzWjBlMQswCQYDVQQG +EwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1N +aWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKW76UM4wplZEWCpW9R2LBifOZ +Nt9GkMml7Xhqb0eRaPgnZ1AzHaGm++DlQ6OEAlcBXZxIQIJTELy/xztokLaCLeX0 +ZdDMbRnMlfl7rEqUrQ7eS0MdhweSE5CAg2Q1OQT85elss7YfUJQ4ZVBcF0a5toW1 +HLUX6NZFndiyJrDKxHBKrmCk3bPZ7Pw71VdyvD/IybLeS2v4I2wDwAW9lcfNcztm +gGTjGqwu+UcF8ga2m3P1eDNbx6H7JyqhtJqRjJHTOoI+dkC0zVJhUXAoP8XFWvLJ +jEm7FFtNyP9nTUwSlq31/niol4fX/V4ggNyhSyL71Imtus5Hl0dVe49FyGcohJUc +aDDv70ngNXtk55iwlNpNhTs+VcQor1fznhPbRiefHqJeRIOkpcrVE7NLP8TjwuaG +YaRSMLl6IE9vDzhTyzMMEyuP1pq9KsgtsRx9S1HKR9FIJ3Jdh+vVReZIZZ2vUpC6 +W6IYZVcSn2i51BVrlMRpIpj0M+Dt+VGOQVDJNE92kKz8OMHY4Xu54+OU4UZpyw4K +UGsTuqwPN1q3ErWQgR5WrlcihtnJ0tHXUeOrO8ZV/R4O03QK0dqq6mm4lyiPSMQH ++FJDOvTKVTUssKZqwJz58oHhEmrARdlns87/I6KJClTUFLkqqNfs+avNJVgyeY+Q +W5g5xAgGwax/Dj0ApQIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUCctZf4aycI8awznjwNnpv7tNsiMwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAKyvPl3CEZaJjqPnktaXFbgToqZC +LgLNFgVZJ8og6Lq46BrsTaiXVq5lQ7GPAJtSzVXNUzltYkyLDVt8LkS/gxCP81OC +gMNPOsduET/m4xaRhPtthH80dK2Jp86519efhGSSvpWhrQlTM93uCupKUY5vVau6 +tZRGrox/2KJQJWVggEbbMwSubLWYdFQl3JPk+ONVFT24bcMKpBLBaYVu32TxU5nh +SnUgnZUP5NbcA/FZGOhHibJXWpS2qdgXKxdJ5XbLwVaZOjex/2kskZGT4d9Mozd2 +TaGf+G0eHdP67Pv0RR0Tbc/3WeUiJ3IrhvNXuzDtJE3cfVa7o7P4NHmJweDyAmH3 +pvwPuxwXC65B2Xy9J6P9LjrRk5Sxcx0ki69bIImtt2dmefU6xqaWM/5TkshGsRGR +xpl/j8nWZjEgQRCHLQzWwa80mMpkg/sTV9HB8Dx6jKXB/ZUhoHHBk2dxEuqPiApp +GWSZI1b7rCoucL5mxAyE7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9 +dOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKTc0QWbej09+CVgI+WXTik9KveCjCHk9hN +AHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D5KbvtwEwXlGjefVwaaZB +RA+GsCyRxj3qrg+E +-----END CERTIFICATE----- diff --git a/src/common/resources/resources/reauth/icon.png b/src/common/resources/resources/reauth/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..68742fb178e3ae3c09ee7adaf1d27ea8aa853890 GIT binary patch literal 1598 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 literal 0 HcmV?d00001 diff --git a/src/common/resources/resources/reauth/reauth.html b/src/common/resources/resources/reauth/reauth.html new file mode 100644 index 0000000..5209835 --- /dev/null +++ b/src/common/resources/resources/reauth/reauth.html @@ -0,0 +1,57 @@ + + + + ReAuth + + + + +ReAuth +

$text1

+

$text2

+ + \ No newline at end of file diff --git a/src/main/java/technicianlp/reauth/ReconnectHelper.java b/src/main/java/technicianlp/reauth/ReconnectHelper.java new file mode 100644 index 0000000..4f5ffb9 --- /dev/null +++ b/src/main/java/technicianlp/reauth/ReconnectHelper.java @@ -0,0 +1,72 @@ +package technicianlp.reauth; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.ConnectScreen; +import net.minecraft.client.multiplayer.resolver.ServerAddress; +import net.minecraft.network.Connection; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.ComponentContents; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.chat.contents.TranslatableContents; +import technicianlp.reauth.authentication.flows.Flow; +import technicianlp.reauth.authentication.flows.Flows; +import technicianlp.reauth.configuration.Profile; +import technicianlp.reauth.gui.FlowScreen; +import technicianlp.reauth.util.ReflectionUtils; + +import java.lang.reflect.Field; +import java.net.InetSocketAddress; +import java.net.SocketAddress; + +public final class ReconnectHelper { + + private static final Field managerField = ReflectionUtils.findObfuscatedField(ConnectScreen.class, "f_95684_", "connection"); + private static final Field previousField = ReflectionUtils.findObfuscatedField(ConnectScreen.class, "f_95686_", "parent"); + private static ConnectScreen screen; + + /** + * Extract the translationKey from the supplied {@link Component} + * + * @param nested whether to extract the key from the nested {@link Component} instead + */ + public static String getTranslationKey(Object component, boolean nested) { + if (component instanceof MutableComponent mutableComponent) { + ComponentContents contents = mutableComponent.getContents(); + if (contents instanceof TranslatableContents translatableContents) { + if (nested) { + Object[] args = translatableContents.getArgs(); + if (args.length >= 1) { + return getTranslationKey(args[0], false); + } + } else { + return translatableContents.getKey(); + } + } + } + return ""; + } + + public static void setConnectScreen(ConnectScreen screen) { + ReconnectHelper.screen = screen; + } + + public static boolean hasConnectionInfo() { + return screen != null; + } + + public static void retryLogin(Profile profile) { + Flow flow = FlowScreen.open(Flows::loginWithProfile, profile, true); + flow.thenRunAsync(ReconnectHelper::connect, Minecraft.getInstance()); + } + + private static void connect() { + if (screen != null) { + SocketAddress add = ReflectionUtils.getField(managerField, screen).getRemoteAddress(); + if (add instanceof InetSocketAddress address) { + Minecraft minecraft = Minecraft.getInstance(); + ServerAddress server = new ServerAddress(address.getHostString(), address.getPort()); + ConnectScreen.startConnecting(ReflectionUtils.getField(previousField, screen), minecraft, server, minecraft.getCurrentServer()); + } + } + } +} diff --git a/src/main/java/technicianlp/reauth/configuration/Config.java b/src/main/java/technicianlp/reauth/configuration/Config.java new file mode 100644 index 0000000..553af05 --- /dev/null +++ b/src/main/java/technicianlp/reauth/configuration/Config.java @@ -0,0 +1,58 @@ +package technicianlp.reauth.configuration; + +import net.minecraftforge.common.ForgeConfigSpec; +import net.minecraftforge.fml.config.ModConfig; +import technicianlp.reauth.ReAuth; +import technicianlp.reauth.crypto.Crypto; + +import java.io.IOException; + +public final class Config { + + private final ForgeConfigSpec spec; + private ModConfig config; + + private final ProfileList profileList; + + public Config() { + ForgeConfigSpec.Builder builder = new ForgeConfigSpec.Builder(); + + builder.comment("Version Number of the Configuration File") + .defineInRange("version", 3, 3, 3); + + this.profileList = new ProfileList(this, builder); + + this.spec = builder.build(); + } + + public final ForgeConfigSpec getSpec() { + return this.spec; + } + + public final void updateConfig(ModConfig config) { + this.config = config; + Crypto.updateConfigPath(this.getPath(config)); + this.profileList.updateConfig(config); + } + + public final void save() { + this.config.save(); + } + + public final ProfileList getProfileList() { + return this.profileList; + } + + /** + * Get the Absolute path of the Config with symlinks resolved. + * Fall back to "local" path if that lookup fails (somehow) + */ + private String getPath(ModConfig config) { + try { + return config.getFullPath().toRealPath().toString(); + } catch (IOException e) { + ReAuth.log.error("Could not resolve real path", e); + return config.getFullPath().toString(); + } + } +} diff --git a/src/main/java/technicianlp/reauth/configuration/Profile.java b/src/main/java/technicianlp/reauth/configuration/Profile.java new file mode 100644 index 0000000..a309765 --- /dev/null +++ b/src/main/java/technicianlp/reauth/configuration/Profile.java @@ -0,0 +1,30 @@ +package technicianlp.reauth.configuration; + +import com.electronwill.nightconfig.core.CommentedConfig; + +import java.util.concurrent.CompletableFuture; + +public final class Profile { + + private final CommentedConfig config; + + Profile(CommentedConfig config) { + this.config = config; + } + + public final String getValue(String key) { + return this.config.get(key); + } + + public final String getValue(String key, String defaultValue) { + return this.config.getOrElse(key, defaultValue); + } + + public final CompletableFuture get(String key) { + return CompletableFuture.completedFuture(this.getValue(key)); + } + + final CommentedConfig getConfig() { + return this.config; + } +} diff --git a/src/main/java/technicianlp/reauth/configuration/ProfileList.java b/src/main/java/technicianlp/reauth/configuration/ProfileList.java new file mode 100644 index 0000000..a4160a5 --- /dev/null +++ b/src/main/java/technicianlp/reauth/configuration/ProfileList.java @@ -0,0 +1,119 @@ +package technicianlp.reauth.configuration; + +import com.electronwill.nightconfig.core.CommentedConfig; +import com.electronwill.nightconfig.toml.TomlFormat; +import net.minecraftforge.common.ForgeConfigSpec; +import net.minecraftforge.fml.config.ModConfig; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.function.Supplier; + +public final class ProfileList { + + private final Config configuration; + private final ForgeConfigSpec.ConfigValue> profilesProperty; + + private Supplier configSupplier = TomlFormat::newConfig; + + ProfileList(Config configuration, ForgeConfigSpec.Builder builder) { + this.configuration = configuration; + this.profilesProperty = builder + .comment("Saved Profiles. Check Documentation for Info & Syntax") + .define("profiles", this::createDefaultProfileList, this::validateProfileList); + } + + final void updateConfig(ModConfig config) { + this.configSupplier = config.getConfigData()::createSubConfig; + + List list = new ArrayList<>(this.profilesProperty.get()); + this.correctProfiles(list); + this.saveProfiles(list); + } + + public final void storeProfile(Profile profile) { + List list = new ArrayList<>(this.profilesProperty.get()); + if (list.isEmpty()) { + list.add(profile.getConfig()); + } else { + list.set(0, profile.getConfig()); + } + this.saveProfiles(list); + } + + public final Profile getProfile() { + List list = this.profilesProperty.get(); + if (list.isEmpty()) { + return null; + } + CommentedConfig config = list.get(0); + String profileType = config.getOrElse(ProfileConstants.PROFILE_TYPE, ProfileConstants.PROFILE_TYPE_NONE); + if (!ProfileConstants.PROFILE_TYPE_NONE.equals(profileType)) { + return new Profile(config); + } else { + return null; + } + } + + final Profile createProfile(Map data) { + Map orderedData = new TreeMap<>(ProfileConstants::compareProfileKeys); + orderedData.putAll(data); + + CommentedConfig config = this.configSupplier.get(); + orderedData.forEach(config::set); + + return new Profile(config); + } + + private void correctProfiles(List profileList) { + for (CommentedConfig profile : profileList) { + Iterator iterator; + for (iterator = profile.entrySet().iterator(); iterator.hasNext(); ) { + CommentedConfig.Entry entry = iterator.next(); + Object value = entry.getValue(); + if (value == null) { + iterator.remove(); + } else if (!(value instanceof String)) { + entry.setValue(value.toString()); + } + } + } + profileList.removeIf(CommentedConfig::isEmpty); + } + + /** + * Save the list of Profiles to config + * Add a dummy profile if the list is empty + */ + private void saveProfiles(List list) { + if (list.isEmpty()) { + list.add(this.createPlaceholderConfig()); + } + this.profilesProperty.set(list); + this.configuration.save(); + } + + private CommentedConfig createPlaceholderConfig() { + CommentedConfig config = this.configSupplier.get(); + config.set(ProfileConstants.PROFILE_TYPE, ProfileConstants.PROFILE_TYPE_NONE); + return config; + } + + + private List createDefaultProfileList() { + List list = new ArrayList<>(); + list.add(this.createPlaceholderConfig()); + return list; + } + + /** + * checks whether the given Object is a {@link List} and all it's elements are an instance of {@link CommentedConfig} + * This effectively checks whether the provided {@link Object} is an Array of Tables as described by the TOML specification + */ + private boolean validateProfileList(Object el) { + return el instanceof List && ((List) el).stream().allMatch(CommentedConfig.class::isInstance); + } +} diff --git a/src/main/java/technicianlp/reauth/gui/AbstractScreen.java b/src/main/java/technicianlp/reauth/gui/AbstractScreen.java new file mode 100644 index 0000000..a86d301 --- /dev/null +++ b/src/main/java/technicianlp/reauth/gui/AbstractScreen.java @@ -0,0 +1,98 @@ +package technicianlp.reauth.gui; + +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.BufferBuilder; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.Tesselator; +import com.mojang.blaze3d.vertex.VertexFormat; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.client.resources.language.I18n; +import net.minecraft.network.chat.Component; + +abstract class AbstractScreen extends Screen { + + static final int BUTTON_WIDTH = 196; + + private final String title; + + protected int baseX; + protected int centerX; + protected int baseY; + protected int centerY; + protected int screenWidth = 300; + protected int screenHeight = 175; + + AbstractScreen(String title) { + super(Component.translatable("reauth.gui.auth.title")); + this.title = title; + } + + @Override + public void init() { + super.init(); + this.getMinecraft().keyboardHandler.setSendRepeatsToGui(true); + + this.centerX = this.width / 2; + this.baseX = this.centerX - this.screenWidth / 2; + this.centerY = this.height / 2; + this.baseY = this.centerY - this.screenHeight / 2; + + Button cancel = new Button(this.centerX + this.screenWidth / 2 - 22, this.baseY + 2, 20, 20, Component.translatable("reauth.gui.close"), (b) -> this.onClose()); + this.addRenderableWidget(cancel); + } + + @Override + public void render(PoseStack poseStack, int mouseX, int mouseY, float partialTicks) { + this.fillGradient(poseStack, 0, 0, this.width, this.height, 0xc0101010, 0xd0101010); + + // modified renderDirtBackground(0); + Tesselator tesselator = Tesselator.getInstance(); + BufferBuilder bufferbuilder = tesselator.getBuilder(); + RenderSystem.setShader(GameRenderer::getPositionTexColorShader); + RenderSystem.setShaderTexture(0, BACKGROUND_LOCATION); + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); + bufferbuilder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX_COLOR); + bufferbuilder.vertex(this.baseX, this.baseY + this.screenHeight, 0.0D).uv(0.0F, this.screenHeight / 32.0F).color(80, 80, 80, 255).endVertex(); + bufferbuilder.vertex(this.baseX + this.screenWidth, this.baseY + this.screenHeight, 0.0D).uv(this.screenWidth / 32.0F, this.screenHeight / 32.0F).color(80, 80, 80, 255).endVertex(); + bufferbuilder.vertex(this.baseX + this.screenWidth, this.baseY, 0.0D).uv(this.screenWidth / 32.0F, 0F).color(80, 80, 80, 255).endVertex(); + bufferbuilder.vertex(this.baseX, this.baseY, 0.0D).uv(0.0F, 0F).color(80, 80, 80, 255).endVertex(); + tesselator.end(); + + super.render(poseStack, mouseX, mouseY, partialTicks); + + this.font.drawShadow(poseStack, I18n.get(this.title), this.centerX - (BUTTON_WIDTH / 2f), this.baseY + 8, 0xFFFFFFFF); + } + + protected final void transitionScreen(Screen newScreen) { + this.getMinecraft().popGuiLayer(); + this.getMinecraft().pushGuiLayer(newScreen); + } + + protected void requestClose(boolean completely) { + if (completely) { + super.onClose(); + } else { + this.transitionScreen(new MainScreen()); + } + } + + /** + * Method called to request this Screen to close itself + */ + @Override + public final void onClose() { + this.requestClose(false); + } + + /** + * Called once this Screen is closed + */ + @Override + public void removed() { + super.removed(); + this.getMinecraft().keyboardHandler.setSendRepeatsToGui(false); + } +} diff --git a/src/main/java/technicianlp/reauth/gui/FlowScreen.java b/src/main/java/technicianlp/reauth/gui/FlowScreen.java new file mode 100644 index 0000000..3b926e1 --- /dev/null +++ b/src/main/java/technicianlp/reauth/gui/FlowScreen.java @@ -0,0 +1,164 @@ +package technicianlp.reauth.gui; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.Util; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.resources.language.I18n; +import net.minecraft.network.chat.Component; +import technicianlp.reauth.ReAuth; +import technicianlp.reauth.authentication.SessionData; +import technicianlp.reauth.authentication.flows.AuthorizationCodeFlow; +import technicianlp.reauth.authentication.flows.DeviceCodeFlow; +import technicianlp.reauth.authentication.flows.Flow; +import technicianlp.reauth.authentication.flows.FlowCallback; +import technicianlp.reauth.authentication.flows.FlowStage; +import technicianlp.reauth.configuration.Profile; +import technicianlp.reauth.session.SessionHelper; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.function.BiFunction; + +public final class FlowScreen extends AbstractScreen implements FlowCallback { + + public static F open(BiFunction flowConstructor, P param, boolean keepParent) { + FlowScreen screen = new FlowScreen(); + F flow = flowConstructor.apply(param, screen); + screen.flow = flow; + if (!keepParent) { + Minecraft.getInstance().popGuiLayer(); + } + Minecraft.getInstance().pushGuiLayer(screen); + return flow; + } + + private Flow flow; + private FlowStage stage = FlowStage.INITIAL; + private String[] formatArgs = new String[0]; + + public FlowScreen() { + super("reauth.gui.title.flow"); + } + + @Override + public void init() { + super.init(); + + int buttonWidth = 196; + int buttonWidthH = buttonWidth / 2; + + this.formatArgs = new String[0]; + if (this.stage == FlowStage.MS_AWAIT_AUTH_CODE && this.flow instanceof AuthorizationCodeFlow) { + try { + URL url = new URL(((AuthorizationCodeFlow) this.flow).getLoginUrl()); + this.addRenderableWidget(new Button(this.centerX - buttonWidthH, this.baseY + this.screenHeight - 42, buttonWidth, 20, Component.translatable("reauth.msauth.button.browser"), (b) -> Util.getPlatform().openUrl((url)))); + } catch (MalformedURLException e) { + ReAuth.log.error("Browser button failed", e); + } + } else if (this.stage == FlowStage.MS_POLL_DEVICE_CODE && this.flow instanceof DeviceCodeFlow) { + try { + DeviceCodeFlow flow = (DeviceCodeFlow) this.flow; + if (CompletableFuture.allOf(flow.getLoginUrl(), flow.getCode()).isDone()) { + String urlString = flow.getLoginUrl().join(); + String code = flow.getCode().join(); + URL url = new URL(urlString); + this.addRenderableWidget(new Button(this.centerX - buttonWidthH, this.baseY + this.screenHeight - 42, buttonWidth, 20, Component.translatable("reauth.msauth.button.browser"), (b) -> Util.getPlatform().openUrl((url)))); + this.formatArgs = new String[]{urlString, code}; + } + } catch (MalformedURLException e) { + ReAuth.log.error("Browser button failed", e); + } + } + } + + @Override + public void render(PoseStack poseStack, int mouseX, int mouseY, float partialTicks) { + super.render(poseStack, mouseX, mouseY, partialTicks); + + String text = I18n.get(this.stage.getRawName(), (Object[]) this.formatArgs); + String[] lines = text.split("\\R"); + int height = lines.length * 9; + for (String s : lines) { + if (s.startsWith("$")) { + height += 9; + } + } + + int y = this.centerY - height / 2; + for (String line : lines) { + if (line.startsWith("$")) { + line = line.substring(1); + poseStack.pushPose(); + poseStack.scale(2, 2, 1); + this.font.drawShadow(poseStack, line, (float) (this.centerX - this.font.width(line)) / 2, (float) y / 2, 0xFFFFFFFF); + y += 18; + poseStack.popPose(); + } else { + this.font.drawShadow(poseStack, line, (float) (this.centerX - this.font.width(line) / 2), (float) y, 0xFFFFFFFF); + y += 9; + } + } + } + + @Override + public void removed() { + super.removed(); + if (this.stage != FlowStage.FINISHED) { + this.flow.cancel(); + } + } + + @Override + public void onSessionComplete(SessionData session, Throwable throwable) { + if (throwable == null) { + SessionHelper.setSession(session); + ReAuth.log.info("Login complete"); + } else { + if (throwable instanceof CancellationException || throwable.getCause() instanceof CancellationException) { + ReAuth.log.info("Login cancelled"); + } else { + ReAuth.log.error("Login failed", throwable); + } + } + } + + @Override + public void onProfileComplete(Profile profile, Throwable throwable) { + if (throwable == null) { + ReAuth.profiles.storeProfile(profile); + ReAuth.log.info("Profile saved successfully"); + } else { + if (throwable instanceof CancellationException || throwable.getCause() instanceof CancellationException) { + ReAuth.log.info("Profile saving cancelled"); + } else { + ReAuth.log.error("Profile failed to save", throwable); + } + } + } + + @Override + public void transitionStage(FlowStage newStage) { + this.stage = newStage; + ReAuth.log.info(this.stage.getLogLine()); + this.init(Minecraft.getInstance(), this.width, this.height); + + if (newStage == FlowStage.MS_AWAIT_AUTH_CODE && this.flow instanceof AuthorizationCodeFlow) { + try { + Util.getPlatform().openUrl(new URL(((AuthorizationCodeFlow) this.flow).getLoginUrl())); + } catch (MalformedURLException e) { + ReAuth.log.error("Failed to open page", e); + } + } else if (newStage == FlowStage.FINISHED) { + this.requestClose(true); + } + } + + @Override + public Executor getExecutor() { + return ReAuth.executor; + } +} diff --git a/src/main/java/technicianlp/reauth/gui/MainScreen.java b/src/main/java/technicianlp/reauth/gui/MainScreen.java new file mode 100644 index 0000000..7dbffb9 --- /dev/null +++ b/src/main/java/technicianlp/reauth/gui/MainScreen.java @@ -0,0 +1,82 @@ +package technicianlp.reauth.gui; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.resources.language.I18n; +import net.minecraft.network.chat.Component; +import net.minecraftforge.fml.VersionChecker; +import org.apache.maven.artifact.versioning.ComparableVersion; +import technicianlp.reauth.ReAuth; +import technicianlp.reauth.authentication.flows.Flows; +import technicianlp.reauth.configuration.Profile; +import technicianlp.reauth.configuration.ProfileConstants; + +import java.util.Map; + +public final class MainScreen extends AbstractScreen { + + private String message = null; + + public MainScreen() { + super("reauth.gui.title.main"); + } + + @Override + public void init() { + super.init(); + + int buttonWidthH = BUTTON_WIDTH / 2; + int y = this.centerY - 55; + + SaveButton.ITooltip saveButtonTooltip = (button, matrixStack, mouseX, mouseY) -> this.renderTooltip(matrixStack, this.font.split(Component.translatable("reauth.gui.button.save.tooltip"), 250), mouseX, mouseY); + SaveButton saveButton = new SaveButton(this.centerX - buttonWidthH, y + 70, Component.translatable("reauth.gui.button.save"), saveButtonTooltip); + this.addRenderableWidget(saveButton); + + Profile profile = ReAuth.profiles.getProfile(); + if (profile != null) { + Component text = Component.translatable("reauth.gui.profile", profile.getValue(ProfileConstants.NAME, "Steve")); + this.addRenderableWidget(new Button(this.centerX - buttonWidthH, y + 10, BUTTON_WIDTH, 20, text, (b) -> FlowScreen.open(Flows::loginWithProfile, profile, false))); + } else { + Button profileButton = new Button(this.centerX - buttonWidthH, y + 10, BUTTON_WIDTH, 20, Component.translatable("reauth.gui.noProfile"), (b) -> { + }); + profileButton.active = false; + this.addRenderableWidget(profileButton); + } + + this.addRenderableWidget(new Button(this.centerX - buttonWidthH, y + 45, buttonWidthH - 1, 20, Component.translatable("reauth.gui.button.authcode"), (b) -> FlowScreen.open(Flows::loginWithAuthCode, saveButton.selected(), false))); + this.addRenderableWidget(new Button(this.centerX + 1, y + 45, buttonWidthH - 1, 20, Component.translatable("reauth.gui.button.devicecode"), (b) -> FlowScreen.open(Flows::loginWithDeviceCode, saveButton.selected(), false))); + this.addRenderableWidget(new Button(this.centerX - buttonWidthH, y + 105, BUTTON_WIDTH, 20, Component.translatable("reauth.gui.button.offline"), (b) -> this.transitionScreen(new OfflineLoginScreen()))); + + + VersionChecker.CheckResult result = VersionChecker.getResult(ReAuth.modInfo); + if (result.status() == VersionChecker.Status.OUTDATED) { + // Cannot be null but is marked as such :( + Map changes = result.changes(); + if (changes != null) { + String msg = changes.get(result.target()); + if (msg != null) { + this.message = I18n.get("reauth.gui.auth.update", msg); + } + } + } + } + + @Override + public void render(PoseStack poseStack, int mouseX, int mouseY, float partialTicks) { + super.render(poseStack, mouseX, mouseY, partialTicks); + + int x = this.centerX - BUTTON_WIDTH / 2; + this.font.drawShadow(poseStack, I18n.get("reauth.gui.text.profile"), x, this.centerY - 55, 0xFFFFFFFF); + this.font.drawShadow(poseStack, I18n.get("reauth.gui.text.microsoft"), x, this.centerY - 20, 0xFFFFFFFF); + this.font.drawShadow(poseStack, I18n.get("reauth.gui.text.offline"), x, this.centerY + 40, 0xFFFFFFFF); + + if (this.message != null) { + this.font.drawShadow(poseStack, this.message, x, this.baseY + 20, 0xFFFFFFFF); + } + } + + @Override + protected void requestClose(boolean completely) { + super.requestClose(true); + } +} diff --git a/src/main/java/technicianlp/reauth/gui/OfflineLoginScreen.java b/src/main/java/technicianlp/reauth/gui/OfflineLoginScreen.java new file mode 100644 index 0000000..437a30c --- /dev/null +++ b/src/main/java/technicianlp/reauth/gui/OfflineLoginScreen.java @@ -0,0 +1,71 @@ +package technicianlp.reauth.gui; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.resources.language.I18n; +import net.minecraft.network.chat.Component; +import org.lwjgl.glfw.GLFW; +import technicianlp.reauth.session.SessionHelper; + +public final class OfflineLoginScreen extends AbstractScreen { + + private EditBox username; + private Button confirm; + + public OfflineLoginScreen() { + super("reauth.gui.title.offline"); + } + + @Override + public void init() { + super.init(); + + this.username = new EditBox(this.font, this.centerX - BUTTON_WIDTH / 2, this.centerY - 5, BUTTON_WIDTH, 20, Component.translatable("reauth.gui.auth.username")); + this.username.setMaxLength(16); + this.addRenderableWidget(this.username); + this.username.setFocus(true); + this.setFocused(this.username); + this.addRenderableWidget(this.username); + + this.confirm = new Button(this.centerX - BUTTON_WIDTH / 2, this.baseY + this.screenHeight - 42, BUTTON_WIDTH, 20, Component.translatable("reauth.gui.button.username"), (b) -> this.performUsernameChange()); + this.addRenderableWidget(this.confirm); + } + + @Override + public void render(PoseStack poseStack, int mouseX, int mouseY, float partialTicks) { + super.render(poseStack, mouseX, mouseY, partialTicks); + + this.font.drawShadow(poseStack, I18n.get("reauth.gui.auth.username"), this.centerX - (BUTTON_WIDTH / 2f), this.centerY - 15, 0xFFFFFFFF); + } + + @Override + public void tick() { + super.tick(); + this.confirm.active = SessionHelper.isValidOfflineUsername(this.username.getValue()); + } + + @Override + public boolean keyPressed(int keyCode, int p_keyPressed_2_, int p_keyPressed_3_) { + if (keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_KP_ENTER) { + GuiEventListener focus = this.getFocused(); + if (focus == this.username) { + this.performUsernameChange(); + return true; + } + } + return super.keyPressed(keyCode, p_keyPressed_2_, p_keyPressed_3_); + } + + /** + * Calls the to do the Login and handles Errors + * Closes the Screen if successful + */ + private void performUsernameChange() { + if (SessionHelper.isValidOfflineUsername(this.username.getValue())) { + SessionHelper.setOfflineUsername(this.username.getValue()); + this.requestClose(true); + } + } +} diff --git a/src/main/java/technicianlp/reauth/gui/SaveButton.java b/src/main/java/technicianlp/reauth/gui/SaveButton.java new file mode 100644 index 0000000..e705a08 --- /dev/null +++ b/src/main/java/technicianlp/reauth/gui/SaveButton.java @@ -0,0 +1,27 @@ +package technicianlp.reauth.gui; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.gui.components.Checkbox; +import net.minecraft.network.chat.Component; + +public final class SaveButton extends Checkbox { + + private final ITooltip tooltip; + + public SaveButton(int x, int y, Component title, ITooltip tooltip) { + super(x, y, 20, 20, title, false); + this.tooltip = tooltip; + } + + @Override + public void renderButton(PoseStack poseStack, int mouseX, int mouseY, float partialTicks) { + super.renderButton(poseStack, mouseX, mouseY, partialTicks); + if (this.isHoveredOrFocused()) { + this.tooltip.onTooltip(this, poseStack, mouseX, mouseY); + } + } + + public interface ITooltip { + void onTooltip(SaveButton button, PoseStack poseStack, int mouseX, int mouseY); + } +} diff --git a/src/main/java/technicianlp/reauth/session/SessionHelper.java b/src/main/java/technicianlp/reauth/session/SessionHelper.java new file mode 100644 index 0000000..9b9065e --- /dev/null +++ b/src/main/java/technicianlp/reauth/session/SessionHelper.java @@ -0,0 +1,104 @@ +package technicianlp.reauth.session; + +import com.mojang.authlib.exceptions.AuthenticationException; +import com.mojang.authlib.minecraft.UserApiService; +import com.mojang.authlib.yggdrasil.YggdrasilAuthenticationService; +import com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService; +import net.minecraft.client.Minecraft; +import net.minecraft.client.User; +import net.minecraft.client.gui.screens.social.PlayerSocialManager; +import net.minecraft.client.main.GameConfig; +import net.minecraft.client.resources.SplashManager; +import technicianlp.reauth.ReAuth; +import technicianlp.reauth.authentication.SessionData; +import technicianlp.reauth.util.ReflectionUtils; + +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import java.util.UUID; +import java.util.regex.Pattern; + +public final class SessionHelper { + + private static final Field userField = ReflectionUtils.findObfuscatedField(Minecraft.class, "f_90998_", "user"); + private static final Field userApiServiceField = ReflectionUtils.findObfuscatedField(Minecraft.class, "f_193584_", "userApiService"); + private static final Field socialManagerField = ReflectionUtils.findObfuscatedField(Minecraft.class, "f_91006_", "playerSocialManager"); + private static final Field splashesSessionField = ReflectionUtils.findObfuscatedField(SplashManager.class, "f_118863_", "user"); + + private static final Pattern usernamePattern = Pattern.compile("[A-Za-z0-9_]{2,16}"); + + /** + * construct a {@link SessionData} for the given offline username + */ + public static void setOfflineUsername(String username) { + UUID uuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + username).getBytes(StandardCharsets.UTF_8)); + setSession(new SessionData(username, uuid.toString(), "invalid", "legacy"), false); + } + + /** + * Set the Session and update dependant fields + */ + public static void setSession(SessionData data) { + setSession(data, true); + } + + /** + * Set the Session and update dependant fields + *

+ *

  • Clear ProfileProperties and repopulate them + *
  • Recreate {@link UserApiService}, using logic from {@link Minecraft#createUserApiService(YggdrasilAuthenticationService, GameConfig)} + *
  • Recreate {@link PlayerSocialManager} with the new SocialInteractionsService + *
  • Update {@link SplashManager#user} + */ + private static void setSession(SessionData data, boolean online) { + try { + Minecraft minecraft = Minecraft.getInstance(); + + User session = new User(data.username, data.uuid, data.accessToken, Optional.empty(), Optional.empty(), User.Type.byName(data.type)); + + ReflectionUtils.setField(userField, minecraft, session); + SessionChecker.invalidate(); + + // Update things depending on the Session. + // TODO keep updated across versions + + // Clear ProfileProperties and repopulate them + minecraft.getProfileProperties().clear(); + minecraft.getProfileProperties(); + // UserProperties are unused + + // Recreate UserApiService + UserApiService userApiService = null; + if (online) { + YggdrasilMinecraftSessionService sessionService = (YggdrasilMinecraftSessionService) minecraft.getMinecraftSessionService(); + YggdrasilAuthenticationService authService = sessionService.getAuthenticationService(); + try { + userApiService = authService.createUserApiService(session.getAccessToken()); + } catch (AuthenticationException authenticationexception) { + ReAuth.log.error("Failed to create UserApiService", authenticationexception); + } + } + if (userApiService == null) { + userApiService = UserApiService.OFFLINE; + } + ReflectionUtils.setField(userApiServiceField, minecraft, userApiService); + + // Recreate FilterManager + PlayerSocialManager socialManager = new PlayerSocialManager(minecraft, userApiService); + ReflectionUtils.setField(socialManagerField, minecraft, socialManager); + + // Update Splashes session + ReflectionUtils.setField(splashesSessionField, minecraft.getSplashManager(), session); + } catch (Exception e) { + ReAuth.log.error("Failed to update Session", e); + } + } + + /** + * checks the username to match the offline username regex + */ + public static boolean isValidOfflineUsername(String username) { + return usernamePattern.matcher(username).matches(); + } +} diff --git a/src/main/resources/assets/reauth/lang/en_us.json b/src/main/resources/assets/reauth/lang/en_us.json index 579ce1d..9ee815e 100644 --- a/src/main/resources/assets/reauth/lang/en_us.json +++ b/src/main/resources/assets/reauth/lang/en_us.json @@ -1,6 +1,9 @@ { "reauth.open": "Open ReAuth", "reauth.gui.auth.title": "ReAuthentication Screen", + "reauth.gui.title.main": "ReAuth", + "reauth.gui.title.flow": "", + "reauth.gui.title.offline": "Choose a Username", "reauth.gui.auth.username": "Username", "reauth.gui.auth.password": "Password", "reauth.gui.auth.checkbox": "Save Password to Config (WARNING: SECURITY RISK!)", @@ -13,14 +16,56 @@ "reauth.gui.auth.text2": "Password:", "reauth.gui.auth.update": "§9Update available: %s", "reauth.gui.button": "ReAuth Login", + "reauth.gui.button.save": "Save Profile to Config", + "reauth.gui.button.save.tooltip": "WARNING: Anyone with access to your config file may be able to extract and decrypt the stored profile.", + "reauth.gui.close": "X", + "reauth.gui.noProfile": "Login with saved Profile", + "reauth.gui.profile": "Login as %s", + "reauth.gui.text.profile": "Login using saved Profile", + "reauth.gui.text.microsoft": "Login using a Xbox Account", + "reauth.gui.text.offline": "Choose an offline Username", + "reauth.gui.button.authcode": "This Device", + "reauth.gui.button.devicecode": "Any Device", + "reauth.gui.button.offline": "Choose Username", + "reauth.gui.button.username": "Set Username", "reauth.status.valid": "Online: §l§a✔", "reauth.status.invalid": "Online: §l§c✘", "reauth.status.unknown": "Online: §l§7?", + "reauth.status.refreshing": "Online: §l§b?", + "reauth.status.error": "Online: §l§c?", "reauth.login.success": "§aLogin successful!", "reauth.login.fail": "§4Login failed: %s", "reauth.login.error": "§4Error: %s", "reauth.retry": "Login as %s and retry", - "reauth.retry.disabled": "Login and retry not available", + "reauth.retry.disabled": "Login with saved Profile and retry", + "reauth.msauth.code.success": "Authentication with Microsoft was successful", + "reauth.msauth.code.success.close": "You may now close this page", + "reauth.msauth.code.fail.cancelled": "Authentication with Microsoft was cancelled at your request", + "reauth.msauth.code.fail.server": "Authentication with Microsoft is temporarily unavailable", + "reauth.msauth.code.fail.unknown": "Authentication with Microsoft failed", + "reauth.msauth.code.retry": "Please click here to try again", + "reauth.msauth.code.error.http200": "Error: This is not an error!?", + "reauth.msauth.code.error.http400": "Error: Authentication failed", + "reauth.msauth.code.error.http404": "Error: This resource does not exist", + "reauth.msauth.code.error.http405": "Error: Incorrect request method", + "reauth.msauth.code.error.http415": "Error: Authentication data has invalid format", + "reauth.msauth.code.error.http501": "Error: Unknown request method", + "reauth.msauth.step.initial": "Starting authentication Process", + "reauth.msauth.step.finished": "Login successful", + "reauth.msauth.step.profile": "Saving Profile", + "reauth.msauth.step.failed": "Login failed - Please try again", + "reauth.msauth.step.crypto": "Initializing Profile encryption", + "reauth.msauth.step.yggdrasil": "Authenticating with Mojang", + "reauth.msauth.step.microsoft.code.await": "Please check your Browser to login", + "reauth.msauth.step.microsoft.code.redeem": "Authenticating with Microsoft", + "reauth.msauth.step.microsoft.device.request": "Setting up Authentication with Microsoft", + "reauth.msauth.step.microsoft.device.poll": "Please enter code\n$%2$s\non %1$s", + "reauth.msauth.step.microsoft.refresh": "Refreshing Authentication with Microsoft", + "reauth.msauth.step.xbox": "Authenticating with Xbox Live", + "reauth.msauth.step.xsts": "Authenticating with Xbox Live Token Service", + "reauth.msauth.step.mojang": "Authenticating with Mojang", + "reauth.msauth.step.fetch": "Retrieving Profile information", + "reauth.msauth.button.browser": "Open Login-Page in Browser", "text.autoconfig.../reauth.title": "ReAuth Config", "text.autoconfig.../reauth.option.credentials.salt": "Salt", "text.autoconfig.../reauth.option.credentials.salt.@Tooltip[0]": "One of the values required to decrypt the credentials", From da2679a948b16987a680e0e278b0bd867b41d143 Mon Sep 17 00:00:00 2001 From: Phu Ngo Date: Tue, 20 Sep 2022 23:20:50 +0700 Subject: [PATCH 3/8] upgrade to 1.19.2 and port to Fabric --- .gitignore | 15 +- build.gradle | 83 +++--- gradle.properties | 24 +- settings.gradle | 4 +- .../flows/impl/MicrosoftProfileFlow.java | 5 +- .../reauth/util/ReflectionUtils.java | 2 + .../java/technicianlp/reauth/AuthHelper.java | 205 --------------- .../technicianlp/reauth/ConfigWrapper.java | 171 ------------- .../technicianlp/reauth/Configuration.java | 50 ---- src/main/java/technicianlp/reauth/Crypto.java | 100 -------- .../technicianlp/reauth/EventHandler.java | 104 ++++++++ .../technicianlp/reauth/JceWorkaround.java | 59 ----- src/main/java/technicianlp/reauth/ReAuth.java | 45 +++- .../technicianlp/reauth/ReconnectHelper.java | 47 ++-- .../technicianlp/reauth/ReflectionHelper.java | 83 ------ .../technicianlp/reauth/VersionChecker.java | 22 +- .../reauth/configuration/Config.java | 40 ++- .../reauth/configuration/ProfileList.java | 52 ++-- .../reauth/gui/AbstractScreen.java | 93 ++++--- .../technicianlp/reauth/gui/AuthScreen.java | 237 ------------------ .../technicianlp/reauth/gui/FlowScreen.java | 55 ++-- .../technicianlp/reauth/gui/MainScreen.java | 76 +++--- .../reauth/gui/OfflineLoginScreen.java | 51 ++-- .../reauth/gui/PasswordFieldWidget.java | 162 ------------ .../technicianlp/reauth/gui/SaveButton.java | 20 +- .../integration/ClothConfigIntegration.java | 22 -- .../integration/ModMenuIntegration.java | 9 +- .../reauth/mixin/ConnectScreenMixin.java | 33 --- .../reauth/mixin/DisconnectedScreenMixin.java | 58 ----- .../reauth/mixin/MinecraftClientMixin.java | 15 -- .../reauth/mixin/MultiplayerScreenMixin.java | 39 --- .../reauth/mixin/TextFieldWidgetMixin.java | 12 - .../reauth/mixinUtil/ConnectScreenDuck.java | 10 - .../reauth/mixinUtil/DisconnectUtil.java | 50 ---- .../mixinUtil/DisconnectedScreenDuck.java | 11 - .../reauth/session/SessionHelper.java | 49 ++-- src/main/resources/fabric.mod.json | 25 +- src/main/resources/reauth.mixins.json | 10 +- 38 files changed, 487 insertions(+), 1661 deletions(-) delete mode 100644 src/main/java/technicianlp/reauth/AuthHelper.java delete mode 100644 src/main/java/technicianlp/reauth/ConfigWrapper.java delete mode 100644 src/main/java/technicianlp/reauth/Configuration.java delete mode 100644 src/main/java/technicianlp/reauth/Crypto.java create mode 100644 src/main/java/technicianlp/reauth/EventHandler.java delete mode 100644 src/main/java/technicianlp/reauth/JceWorkaround.java delete mode 100644 src/main/java/technicianlp/reauth/ReflectionHelper.java delete mode 100644 src/main/java/technicianlp/reauth/gui/AuthScreen.java delete mode 100644 src/main/java/technicianlp/reauth/gui/PasswordFieldWidget.java delete mode 100644 src/main/java/technicianlp/reauth/integration/ClothConfigIntegration.java delete mode 100644 src/main/java/technicianlp/reauth/mixin/ConnectScreenMixin.java delete mode 100644 src/main/java/technicianlp/reauth/mixin/DisconnectedScreenMixin.java delete mode 100644 src/main/java/technicianlp/reauth/mixin/MinecraftClientMixin.java delete mode 100644 src/main/java/technicianlp/reauth/mixin/MultiplayerScreenMixin.java delete mode 100644 src/main/java/technicianlp/reauth/mixin/TextFieldWidgetMixin.java delete mode 100644 src/main/java/technicianlp/reauth/mixinUtil/ConnectScreenDuck.java delete mode 100644 src/main/java/technicianlp/reauth/mixinUtil/DisconnectUtil.java delete mode 100644 src/main/java/technicianlp/reauth/mixinUtil/DisconnectedScreenDuck.java diff --git a/.gitignore b/.gitignore index 6ab9de8..81c2061 100644 --- a/.gitignore +++ b/.gitignore @@ -7,22 +7,21 @@ bin .project # idea -out +/.idea +/out *.ipr *.iws *.iml -.idea # gradle -build -.gradle +/build +/.gradle +/gradle +/gradlew +/gradlew.bat # other eclipse run annotations logs -scrapped - -# Files from Forge MDK -forge*changelog.txt diff --git a/build.gradle b/build.gradle index 7b1460a..7e19d42 100644 --- a/build.gradle +++ b/build.gradle @@ -1,30 +1,47 @@ plugins { - id 'fabric-loom' version '0.4-SNAPSHOT' - id 'maven-publish' + id 'fabric-loom' version '1.0-SNAPSHOT' } -sourceCompatibility = JavaVersion.VERSION_1_8 -targetCompatibility = JavaVersion.VERSION_1_8 +sourceCompatibility = JavaVersion.VERSION_17 +targetCompatibility = JavaVersion.VERSION_17 archivesBaseName = project.archives_base_name version = project.mod_version +group = project.maven_group + +repositories { + // Add repositories to retrieve artifacts from in here. + // You should only use this when depending on other mods because + // Loom adds the essential maven repositories to download Minecraft and libraries from automatically. + // See https://docs.gradle.org/current/userguide/declaring_repositories.html + // for more information about repositories. + maven { + url "https://maven.terraformersmc.com/releases" + } + maven { + url "https://maven.shedaniel.me/" + } +} dependencies { - //to change the versions see the gradle.properties file - minecraft "com.mojang:minecraft:${project.minecraft_version}" - mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" - modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" + // To change the versions see the gradle.properties file + minecraft "com.mojang:minecraft:${project.minecraft_version}" + mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" + modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" - // Fabric API. This is technically optional, but you probably want it anyway. - modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" + // Fabric API. This is technically optional, but you probably want it anyway. + modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" - modApi "me.sargunvohra.mcmods:autoconfig1u:3.2.0-unstable" - modImplementation "io.github.prospector:modmenu:1.14.6+build.31" - modApi ("me.shedaniel.cloth:config-2:4.7.0-unstable") { - exclude(group: "net.fabricmc.fabric-api") - } - // PSA: Some older mods, compiled on Loom 0.2.1, might have outdated Maven POMs. - // You may need to force-disable transitiveness on them. + modImplementation "com.terraformersmc:modmenu:${project.modmenu_version}" + modApi("me.shedaniel.cloth:cloth-config-fabric:${project.cloth_config_version}") { + exclude(group: "net.fabricmc.fabric-api") + } + // modApi ("me.shedaniel.cloth:config-2:4.7.0-unstable") { + // exclude(group: "net.fabricmc.fabric-api") + // } + // PSA: Some older mods, compiled on Loom 0.2.1, might have outdated Maven POMs. + // You may need to force-disable transitiveness on them. + implementation 'com.electronwill.night-config:toml:3.6.6' } sourceSets { @@ -35,34 +52,28 @@ sourceSets { } processResources { - inputs.property "version", project.version - - from(sourceSets.main.resources.srcDirs) { - include "fabric.mod.json" - expand "version": project.version - } + inputs.property "version", project.version - from(sourceSets.main.resources.srcDirs) { - exclude "fabric.mod.json" - } + filesMatching("fabric.mod.json") { + expand "version": project.version + } } -// ensure that the encoding is set to UTF-8, no matter what the system default is -// this fixes some edge cases with special characters not displaying correctly -// see http://yodaconditions.net/blog/fix-for-java-file-encoding-problems-with-gradle.html -tasks.withType(JavaCompile) { - options.encoding = "UTF-8" +tasks.withType(JavaCompile).configureEach { + // Minecraft 1.18 (1.18-pre2) upwards uses Java 17. + it.options.release = 17 } // Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task // if it is present. // If you remove this task, sources will not be generated. task sourcesJar(type: Jar, dependsOn: classes) { - classifier = "sources" - from sourceSets.main.allSource + classifier = "sources" + from sourceSets.main.allSource } -ext { - autoSignProfile = "reauth" - autoSignTarget = tasks.remapJar.archivePath +jar { + from("LICENSE") { + rename { "${it}_${project.archivesBaseName}" } + } } diff --git a/gradle.properties b/gradle.properties index b51e4f3..90af8ac 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,16 +1,18 @@ # Done to increase the memory available to gradle. org.gradle.jvmargs=-Xmx1G - # Fabric Properties - # check these on https://fabricmc.net/use - minecraft_version=1.16.2 - yarn_mappings=1.16.2+build.1 - loader_version=0.9.1+build.205 - +# check these on https://fabricmc.net/use +minecraft_version=1.19.2 +# https://maven.fabricmc.net/net/fabricmc/yarn/ +yarn_mappings=1.19.2+build.9 +# https://maven.fabricmc.net/net/fabricmc/fabric-loader/ +loader_version=0.14.9 # Mod Properties - mod_version = 3.9.3 - archives_base_name = ReAuth-1.16-Fabric - +mod_version=4.1.0-Fabric +maven_group=technicianlp.reauth +archives_base_name=ReAuth-1.19.2-Fabric # Dependencies - # currently not on the main fabric site, check on the maven: https://maven.fabricmc.net/net/fabricmc/fabric-api/fabric-api - fabric_version=0.19.0+build.398-1.16 +# currently not on the main fabric site, check on the maven: https://maven.fabricmc.net/net/fabricmc/fabric-api/fabric-api +fabric_version=0.61.0+1.19.2 +cloth_config_version=8.2.88 +modmenu_version=4.0.6 diff --git a/settings.gradle b/settings.gradle index a9ae1f4..d2a5a93 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,12 +1,12 @@ pluginManagement { repositories { - jcenter() maven { name = 'Fabric' url = 'https://maven.fabricmc.net/' } + mavenCentral() gradlePluginPortal() } } -rootProject.name='ReAuth Fabric 1.16' +rootProject.name='ReAuth Fabric 1.19.2' diff --git a/src/common/java/technicianlp/reauth/authentication/flows/impl/MicrosoftProfileFlow.java b/src/common/java/technicianlp/reauth/authentication/flows/impl/MicrosoftProfileFlow.java index e76a763..66b9259 100644 --- a/src/common/java/technicianlp/reauth/authentication/flows/impl/MicrosoftProfileFlow.java +++ b/src/common/java/technicianlp/reauth/authentication/flows/impl/MicrosoftProfileFlow.java @@ -8,7 +8,6 @@ import technicianlp.reauth.authentication.flows.FlowStage; import technicianlp.reauth.authentication.flows.Tokens; import technicianlp.reauth.authentication.flows.impl.util.Futures; -import technicianlp.reauth.authentication.http.Response; import technicianlp.reauth.configuration.Profile; import technicianlp.reauth.configuration.ProfileBuilder; import technicianlp.reauth.configuration.ProfileConstants; @@ -31,10 +30,10 @@ public final class MicrosoftProfileFlow extends FlowBase { private final CompletableFuture profileFuture; /** - * tries to login using the stored accessToken. + * tries to log in using the stored accessToken. * When authentication fails due to an expired token, the stored refreshToken is used to acquire a new accessToken followed by a second login attempt * - * @see XboxAuthenticationFlow#isExpiredToken(Response, Throwable) + * @see XboxAuthenticationFlow#hasExpiredTokenError() */ public MicrosoftProfileFlow(Profile profile, FlowCallback callback) { super(callback); diff --git a/src/common/java/technicianlp/reauth/util/ReflectionUtils.java b/src/common/java/technicianlp/reauth/util/ReflectionUtils.java index 3a9e9a3..7a49b7e 100644 --- a/src/common/java/technicianlp/reauth/util/ReflectionUtils.java +++ b/src/common/java/technicianlp/reauth/util/ReflectionUtils.java @@ -56,6 +56,8 @@ public static Field findField(Class clz, String name) { } } + // TODO: use Mutator mixins? + public static Field findObfuscatedField(Class clz, String obfName, String name) { try { return findFieldInternal(clz, obfName); diff --git a/src/main/java/technicianlp/reauth/AuthHelper.java b/src/main/java/technicianlp/reauth/AuthHelper.java deleted file mode 100644 index bc07ceb..0000000 --- a/src/main/java/technicianlp/reauth/AuthHelper.java +++ /dev/null @@ -1,205 +0,0 @@ -package technicianlp.reauth; - -import com.google.common.base.Preconditions; -import com.mojang.authlib.Agent; -import com.mojang.authlib.exceptions.AuthenticationException; -import com.mojang.authlib.yggdrasil.YggdrasilAuthenticationService; -import com.mojang.authlib.yggdrasil.YggdrasilUserAuthentication; -import com.mojang.util.UUIDTypeAdapter; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.util.Session; -import technicianlp.reauth.mixin.MinecraftClientMixin; - -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.net.Proxy; -import java.nio.charset.StandardCharsets; -import java.util.UUID; -import java.util.function.BiFunction; -import java.util.regex.Pattern; - -public final class AuthHelper { - - /** - * Time for which the Validity gets cached (5 Minutes) - */ - private static final long cacheTime = 5 * 1000 * 60L; - - /** - * Reflective YggdrasilUserAuthentication#setAccessToken - */ - private static final Field accessToken; - /** - * Reflective YggdrasilUserAuthentication#checkTokenValidity - */ - private static final Method checkTokenValidity; - /** - * Reflective YggdrasilUserAuthentication#logInWithPassword - */ - private static final Method logInWithPassword; - /** - * Reflective {@link YggdrasilUserAuthentication#YggdrasilUserAuthentication} - */ - private static final Constructor authConstructor; - private static final BiFunction authParameterFactory; - - static { - Class clz = YggdrasilUserAuthentication.class; - accessToken = ReflectionHelper.findField(clz, "accessToken"); - Preconditions.checkNotNull(accessToken, "Reflection failed: accessToken"); - checkTokenValidity = ReflectionHelper.findMethod(clz, "checkTokenValidity"); - Preconditions.checkNotNull(checkTokenValidity, "Reflection failed: checkTokenValidity"); - logInWithPassword = ReflectionHelper.findMethod(clz, "logInWithPassword"); - Preconditions.checkNotNull(logInWithPassword, "Reflection failed: logInWithPassword"); - - // Constructor changed between 1.16.3 and 1.16.4: clientToken needs to be passed into the YUA - Constructor constructor; - constructor = ReflectionHelper.findConstructor(clz, YggdrasilAuthenticationService.class, String.class, Agent.class); - if (constructor != null) { - authParameterFactory = (authService, clientToken) -> new Object[]{authService, clientToken, Agent.MINECRAFT}; - } else { - constructor = ReflectionHelper.findConstructor(clz, YggdrasilAuthenticationService.class, Agent.class); - authParameterFactory = (authService, clientToken) -> new Object[]{authService, Agent.MINECRAFT}; - } - Preconditions.checkNotNull(constructor, "Reflection failed: "); - authConstructor = constructor; - } - - /** - * Current cached Session Validity - */ - private SessionStatus status = SessionStatus.UNKNOWN; - private long lastCheck = 0; - - /** - * Two Authentication Service are required as a - * Validation-Request may not have a clientToken, - * yet a Login-Request requires it. - */ - private final YggdrasilUserAuthentication checkAuth; - private final YggdrasilUserAuthentication loginAuth; - - /** - * Pattern for valid Minecraft Names according to Wiki - */ - private final Pattern namePattern = Pattern.compile("[A-Za-z0-9_]{2,16}"); - - public AuthHelper() { - Proxy proxy = MinecraftClient.getInstance().getNetworkProxy(); - String clientId = UUID.randomUUID().toString(); - Object[] checkAuthParams = authParameterFactory.apply(new YggdrasilAuthenticationService(proxy, (String) null), null); - checkAuth = ReflectionHelper.callConstructor(authConstructor, checkAuthParams); - Object[] loginAuthParams = authParameterFactory.apply(new YggdrasilAuthenticationService(proxy, clientId), clientId); - loginAuth = ReflectionHelper.callConstructor(authConstructor, loginAuthParams); - } - - /** - * Get the cached Validity Status of the accessToken - * Re-Validation is done if the cache expires or it is forced by the Parameter of the same name - */ - public SessionStatus getSessionStatus(boolean force) { - if (force || lastCheck + cacheTime < System.currentTimeMillis()) - status = SessionStatus.UNKNOWN; - - if (status == SessionStatus.UNKNOWN) { - status = SessionStatus.REFRESHING; - lastCheck = System.currentTimeMillis(); - - Thread t = new Thread(this::updateSessionStatus, "ReAuth Session Validator"); - t.setDaemon(true); - t.start(); - } - return status; - } - - /** - * Uses the Validate Endpoint to check the current Tokens validity and updates the cache accordingly - */ - private void updateSessionStatus() { - ReflectionHelper.setField(accessToken, checkAuth, getSession().getAccessToken()); - boolean valid = ReflectionHelper.callMethod(checkTokenValidity, checkAuth); - status = valid ? SessionStatus.VALID : SessionStatus.INVALID; - lastCheck = System.currentTimeMillis(); - } - - /** - * Login with the Supplied Username and Password - * Password is saved to config if {@code savePassword} is true - **/ - public void login(String user, char[] password, boolean savePassword) throws AuthenticationException { - login(user, new String(password), savePassword); - } - - public void login(String user, String password, boolean savePassword) throws AuthenticationException { - loginAuth.setUsername(user); - loginAuth.setPassword(password); - try { - try { - ReflectionHelper.callMethod(logInWithPassword, loginAuth); - } catch (ReflectionHelper.UncheckedInvocationTargetException exception) { - Throwable parent = exception.getCause(); - if (parent instanceof AuthenticationException) - throw (AuthenticationException) parent; - ReAuth.log.error("LogInWithPassword has thrown unexpected exception:", parent); - } - - String username = loginAuth.getSelectedProfile().getName(); - String uuid = UUIDTypeAdapter.fromUUID(loginAuth.getSelectedProfile().getId()); - String access = loginAuth.getAuthenticatedToken(); - String type = loginAuth.getUserType().getName(); - - Session session = new Session(username, uuid, access, type); -// session.setProperties(loginAuth.getUserProperties()); - - loginAuth.logOut(); - - setSession(session); - - ReAuth.config.setCredentials(username, user, savePassword ? password : ""); - } finally { - loginAuth.logOut(); - } - } - - /** - * Sets the Players Offline Username - * UUID is generated from the supplied Username - */ - public void offline(String username) { - UUID uuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + username).getBytes(StandardCharsets.UTF_8)); - setSession(new Session(username, uuid.toString(), "invalid", "legacy")); - ReAuth.log.info("Offline Username set!"); - ReAuth.config.setCredentials(username, "", ""); - } - - private Session getSession() { - return MinecraftClient.getInstance().getSession(); - } - - private void setSession(Session s) { - ((MinecraftClientMixin) MinecraftClient.getInstance()).reauthSetSession(s); - status = SessionStatus.UNKNOWN; - } - - public boolean isValidName(String username) { - return namePattern.matcher(username).matches(); - } - - public enum SessionStatus { - VALID("valid"), - UNKNOWN("unknown"), - REFRESHING("unknown"), - INVALID("invalid"); - - private final String translationKey; - - SessionStatus(String translationKey) { - this.translationKey = "reauth.status." + translationKey; - } - - public String getTranslationKey() { - return translationKey; - } - } -} diff --git a/src/main/java/technicianlp/reauth/ConfigWrapper.java b/src/main/java/technicianlp/reauth/ConfigWrapper.java deleted file mode 100644 index 5b4936a..0000000 --- a/src/main/java/technicianlp/reauth/ConfigWrapper.java +++ /dev/null @@ -1,171 +0,0 @@ -package technicianlp.reauth; - -import me.sargunvohra.mcmods.autoconfig1u.AutoConfig; -import me.sargunvohra.mcmods.autoconfig1u.ConfigHolder; -import me.sargunvohra.mcmods.autoconfig1u.ConfigManager; -import me.sargunvohra.mcmods.autoconfig1u.serializer.Toml4jConfigSerializer; -import net.fabricmc.loader.api.FabricLoader; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.security.GeneralSecurityException; -import java.security.SecureRandom; -import java.util.Base64; - -public final class ConfigWrapper { - - public static final String CONFIG_NAME = "../reauth"; - - static void registerConfig() { - AutoConfig.register(Configuration.class, Toml4jConfigSerializer::new); - } - - private Configuration config; - private Crypto crypto; - - public ConfigWrapper() { - if (!Boolean.getBoolean("mods.reauth.disableCrypto")) { - try { - this.crypto = new Crypto(); - } catch (GeneralSecurityException e) { - ReAuth.log.error("Unable to locate cryptographic algorithms. Credentials cannot be saved", e); - } - } else { - ReAuth.log.error("Crypto disabled by commandline"); - } - } - - public void onLoad(Configuration config0) { - this.config = config0; - if (config.version == 1) { - convertConfigV1(); - return; - } - - Configuration.Credentials credentials = config.credentials; - byte[] salt = null; - boolean saltLoaded = false; - String saltRaw = credentials.salt; - if (!saltRaw.isEmpty()) { - try { - salt = Base64.getDecoder().decode(saltRaw); - if (salt.length == 16) - saltLoaded = true; - else - ReAuth.log.error("Salt corrupted, saved credentials cannot be recovered"); - } catch (IllegalArgumentException e) { - ReAuth.log.error("Could not load salt, saved credentials cannot be recovered", e); - } - } - if (!saltLoaded) { - salt = createSalt(); - credentials.salt = Base64.getEncoder().encodeToString(salt); - } - if (crypto != null) - crypto.setup(getPath(), salt); - } - - /** - * Migrate Version 1 > 2 - */ - private void convertConfigV1() { - config.version = 2; - byte[] salt = createSalt(); - Configuration.Credentials credentials = config.credentials; - credentials.salt = Base64.getEncoder().encodeToString(salt); - if (crypto != null) - crypto.setup(getPath(), salt); - setCredentials("", credentials.username, credentials.password); - } - - /** - * Path is used as the Key for encryption - * Resolve symlinks so as to allow the resulting hash to be the same across symlinks - * Fall back to "local" path if that lookup fails (somehow) - */ - private String getPath() { - Path configFile = FabricLoader.getInstance().getConfigDir().resolve(CONFIG_NAME + ".toml"); - try { - if (Files.exists(configFile)) - return configFile.toRealPath().toString(); - } catch (IOException e) { - ReAuth.log.error("Could not resolve real path", e); - } - return configFile.toString(); - } - - private byte[] createSalt() { - byte[] salt = new byte[16]; - new SecureRandom().nextBytes(salt); - return salt; - } - - /** - * Updates the credentials in Config - * Credentials are encrypted; If encryption is unavailable they are not saved - * Empty String is not encrypted - */ - public void setCredentials(String profileName, String username, String password) { - if (crypto == null) - return; - Configuration.Credentials credentials = config.credentials; - credentials.profile = profileName; - if (!username.isEmpty()) - username = crypto.encryptString(username); - credentials.username = username; - if (!password.isEmpty()) - password = crypto.encryptString(password); - credentials.password = password; - - ConfigHolder cfg = AutoConfig.getConfigHolder(Configuration.class); - if (cfg instanceof ConfigManager) { - ((ConfigManager) cfg).save(); - } else { - ReAuth.log.warn("Unknown ConfigHolder: cannot save config"); - } - } - - /** - * Decrypt Username - * Empty String means offline Username -> return that - */ - public String getUsername() { - Configuration.Credentials credentials = config.credentials; - String username = credentials.username; - if (username.isEmpty()) - return credentials.profile; - if (crypto != null) - return crypto.decryptString(username); - return ""; - } - - /** - * Decrypt Password - * Empty String is not decrypted - */ - public String getPassword() { - String password = config.credentials.password; - if (!password.isEmpty() && crypto != null) - return crypto.decryptString(password); - return ""; - } - - public String getProfile() { - return config.credentials.profile; - } - - /** - * Is Cryptography available - */ - public boolean hasCrypto() { - return crypto != null; - } - - public boolean hasCredentials() { - return hasCrypto() && - !config.credentials.username.isEmpty() && - !config.credentials.password.isEmpty() && - !config.credentials.profile.isEmpty(); - } -} diff --git a/src/main/java/technicianlp/reauth/Configuration.java b/src/main/java/technicianlp/reauth/Configuration.java deleted file mode 100644 index 6c9c606..0000000 --- a/src/main/java/technicianlp/reauth/Configuration.java +++ /dev/null @@ -1,50 +0,0 @@ -package technicianlp.reauth; - -import me.sargunvohra.mcmods.autoconfig1u.ConfigData; -import me.sargunvohra.mcmods.autoconfig1u.annotation.Config; -import me.sargunvohra.mcmods.autoconfig1u.annotation.ConfigEntry; -import me.sargunvohra.mcmods.autoconfig1u.shadowed.blue.endless.jankson.Comment; - -@Config(name = ConfigWrapper.CONFIG_NAME) -public final class Configuration implements ConfigData { - @ConfigEntry.Gui.Excluded - @ConfigEntry.BoundedDiscrete(min = 1, max = 2) - @Comment("Version Number of the Configuration File") - public int version = 2; - -// @Comment("Credentials for login, encrypted with AES-CBC-PKCS5Padding and PBKDF2WithHmacSHA512 " + -// "based on the Path of this file and the contained Salt.\n" + -// "Manually editing one of the Encrypted Values or the Salt, is inadvisable - " + -// "Reset them to \"\" if required\n" + -// "Keep in mind that while the credentials are no longer humanly readable, " + -// "they could still be decrypted by malicious third parties") - @ConfigEntry.Gui.TransitiveObject - Credentials credentials = new Credentials(); - - static final class Credentials { - @Comment("One of the values required to decrypt the credentials\n" + - "See https://en.wikipedia.org/wiki/Salt_(cryptography) for details") - @ConfigEntry.Gui.Tooltip(count = 2) - public String salt = ""; - - @Comment("The Name of the last used Profile") - @ConfigEntry.Gui.Tooltip - public String profile = ""; - - @Comment("Your Username (encrypted)") - @ConfigEntry.Gui.Tooltip - public String username = ""; - - @Comment("Your Password (encrypted)") - @ConfigEntry.Gui.Tooltip - public String password = ""; - } - - @Override - public void validatePostLoad() throws ValidationException { - if (1 <= version && version <= 2) - ReAuth.config.onLoad(this); - else - throw new ValidationException("Unknown Version"); - } -} diff --git a/src/main/java/technicianlp/reauth/Crypto.java b/src/main/java/technicianlp/reauth/Crypto.java deleted file mode 100644 index 94b4290..0000000 --- a/src/main/java/technicianlp/reauth/Crypto.java +++ /dev/null @@ -1,100 +0,0 @@ -package technicianlp.reauth; - -import javax.crypto.Cipher; -import javax.crypto.SecretKeyFactory; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.PBEKeySpec; -import javax.crypto.spec.SecretKeySpec; -import java.nio.charset.StandardCharsets; -import java.security.GeneralSecurityException; -import java.security.NoSuchAlgorithmException; -import java.security.spec.InvalidKeySpecException; -import java.util.Arrays; -import java.util.Base64; - -/** - * Basic encryption for credentials via PBKDF2 and AES-CBC
    - * Key and IV for AES are derived from the PBKDF2-Hash
    - */ -final class Crypto { - - private static final int iterations = 100_000; - - private final Cipher aes; - private final SecretKeyFactory pbkdf; - - private String key; - private byte[] salt; - private byte[] hash; - - public Crypto() throws GeneralSecurityException { - if (Cipher.getMaxAllowedKeyLength("AES") < 256) { - removeJceRestriction(); - } - aes = Cipher.getInstance("AES/CBC/PKCS5Padding"); - pbkdf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512"); - } - - private void removeJceRestriction() throws NoSuchAlgorithmException { - ReAuth.log.warn("Cryptography is restricted in this Java installation"); - ReAuth.log.warn("Please complain to Mojang for shipping a 5 year old Java version"); - new JceWorkaround().removeCryptographyRestrictions(); - if (Cipher.getMaxAllowedKeyLength("AES") < 256) { - ReAuth.log.error("Failed to remove cryptography restriction - saving credentials will not be available"); - throw new NoSuchAlgorithmException("AES 256 unsupported by JVM"); - } else { - ReAuth.log.info("Cryptography restriction removed successfully"); - } - } - - private byte[] crypt(int mode, byte[] secret) throws GeneralSecurityException { - SecretKeySpec secretKey = new SecretKeySpec(getHash(), 0, 32, "AES"); - IvParameterSpec ivParameterSpec = new IvParameterSpec(getHash(), 32, 16); - - aes.init(mode, secretKey, ivParameterSpec); - return aes.doFinal(secret); - } - - public String encryptString(String string) { - try { - byte[] raw = string.getBytes(StandardCharsets.UTF_8); - byte[] enc = crypt(Cipher.ENCRYPT_MODE, raw); - return Base64.getEncoder().encodeToString(enc); - } catch (GeneralSecurityException e) { - ReAuth.log.error("Unexpected Crypto Exception", e); - } - return ""; - } - - public String decryptString(String string) { - try { - byte[] raw = Base64.getDecoder().decode(string); - byte[] dec = crypt(Cipher.DECRYPT_MODE, raw); - return new String(dec, StandardCharsets.UTF_8); - } catch (GeneralSecurityException e) { - ReAuth.log.error("Unexpected Crypto Exception", e); - } - return ""; - } - - /** - * set Parameters, invalidate hash if parameters changed - */ - public void setup(String key, byte[] salt) { - if (key.equals(this.key) && Arrays.equals(salt, this.salt)) - return; - this.key = key; - this.salt = salt; - this.hash = null; - } - - /** - * Lazy generate hash - */ - private byte[] getHash() throws InvalidKeySpecException { - if (hash == null) { - hash = pbkdf.generateSecret(new PBEKeySpec(key.toCharArray(), salt, iterations, 512)).getEncoded(); - } - return hash; - } -} diff --git a/src/main/java/technicianlp/reauth/EventHandler.java b/src/main/java/technicianlp/reauth/EventHandler.java new file mode 100644 index 0000000..5c7cae3 --- /dev/null +++ b/src/main/java/technicianlp/reauth/EventHandler.java @@ -0,0 +1,104 @@ +package technicianlp.reauth; + +import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; +import net.fabricmc.fabric.api.client.screen.v1.Screens; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.ConnectScreen; +import net.minecraft.client.gui.screen.DisconnectedScreen; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.TitleScreen; +import net.minecraft.client.gui.screen.multiplayer.MultiplayerScreen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.client.resource.language.I18n; +import net.minecraft.client.util.Session; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.text.Text; +import technicianlp.reauth.configuration.Profile; +import technicianlp.reauth.configuration.ProfileConstants; +import technicianlp.reauth.gui.MainScreen; +import technicianlp.reauth.session.SessionChecker; +import technicianlp.reauth.session.SessionStatus; +import technicianlp.reauth.util.ReflectionUtils; + +import java.lang.reflect.Field; +import java.util.List; + +public final class EventHandler { + + private static final Field disconnectMessage = ReflectionUtils.findObfuscatedField(DisconnectedScreen.class, + "f_95988_", "reason"); + + public static void register() { + ScreenEvents.AFTER_INIT.register(EventHandler::afterInit); + ScreenEvents.BEFORE_INIT.register(EventHandler::beforeInit); + } + + public static void afterInit(MinecraftClient client, Screen screen, int scaledWidth, int scaledHeight) { + if (screen instanceof MultiplayerScreen) { + // Add Button to MultiplayerScreen + Screens.getButtons(screen).add(new ButtonWidget(5, 5, 100, 20, + Text.translatable("reauth.gui.button"), b -> openAuthenticationScreen(screen))); + ScreenEvents.afterRender(screen).register(EventHandler::afterRender); + } else if (screen instanceof TitleScreen) { + // Support for Custom Main Menu (add button outside of viewport) + Screens.getButtons(screen).add(new ButtonWidget(-50, -50, 20, 20, + Text.translatable("reauth.gui.button"), b -> openAuthenticationScreen(screen))); + } else if (screen instanceof DisconnectedScreen) { + // Add Buttons to DisconnectedScreen if its reason is an invalid session + handleDisconnectScreen(screen); + } else if (screen instanceof ConnectScreen) { + // Save Screen to retrieve server later + ReconnectHelper.setConnectScreen((ConnectScreen) screen); + } + } + + private static void handleDisconnectScreen(Screen screen) { + if ("connect.failed".equals(ReconnectHelper.getTranslationKey(screen.getTitle(), false))) { + if (ReconnectHelper.getTranslationKey(ReflectionUtils.getField(disconnectMessage, screen), true) + .startsWith("disconnect.loginFailed")) { + List buttons = Screens.getButtons(screen); + ClickableWidget menu = buttons.get(0); + + Profile profile = ReAuth.profiles.getProfile(); + Text retryText; + if (profile != null) { + retryText = Text.translatable("reauth.retry", profile.getValue(ProfileConstants.NAME, + "Steve")); + } else { + retryText = Text.translatable("reauth.retry.disabled"); + } + ButtonWidget retryButton = new ButtonWidget(menu.x, menu.y + 25, 200, 20, retryText, + b -> ReconnectHelper + .retryLogin(profile)); + if (profile == null || !ReconnectHelper.hasConnectionInfo()) { + retryButton.active = false; + } + buttons.add(retryButton); + } + } + } + + private static void openAuthenticationScreen(Screen screen) { + Screens.getClient(screen).setScreen(new MainScreen(screen)); + } + + public static void afterRender(Screen screen, MatrixStack matrices, int mouseX, int mouseY, float tickDelta) { + if (screen instanceof MultiplayerScreen) { + MinecraftClient client = Screens.getClient(screen); + Session session = client.getSession(); + SessionStatus state = SessionChecker.getSessionStatus(session.getAccessToken(), session.getUuid()); + Screens.getTextRenderer(screen).drawWithShadow(matrices, I18n.translate(state.getTranslationKey()), + 110, 10, + 0xFFFFFFFF); + } + } + + // TODO: add refresh on clicking status + public static void beforeInit(MinecraftClient client, Screen screen, int scaledWidth, int scaledHeight) { + if (screen instanceof MultiplayerScreen && MinecraftClient.getInstance().currentScreen instanceof + MultiplayerScreen && Screen.hasShiftDown()) { + SessionChecker.invalidate(); + } + } +} diff --git a/src/main/java/technicianlp/reauth/JceWorkaround.java b/src/main/java/technicianlp/reauth/JceWorkaround.java deleted file mode 100644 index a33bf8b..0000000 --- a/src/main/java/technicianlp/reauth/JceWorkaround.java +++ /dev/null @@ -1,59 +0,0 @@ -package technicianlp.reauth; - -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -import java.security.PermissionCollection; -import java.util.Map; - -final class JceWorkaround { - - /** - * Java had for legal reasons limited the allowed strength of cryptographic algorithms. - * Historically to disable this restriction the so called "Java Cryptography Extension (JCE) Unlimited Strength - * Jurisdiction Policy Files" has to be installed within the JRE directory. - * Since update 151 (October 17, 2017) this restrictions can be disabled programmatically - * and has since been disabled by default in update 161 (January 16, 2018). - *

    - * Since Mojang for some insane reason ships the 5 year old update 51 (July 14, 2015), installation of the policy files - * would be necessary. Since installation of those files cannot be required of the user, a workaround has been found in - * https://stackoverflow.com/questions/1179672 and is used to disable this restriction at runtime: - *

    - * JceSecurity.isRestricted = false; - * JceSecurity.defaultPolicy.perms.clear(); - * JceSecurity.defaultPolicy.add(CryptoAllPermission.INSTANCE); - *

    - * The alternative to this workaround would have been to drop the AES key-length from 256 bits to 128 bits. - */ - public void removeCryptographyRestrictions() { - try { - final Class jceSecurity = Class.forName("javax.crypto.JceSecurity"); - final Class cryptoPermissions = Class.forName("javax.crypto.CryptoPermissions"); - final Class cryptoAllPermission = Class.forName("javax.crypto.CryptoAllPermission"); - - setFinalField(jceSecurity, "isRestricted", null, true); - - final PermissionCollection defaultPolicy = getFieldValue(jceSecurity, "defaultPolicy", null); - ((Map) getFieldValue(cryptoPermissions, "perms", defaultPolicy)).clear(); - defaultPolicy.add(getFieldValue(cryptoAllPermission, "INSTANCE", null)); - } catch (final Exception e) { - ReAuth.log.error("Exception removing cryptography restrictions", e); - } - } - - @SuppressWarnings("unchecked") - private E getFieldValue(Class clz, String name, Object target) throws ReflectiveOperationException { - Field field = clz.getDeclaredField(name); - field.setAccessible(true); - return (E) field.get(target); - } - - @SuppressWarnings("SameParameterValue") - private void setFinalField(Class clz, String name, Object target, Object value) throws ReflectiveOperationException { - Field field = clz.getDeclaredField(name); - field.setAccessible(true); - final Field modifiersField = Field.class.getDeclaredField("modifiers"); - modifiersField.setAccessible(true); - modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); - field.set(target, value); - } -} diff --git a/src/main/java/technicianlp/reauth/ReAuth.java b/src/main/java/technicianlp/reauth/ReAuth.java index 82b46a3..c008267 100644 --- a/src/main/java/technicianlp/reauth/ReAuth.java +++ b/src/main/java/technicianlp/reauth/ReAuth.java @@ -1,23 +1,52 @@ package technicianlp.reauth; -import net.fabricmc.api.ModInitializer; +import net.fabricmc.api.ClientModInitializer; import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.ModContainer; +import net.minecraft.client.resource.language.I18n; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; +import technicianlp.reauth.configuration.Config; +import technicianlp.reauth.configuration.ProfileList; +import technicianlp.reauth.mojangfix.MojangJavaFix; -public final class ReAuth implements ModInitializer { +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiFunction; + +public final class ReAuth implements ClientModInitializer { public static final Logger log = LogManager.getLogger("ReAuth"); - public static final AuthHelper auth = new AuthHelper(); + public static final ExecutorService executor = Executors.newCachedThreadPool(new ReAuthThreadFactory()); public static final VersionChecker versionCheck = new VersionChecker(); - public static ConfigWrapper config = new ConfigWrapper(); - public static ModContainer container; + public static final Config config = new Config(); + public static ProfileList profiles; + public static BiFunction i18n = I18n::translate; + public static ModContainer container = FabricLoader.getInstance().getModContainer("reauth").orElse(null); @Override - public void onInitialize() { - ConfigWrapper.registerConfig(); - container = FabricLoader.getInstance().getModContainer("reauth").orElse(null); + public void onInitializeClient() { + MojangJavaFix.fixMojangJava(); + profiles = config.getProfileList(); versionCheck.runVersionCheck(); + EventHandler.register(); + } + + private static final class ReAuthThreadFactory implements ThreadFactory { + private final AtomicInteger threadNumber = new AtomicInteger(1); + private final ThreadGroup group = new ThreadGroup("ReAuth"); + + @Override + public Thread newThread(@NotNull Runnable runnable) { + Thread t = new Thread(this.group, runnable, "ReAuth-" + this.threadNumber.getAndIncrement()); + if (t.isDaemon()) + t.setDaemon(false); + if (t.getPriority() != Thread.NORM_PRIORITY) + t.setPriority(Thread.NORM_PRIORITY); + return t; + } } } diff --git a/src/main/java/technicianlp/reauth/ReconnectHelper.java b/src/main/java/technicianlp/reauth/ReconnectHelper.java index 4f5ffb9..7fb3a13 100644 --- a/src/main/java/technicianlp/reauth/ReconnectHelper.java +++ b/src/main/java/technicianlp/reauth/ReconnectHelper.java @@ -1,13 +1,13 @@ package technicianlp.reauth; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.screens.ConnectScreen; -import net.minecraft.client.multiplayer.resolver.ServerAddress; -import net.minecraft.network.Connection; -import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.ComponentContents; -import net.minecraft.network.chat.MutableComponent; -import net.minecraft.network.chat.contents.TranslatableContents; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.ConnectScreen; +import net.minecraft.client.network.ServerAddress; +import net.minecraft.network.ClientConnection; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.text.TextContent; +import net.minecraft.text.TranslatableTextContent; import technicianlp.reauth.authentication.flows.Flow; import technicianlp.reauth.authentication.flows.Flows; import technicianlp.reauth.configuration.Profile; @@ -20,26 +20,28 @@ public final class ReconnectHelper { - private static final Field managerField = ReflectionUtils.findObfuscatedField(ConnectScreen.class, "f_95684_", "connection"); - private static final Field previousField = ReflectionUtils.findObfuscatedField(ConnectScreen.class, "f_95686_", "parent"); + private static final Field managerField = ReflectionUtils.findObfuscatedField(ConnectScreen.class, "f_95684_", + "connection"); + private static final Field previousField = ReflectionUtils.findObfuscatedField(ConnectScreen.class, "f_95686_", + "parent"); private static ConnectScreen screen; /** - * Extract the translationKey from the supplied {@link Component} + * Extract the translationKey from the supplied {@link Text} * - * @param nested whether to extract the key from the nested {@link Component} instead + * @param nested whether to extract the key from the nested {@link Text} instead */ public static String getTranslationKey(Object component, boolean nested) { - if (component instanceof MutableComponent mutableComponent) { - ComponentContents contents = mutableComponent.getContents(); - if (contents instanceof TranslatableContents translatableContents) { + if (component instanceof MutableText mutableText) { + TextContent contents = mutableText.getContent(); + if (contents instanceof TranslatableTextContent translatableTextContent) { if (nested) { - Object[] args = translatableContents.getArgs(); + Object[] args = translatableTextContent.getArgs(); if (args.length >= 1) { return getTranslationKey(args[0], false); } } else { - return translatableContents.getKey(); + return translatableTextContent.getKey(); } } } @@ -55,17 +57,18 @@ public static boolean hasConnectionInfo() { } public static void retryLogin(Profile profile) { - Flow flow = FlowScreen.open(Flows::loginWithProfile, profile, true); - flow.thenRunAsync(ReconnectHelper::connect, Minecraft.getInstance()); + Flow flow = FlowScreen.open(Flows::loginWithProfile, profile); + flow.thenRunAsync(ReconnectHelper::connect, MinecraftClient.getInstance()); } private static void connect() { if (screen != null) { - SocketAddress add = ReflectionUtils.getField(managerField, screen).getRemoteAddress(); + SocketAddress add = ReflectionUtils.getField(managerField, screen).getAddress(); if (add instanceof InetSocketAddress address) { - Minecraft minecraft = Minecraft.getInstance(); + MinecraftClient minecraft = MinecraftClient.getInstance(); ServerAddress server = new ServerAddress(address.getHostString(), address.getPort()); - ConnectScreen.startConnecting(ReflectionUtils.getField(previousField, screen), minecraft, server, minecraft.getCurrentServer()); + ConnectScreen.connect(ReflectionUtils.getField(previousField, screen), minecraft, server, + minecraft.getCurrentServerEntry()); } } } diff --git a/src/main/java/technicianlp/reauth/ReflectionHelper.java b/src/main/java/technicianlp/reauth/ReflectionHelper.java deleted file mode 100644 index c20c42a..0000000 --- a/src/main/java/technicianlp/reauth/ReflectionHelper.java +++ /dev/null @@ -1,83 +0,0 @@ -package technicianlp.reauth; - -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -public final class ReflectionHelper { - - public static Method findMethod(Class clz, String name, Class... parameterTypes) { - try { - Method method = clz.getDeclaredMethod(name, parameterTypes); - method.setAccessible(true); - return method; - } catch (ReflectiveOperationException ignored) { - return null; - } - } - - public static T callMethod(Method method, Object target, Object... args) throws UncheckedInvocationTargetException { - try { - //noinspection unchecked - return (T) method.invoke(target, args); - } catch (InvocationTargetException e) { - throw new UncheckedInvocationTargetException(e); - } catch (ReflectiveOperationException e) { - throw new RuntimeException("Failed reflective Method call", e); - } - } - - public static Constructor findConstructor(Class clz, Class... parameterTypes) { - try { - Constructor constructor = clz.getDeclaredConstructor(parameterTypes); - constructor.setAccessible(true); - return constructor; - } catch (ReflectiveOperationException ignored) { - return null; - } - } - - public static T callConstructor(Constructor constructor, Object... args) throws UncheckedInvocationTargetException { - try { - return constructor.newInstance(args); - } catch (InvocationTargetException e) { - throw new UncheckedInvocationTargetException(e); - } catch (ReflectiveOperationException e) { - throw new RuntimeException("Failed reflective Constructor call", e); - } - } - - public static Field findField(Class clz, String name) { - try { - Field field = clz.getDeclaredField(name); - field.setAccessible(true); - return field; - } catch (ReflectiveOperationException ignored) { - return null; - } - } - - public static void setField(Field field, Object target, Object value){ - try { - field.set(target, value); - } catch (ReflectiveOperationException throwable) { - throw new RuntimeException("Failed Reflective set", throwable); - } - } - - public static T getField(Field field, Object target){ - try { - //noinspection unchecked - return (T) field.get(target); - } catch (ReflectiveOperationException throwable) { - throw new RuntimeException("Failed Reflective get", throwable); - } - } - - public static class UncheckedInvocationTargetException extends RuntimeException { - public UncheckedInvocationTargetException(InvocationTargetException e) { - super(e.getCause()); - } - } -} diff --git a/src/main/java/technicianlp/reauth/VersionChecker.java b/src/main/java/technicianlp/reauth/VersionChecker.java index 7243d60..5596e43 100644 --- a/src/main/java/technicianlp/reauth/VersionChecker.java +++ b/src/main/java/technicianlp/reauth/VersionChecker.java @@ -1,12 +1,6 @@ package technicianlp.reauth; -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.*; import com.google.gson.reflect.TypeToken; import net.minecraft.SharedConstants; import org.apache.commons.io.IOUtils; @@ -15,14 +9,16 @@ import java.lang.reflect.Type; import java.net.URL; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; public final class VersionChecker implements Runnable { private static final String MC_VERSION = SharedConstants.getGameVersion().getReleaseTarget(); - private static final String JSON_URL = "https://github.com/TechnicianLP/ReAuth/raw/master/update.json"; - private static final Type mapType = new TypeToken>() {}.getType(); + private static final String JSON_URL = "https://github.com/NgoKimPhu/ReAuth/raw/master/update.json"; + private static final Type mapType = new TypeToken>() { + }.getType(); private Status status = Status.UNKNOWN; private String changes = null; @@ -89,12 +85,8 @@ public String getChanges() { } private static int versionToInt(String version) { - String[] split = version.split("\\.", 3); - int ver = 0; - for (String s : split) { - ver = (ver << 8) | Integer.parseInt(s); - } - return ver; + return Arrays.stream(version.split("[.-]")).limit(3) + .mapToInt(Integer::parseInt).reduce(0, (ver, i) -> (ver << 8) | i); } private static class VersionJson { diff --git a/src/main/java/technicianlp/reauth/configuration/Config.java b/src/main/java/technicianlp/reauth/configuration/Config.java index 553af05..6621f7c 100644 --- a/src/main/java/technicianlp/reauth/configuration/Config.java +++ b/src/main/java/technicianlp/reauth/configuration/Config.java @@ -1,38 +1,26 @@ package technicianlp.reauth.configuration; -import net.minecraftforge.common.ForgeConfigSpec; -import net.minecraftforge.fml.config.ModConfig; +import com.electronwill.nightconfig.core.CommentedConfig; +import com.electronwill.nightconfig.core.file.CommentedFileConfig; +import com.electronwill.nightconfig.toml.TomlFormat; +import net.fabricmc.loader.api.FabricLoader; import technicianlp.reauth.ReAuth; import technicianlp.reauth.crypto.Crypto; import java.io.IOException; public final class Config { - - private final ForgeConfigSpec spec; - private ModConfig config; - + public static final String CONFIG_NAME = "reauth"; + private final CommentedFileConfig config; private final ProfileList profileList; public Config() { - ForgeConfigSpec.Builder builder = new ForgeConfigSpec.Builder(); - - builder.comment("Version Number of the Configuration File") - .defineInRange("version", 3, 3, 3); - - this.profileList = new ProfileList(this, builder); - - this.spec = builder.build(); - } - - public final ForgeConfigSpec getSpec() { - return this.spec; - } - - public final void updateConfig(ModConfig config) { - this.config = config; + this.config = CommentedFileConfig.builder( + FabricLoader.getInstance().getConfigDir().resolve(CONFIG_NAME + ".toml"), + TomlFormat.instance()).autosave().build(); + config.load(); Crypto.updateConfigPath(this.getPath(config)); - this.profileList.updateConfig(config); + this.profileList = new ProfileList(this, config); } public final void save() { @@ -47,12 +35,12 @@ public final ProfileList getProfileList() { * Get the Absolute path of the Config with symlinks resolved. * Fall back to "local" path if that lookup fails (somehow) */ - private String getPath(ModConfig config) { + private String getPath(CommentedConfig config) { try { - return config.getFullPath().toRealPath().toString(); + return ((CommentedFileConfig) config).getNioPath().toRealPath().toString(); } catch (IOException e) { ReAuth.log.error("Could not resolve real path", e); - return config.getFullPath().toString(); + return ((CommentedFileConfig) config).getNioPath().toString(); } } } diff --git a/src/main/java/technicianlp/reauth/configuration/ProfileList.java b/src/main/java/technicianlp/reauth/configuration/ProfileList.java index a4160a5..8eb098e 100644 --- a/src/main/java/technicianlp/reauth/configuration/ProfileList.java +++ b/src/main/java/technicianlp/reauth/configuration/ProfileList.java @@ -1,51 +1,39 @@ package technicianlp.reauth.configuration; import com.electronwill.nightconfig.core.CommentedConfig; -import com.electronwill.nightconfig.toml.TomlFormat; -import net.minecraftforge.common.ForgeConfigSpec; -import net.minecraftforge.fml.config.ModConfig; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; + +import java.util.*; import java.util.function.Supplier; public final class ProfileList { + public static final String PROFILES_PATH = "profiles"; private final Config configuration; - private final ForgeConfigSpec.ConfigValue> profilesProperty; + private final List profilesProperty; - private Supplier configSupplier = TomlFormat::newConfig; + private final Supplier configSupplier; - ProfileList(Config configuration, ForgeConfigSpec.Builder builder) { + ProfileList(Config configuration, CommentedConfig config) { this.configuration = configuration; - this.profilesProperty = builder - .comment("Saved Profiles. Check Documentation for Info & Syntax") - .define("profiles", this::createDefaultProfileList, this::validateProfileList); - } - - final void updateConfig(ModConfig config) { - this.configSupplier = config.getConfigData()::createSubConfig; - - List list = new ArrayList<>(this.profilesProperty.get()); - this.correctProfiles(list); - this.saveProfiles(list); + this.configSupplier = config::createSubConfig; + this.profilesProperty = config.getOrElse(PROFILES_PATH, + () -> config.set(PROFILES_PATH, new ArrayList<>())); + this.correctProfiles(this.profilesProperty); + this.saveProfiles(); } public final void storeProfile(Profile profile) { - List list = new ArrayList<>(this.profilesProperty.get()); + List list = this.profilesProperty; if (list.isEmpty()) { list.add(profile.getConfig()); - } else { + } else { // TODO check this logic list.set(0, profile.getConfig()); } - this.saveProfiles(list); + this.saveProfiles(); } public final Profile getProfile() { - List list = this.profilesProperty.get(); + List list = this.profilesProperty; if (list.isEmpty()) { return null; } @@ -88,11 +76,11 @@ private void correctProfiles(List profileList) { * Save the list of Profiles to config * Add a dummy profile if the list is empty */ - private void saveProfiles(List list) { + private void saveProfiles() { + List list = this.profilesProperty; if (list.isEmpty()) { list.add(this.createPlaceholderConfig()); } - this.profilesProperty.set(list); this.configuration.save(); } @@ -110,8 +98,10 @@ private List createDefaultProfileList() { } /** - * checks whether the given Object is a {@link List} and all it's elements are an instance of {@link CommentedConfig} - * This effectively checks whether the provided {@link Object} is an Array of Tables as described by the TOML specification + * checks whether the given Object is a {@link List} and all it's elements are an instance of + * {@link CommentedConfig} + * This effectively checks whether the provided {@link Object} is an Array of Tables as described by the TOML + * specification */ private boolean validateProfileList(Object el) { return el instanceof List && ((List) el).stream().allMatch(CommentedConfig.class::isInstance); diff --git a/src/main/java/technicianlp/reauth/gui/AbstractScreen.java b/src/main/java/technicianlp/reauth/gui/AbstractScreen.java index a86d301..c1ba6b3 100644 --- a/src/main/java/technicianlp/reauth/gui/AbstractScreen.java +++ b/src/main/java/technicianlp/reauth/gui/AbstractScreen.java @@ -1,21 +1,24 @@ package technicianlp.reauth.gui; -import com.mojang.blaze3d.systems.RenderSystem; -import com.mojang.blaze3d.vertex.BufferBuilder; -import com.mojang.blaze3d.vertex.DefaultVertexFormat; -import com.mojang.blaze3d.vertex.PoseStack; -import com.mojang.blaze3d.vertex.Tesselator; -import com.mojang.blaze3d.vertex.VertexFormat; -import net.minecraft.client.gui.components.Button; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.client.renderer.GameRenderer; -import net.minecraft.client.resources.language.I18n; -import net.minecraft.network.chat.Component; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.resource.language.I18n; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.text.Text; + +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; abstract class AbstractScreen extends Screen { static final int BUTTON_WIDTH = 196; + private final Screen parent; + + private final CompletableFuture closed = new CompletableFuture<>(); + private final String title; protected int baseX; @@ -26,64 +29,72 @@ abstract class AbstractScreen extends Screen { protected int screenHeight = 175; AbstractScreen(String title) { - super(Component.translatable("reauth.gui.auth.title")); + this(title, MinecraftClient.getInstance().currentScreen); + } + + AbstractScreen(String title, Screen parent) { + super(Text.translatable(title)); this.title = title; + this.parent = parent; } @Override public void init() { super.init(); - this.getMinecraft().keyboardHandler.setSendRepeatsToGui(true); + Objects.requireNonNull(this.client).keyboard.setRepeatEvents(true); this.centerX = this.width / 2; this.baseX = this.centerX - this.screenWidth / 2; this.centerY = this.height / 2; this.baseY = this.centerY - this.screenHeight / 2; - Button cancel = new Button(this.centerX + this.screenWidth / 2 - 22, this.baseY + 2, 20, 20, Component.translatable("reauth.gui.close"), (b) -> this.onClose()); - this.addRenderableWidget(cancel); + ButtonWidget cancel = new ButtonWidget(this.centerX + this.screenWidth / 2 - 22, this.baseY + 2, 20, 20, + Text.translatable("reauth.gui.close"), (b) -> this.close()); + this.addDrawableChild(cancel); } @Override - public void render(PoseStack poseStack, int mouseX, int mouseY, float partialTicks) { - this.fillGradient(poseStack, 0, 0, this.width, this.height, 0xc0101010, 0xd0101010); - - // modified renderDirtBackground(0); - Tesselator tesselator = Tesselator.getInstance(); - BufferBuilder bufferbuilder = tesselator.getBuilder(); - RenderSystem.setShader(GameRenderer::getPositionTexColorShader); - RenderSystem.setShaderTexture(0, BACKGROUND_LOCATION); - RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); - bufferbuilder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX_COLOR); - bufferbuilder.vertex(this.baseX, this.baseY + this.screenHeight, 0.0D).uv(0.0F, this.screenHeight / 32.0F).color(80, 80, 80, 255).endVertex(); - bufferbuilder.vertex(this.baseX + this.screenWidth, this.baseY + this.screenHeight, 0.0D).uv(this.screenWidth / 32.0F, this.screenHeight / 32.0F).color(80, 80, 80, 255).endVertex(); - bufferbuilder.vertex(this.baseX + this.screenWidth, this.baseY, 0.0D).uv(this.screenWidth / 32.0F, 0F).color(80, 80, 80, 255).endVertex(); - bufferbuilder.vertex(this.baseX, this.baseY, 0.0D).uv(0.0F, 0F).color(80, 80, 80, 255).endVertex(); - tesselator.end(); - - super.render(poseStack, mouseX, mouseY, partialTicks); - - this.font.drawShadow(poseStack, I18n.get(this.title), this.centerX - (BUTTON_WIDTH / 2f), this.baseY + 8, 0xFFFFFFFF); + public void render(MatrixStack matrixStack, int mouseX, int mouseY, float partialTicks) { + if (this.closed.isDone()) { + try { + this.requestClose(this.closed.get()); + } catch (InterruptedException | ExecutionException e) { + this.requestClose(true); + } + return; + } + + this.renderBackground(matrixStack); + super.render(matrixStack, mouseX, mouseY, partialTicks); + + AbstractScreen.drawCenteredText(matrixStack, this.textRenderer, I18n.translate(this.title), + this.centerX, this.baseY + 8, 0xFFFFFF); } protected final void transitionScreen(Screen newScreen) { - this.getMinecraft().popGuiLayer(); - this.getMinecraft().pushGuiLayer(newScreen); + Objects.requireNonNull(this.client).setScreen(newScreen); } protected void requestClose(boolean completely) { + if (!MinecraftClient.getInstance().isOnThread()) { + this.closed.complete(completely); + return; + } + + Screen parent = this.parent; if (completely) { - super.onClose(); - } else { - this.transitionScreen(new MainScreen()); + while (parent instanceof AbstractScreen abstractScreen) { + parent = abstractScreen.parent; + } } + transitionScreen(parent); } /** * Method called to request this Screen to close itself */ @Override - public final void onClose() { + public final void close() { this.requestClose(false); } @@ -93,6 +104,6 @@ public final void onClose() { @Override public void removed() { super.removed(); - this.getMinecraft().keyboardHandler.setSendRepeatsToGui(false); + Objects.requireNonNull(this.client).keyboard.setRepeatEvents(false); } } diff --git a/src/main/java/technicianlp/reauth/gui/AuthScreen.java b/src/main/java/technicianlp/reauth/gui/AuthScreen.java deleted file mode 100644 index 6b004bc..0000000 --- a/src/main/java/technicianlp/reauth/gui/AuthScreen.java +++ /dev/null @@ -1,237 +0,0 @@ -package technicianlp.reauth.gui; - -import com.mojang.authlib.exceptions.AuthenticationException; -import net.minecraft.client.font.TextRenderer; -import net.minecraft.client.gui.Element; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.client.gui.widget.ButtonWidget; -import net.minecraft.client.gui.widget.CheckboxWidget; -import net.minecraft.client.gui.widget.TextFieldWidget; -import net.minecraft.client.resource.language.I18n; -import net.minecraft.client.util.math.MatrixStack; -import net.minecraft.text.LiteralText; -import net.minecraft.text.TranslatableText; -import org.lwjgl.glfw.GLFW; -import technicianlp.reauth.AuthHelper; -import technicianlp.reauth.ReAuth; -import technicianlp.reauth.VersionChecker; -import technicianlp.reauth.integration.ClothConfigIntegration; - -import java.awt.Color; - -public final class AuthScreen extends Screen { - - private TextFieldWidget username; - private PasswordFieldWidget pw; - private ButtonWidget confirm; - private ButtonWidget cancel; - private CheckboxWidget save; - private ButtonWidget config; - - private Screen prev; - - private int baseY; - - private String message = ""; - - public AuthScreen(Screen prev) { - super(new TranslatableText("reauth.gui.auth.title")); - this.prev = prev; - } - - public AuthScreen(Screen prev, String message) { - this(prev); - this.message = message; - } - - @Override - public void init() { - super.init(); - this.client.keyboard.setRepeatEvents(true); - - this.baseY = this.height / 2 - 110 / 2; - - this.username = new TextFieldWidget(this.textRenderer, this.width / 2 - 155, this.baseY + 15, 2 * 155, 20, new TranslatableText("reauth.gui.auth.username")); - this.username.setMaxLength(512); - this.username.setText(ReAuth.config.getUsername()); - addButton(username); - - this.pw = new PasswordFieldWidget(this.textRenderer, this.width / 2 - 155, this.baseY + 60, 2 * 155, 20, new TranslatableText("reauth.gui.auth.password")); - this.pw.setMaxLength(Short.MAX_VALUE); - this.pw.setText(ReAuth.config.getPassword()); - addButton(this.pw); - - setInitialFocus(username.getText().isEmpty() ? username : pw); - - this.save = new CheckboxWidget(this.width / 2 - 155, this.baseY + 85, 2 * 155, 20, new TranslatableText("reauth.gui.auth.checkbox"), !pw.getText().isEmpty()); - if (ReAuth.config.hasCrypto()) { - addButton(this.save); - } - - this.confirm = new ButtonWidget(this.width / 2 - 155, this.baseY + 110, 153, 20, LiteralText.EMPTY, b -> doLogin()); - addButton(confirm); - this.cancel = new ButtonWidget(this.width / 2 + 2, this.baseY + 110, 155, 20, new TranslatableText("gui.cancel"), b -> this.client.openScreen(prev)); - addButton(cancel); - - this.config = new ButtonWidget(this.width - 80, this.height - 25, 75, 20, new TranslatableText("reauth.gui.auth.config"), b -> { - this.client.openScreen(ClothConfigIntegration.getConfigScreen(this)); - }); - if (ClothConfigIntegration.isAvailable()) { - addButton(config); - } - - if (message.isEmpty() && ReAuth.versionCheck.getStatus() == VersionChecker.Status.OUTDATED) { - String msg = ReAuth.versionCheck.getChanges(); - if (msg != null) { - message = I18n.translate("reauth.gui.auth.update", msg); - } - } - } - - @Override - public void render(MatrixStack matrices, int mouseX, int mouseY, float partialTicks) { - this.renderBackground(matrices); - - drawCenteredString2(matrices, this.textRenderer, I18n.translate("reauth.gui.auth.text1"), this.width / 2, this.baseY, Color.WHITE.getRGB()); - drawCenteredString2(matrices, this.textRenderer, I18n.translate("reauth.gui.auth.text2"), this.width / 2, this.baseY + 45, Color.WHITE.getRGB()); - if (!this.message.isEmpty()) { - drawCenteredString2(matrices, this.textRenderer, this.message, this.width / 2, this.baseY - 15, 0xFFFFFF); - } - - if (!ReAuth.config.hasCrypto()) { - this.textRenderer.draw(matrices, I18n.translate("reauth.gui.auth.noCrypto"), this.width / 2 - 155, this.baseY + 90, Color.WHITE.getRGB()); - } - - LoginType status = getLoginType(); - this.confirm.setMessage(new TranslatableText(status.getTranslation())); - this.confirm.active = status.isActive(); - - super.render(matrices, mouseX, mouseY, partialTicks); - } - - /** - * Sets the focus to the given TextFieldWidget - */ - private void focus(TextFieldWidget widget) { - Element old = getFocused(); - if (old instanceof TextFieldWidget) - ((TextFieldWidget) old).setSelected(false); - if (widget != null) - widget.setSelected(true); - focusOn(widget); - } - - @Override - public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - if (keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_KP_ENTER) { - Element focus = getFocused(); - if (focus == username) { - focus(pw); - return true; - } else if (focus == pw) { - doLogin(); - return true; - } - } - return super.keyPressed(keyCode, scanCode, modifiers); - } - - /** - * Determines which {@link LoginType} is applicable
    - * - {@link LoginType#ONLINE} requires a Password
    - * - {@link LoginType#OFFLINE} needs the name to match the Regex for valid Minecraft Names - */ - private LoginType getLoginType() { - String user = this.username.getText(); - if (user.isEmpty()) { - return LoginType.NONE; - } else if (this.pw.getText().isEmpty()) { - if (ReAuth.auth.isValidName(user)) { - return LoginType.OFFLINE; - } else { - return LoginType.NONE; - } - } else { - return LoginType.ONLINE; - } - } - - /** - * Calls the {@link AuthHelper} to do the Login and handles Errors - * Closes the Screen if successful - */ - private void doLogin() { - boolean success = false; - try { - LoginType type = getLoginType(); - switch (type) { - case ONLINE: - ReAuth.auth.login(this.username.getText(), this.pw.getPassword(), this.save.isChecked()); - break; - case OFFLINE: - ReAuth.auth.offline(this.username.getText()); - break; - default: - return; - } - this.message = I18n.translate("reauth.login.success"); - success = true; - } catch (AuthenticationException e) { - this.message = I18n.translate("reauth.login.fail", e.getMessage()); - ReAuth.log.error("Login failed:", e); - } catch (Exception e) { - this.message = I18n.translate("reauth.login.error", e.getMessage()); - ReAuth.log.error("Error:", e); - } - if (success) - onClose(); - } - - /** - * Method called to request this Screen to close itself (unfortunate MCP name) - */ - @Override - public void onClose() { - this.client.openScreen(prev); - } - - /** - * Called once this Screen is closed (unfortunate MCP name) - */ - @Override - public void removed() { - super.removed(); - this.pw.setPassword(new char[0]); - this.client.keyboard.setRepeatEvents(false); - } - - private enum LoginType { - NONE(false, "none"), - ONLINE(true, "online"), - OFFLINE(true, "offline"); - - private final boolean active; - private final String translation; - - LoginType(boolean active, String translation) { - this.active = active; - this.translation = translation; - } - - public boolean isActive() { - return active; - } - - public String getTranslation() { - return "reauth.gui.auth.confirm." + translation; - } - } - - /** - * for 1.16.x compat this needs to be copied - * the superclass method changes from instance to static between 1.16.1 and 1.16.2 - */ - private void drawCenteredString2(MatrixStack matrices, TextRenderer textRenderer, String text, int centerX, int y, int color) { - textRenderer.drawWithShadow(matrices, text, (float)(centerX - textRenderer.getWidth(text) / 2), (float)y, color); - } -} diff --git a/src/main/java/technicianlp/reauth/gui/FlowScreen.java b/src/main/java/technicianlp/reauth/gui/FlowScreen.java index 3b926e1..9c4aac3 100644 --- a/src/main/java/technicianlp/reauth/gui/FlowScreen.java +++ b/src/main/java/technicianlp/reauth/gui/FlowScreen.java @@ -1,18 +1,14 @@ package technicianlp.reauth.gui; -import com.mojang.blaze3d.vertex.PoseStack; -import net.minecraft.Util; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.components.Button; -import net.minecraft.client.resources.language.I18n; -import net.minecraft.network.chat.Component; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.resource.language.I18n; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.text.Text; +import net.minecraft.util.Util; import technicianlp.reauth.ReAuth; import technicianlp.reauth.authentication.SessionData; -import technicianlp.reauth.authentication.flows.AuthorizationCodeFlow; -import technicianlp.reauth.authentication.flows.DeviceCodeFlow; -import technicianlp.reauth.authentication.flows.Flow; -import technicianlp.reauth.authentication.flows.FlowCallback; -import technicianlp.reauth.authentication.flows.FlowStage; +import technicianlp.reauth.authentication.flows.*; import technicianlp.reauth.configuration.Profile; import technicianlp.reauth.session.SessionHelper; @@ -25,14 +21,11 @@ public final class FlowScreen extends AbstractScreen implements FlowCallback { - public static F open(BiFunction flowConstructor, P param, boolean keepParent) { + public static F open(BiFunction flowConstructor, P param) { FlowScreen screen = new FlowScreen(); F flow = flowConstructor.apply(param, screen); screen.flow = flow; - if (!keepParent) { - Minecraft.getInstance().popGuiLayer(); - } - Minecraft.getInstance().pushGuiLayer(screen); + MinecraftClient.getInstance().setScreen(screen); return flow; } @@ -55,7 +48,9 @@ public void init() { if (this.stage == FlowStage.MS_AWAIT_AUTH_CODE && this.flow instanceof AuthorizationCodeFlow) { try { URL url = new URL(((AuthorizationCodeFlow) this.flow).getLoginUrl()); - this.addRenderableWidget(new Button(this.centerX - buttonWidthH, this.baseY + this.screenHeight - 42, buttonWidth, 20, Component.translatable("reauth.msauth.button.browser"), (b) -> Util.getPlatform().openUrl((url)))); + this.addDrawableChild(new ButtonWidget(this.centerX - buttonWidthH, + this.baseY + this.screenHeight - 42, buttonWidth, 20, Text.translatable("reauth.msauth.button" + + ".browser"), b -> Util.getOperatingSystem().open(url))); } catch (MalformedURLException e) { ReAuth.log.error("Browser button failed", e); } @@ -66,7 +61,9 @@ public void init() { String urlString = flow.getLoginUrl().join(); String code = flow.getCode().join(); URL url = new URL(urlString); - this.addRenderableWidget(new Button(this.centerX - buttonWidthH, this.baseY + this.screenHeight - 42, buttonWidth, 20, Component.translatable("reauth.msauth.button.browser"), (b) -> Util.getPlatform().openUrl((url)))); + this.addDrawableChild(new ButtonWidget(this.centerX - buttonWidthH, + this.baseY + this.screenHeight - 42, buttonWidth, 20, Text.translatable("reauth.msauth" + + ".button.browser"), b -> Util.getOperatingSystem().open(url))); this.formatArgs = new String[]{urlString, code}; } } catch (MalformedURLException e) { @@ -76,10 +73,10 @@ public void init() { } @Override - public void render(PoseStack poseStack, int mouseX, int mouseY, float partialTicks) { - super.render(poseStack, mouseX, mouseY, partialTicks); + public void render(MatrixStack matrixStack, int mouseX, int mouseY, float partialTicks) { + super.render(matrixStack, mouseX, mouseY, partialTicks); - String text = I18n.get(this.stage.getRawName(), (Object[]) this.formatArgs); + String text = I18n.translate(this.stage.getRawName(), (Object[]) this.formatArgs); String[] lines = text.split("\\R"); int height = lines.length * 9; for (String s : lines) { @@ -92,13 +89,15 @@ public void render(PoseStack poseStack, int mouseX, int mouseY, float partialTic for (String line : lines) { if (line.startsWith("$")) { line = line.substring(1); - poseStack.pushPose(); - poseStack.scale(2, 2, 1); - this.font.drawShadow(poseStack, line, (float) (this.centerX - this.font.width(line)) / 2, (float) y / 2, 0xFFFFFFFF); + matrixStack.push(); + matrixStack.scale(2, 2, 1); + this.textRenderer.drawWithShadow(matrixStack, line, + (float) (this.centerX - this.textRenderer.getWidth(line)) / 2, (float) y / 2, 0xFFFFFFFF); y += 18; - poseStack.popPose(); + matrixStack.pop(); } else { - this.font.drawShadow(poseStack, line, (float) (this.centerX - this.font.width(line) / 2), (float) y, 0xFFFFFFFF); + this.textRenderer.drawWithShadow(matrixStack, line, + (float) (this.centerX - this.textRenderer.getWidth(line) / 2), (float) y, 0xFFFFFFFF); y += 9; } } @@ -144,11 +143,11 @@ public void onProfileComplete(Profile profile, Throwable throwable) { public void transitionStage(FlowStage newStage) { this.stage = newStage; ReAuth.log.info(this.stage.getLogLine()); - this.init(Minecraft.getInstance(), this.width, this.height); + this.init(MinecraftClient.getInstance(), this.width, this.height); if (newStage == FlowStage.MS_AWAIT_AUTH_CODE && this.flow instanceof AuthorizationCodeFlow) { try { - Util.getPlatform().openUrl(new URL(((AuthorizationCodeFlow) this.flow).getLoginUrl())); + Util.getOperatingSystem().open(new URL(((AuthorizationCodeFlow) this.flow).getLoginUrl())); } catch (MalformedURLException e) { ReAuth.log.error("Failed to open page", e); } diff --git a/src/main/java/technicianlp/reauth/gui/MainScreen.java b/src/main/java/technicianlp/reauth/gui/MainScreen.java index 7dbffb9..9184041 100644 --- a/src/main/java/technicianlp/reauth/gui/MainScreen.java +++ b/src/main/java/technicianlp/reauth/gui/MainScreen.java @@ -1,24 +1,22 @@ package technicianlp.reauth.gui; -import com.mojang.blaze3d.vertex.PoseStack; -import net.minecraft.client.gui.components.Button; -import net.minecraft.client.resources.language.I18n; -import net.minecraft.network.chat.Component; -import net.minecraftforge.fml.VersionChecker; -import org.apache.maven.artifact.versioning.ComparableVersion; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.resource.language.I18n; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.text.Text; import technicianlp.reauth.ReAuth; +import technicianlp.reauth.VersionChecker; import technicianlp.reauth.authentication.flows.Flows; import technicianlp.reauth.configuration.Profile; import technicianlp.reauth.configuration.ProfileConstants; -import java.util.Map; - public final class MainScreen extends AbstractScreen { private String message = null; - public MainScreen() { - super("reauth.gui.title.main"); + public MainScreen(Screen screen) { + super("reauth.gui.title.main", screen); } @Override @@ -28,50 +26,62 @@ public void init() { int buttonWidthH = BUTTON_WIDTH / 2; int y = this.centerY - 55; - SaveButton.ITooltip saveButtonTooltip = (button, matrixStack, mouseX, mouseY) -> this.renderTooltip(matrixStack, this.font.split(Component.translatable("reauth.gui.button.save.tooltip"), 250), mouseX, mouseY); - SaveButton saveButton = new SaveButton(this.centerX - buttonWidthH, y + 70, Component.translatable("reauth.gui.button.save"), saveButtonTooltip); - this.addRenderableWidget(saveButton); + SaveButton.ITooltip saveButtonTooltip = + (button, matrixStack, mouseX, mouseY) -> this.renderOrderedTooltip(matrixStack, + this.textRenderer.wrapLines(Text.translatable("reauth.gui.button.save.tooltip"), 250), + mouseX, mouseY); + SaveButton saveButton = new SaveButton(this.centerX - buttonWidthH, y + 70, + Text.translatable("reauth.gui.button.save"), saveButtonTooltip); + this.addDrawableChild(saveButton); Profile profile = ReAuth.profiles.getProfile(); if (profile != null) { - Component text = Component.translatable("reauth.gui.profile", profile.getValue(ProfileConstants.NAME, "Steve")); - this.addRenderableWidget(new Button(this.centerX - buttonWidthH, y + 10, BUTTON_WIDTH, 20, text, (b) -> FlowScreen.open(Flows::loginWithProfile, profile, false))); + Text text = Text.translatable("reauth.gui.profile", profile.getValue(ProfileConstants.NAME, "Steve")); + this.addDrawableChild(new ButtonWidget(this.centerX - buttonWidthH, y + 10, BUTTON_WIDTH, 20, text, + (b) -> FlowScreen.open(Flows::loginWithProfile, profile))); } else { - Button profileButton = new Button(this.centerX - buttonWidthH, y + 10, BUTTON_WIDTH, 20, Component.translatable("reauth.gui.noProfile"), (b) -> { + ButtonWidget profileButton = new ButtonWidget(this.centerX - buttonWidthH, y + 10, BUTTON_WIDTH, 20, + Text.translatable("reauth.gui.noProfile"), (b) -> { }); profileButton.active = false; - this.addRenderableWidget(profileButton); + this.addDrawableChild(profileButton); } - this.addRenderableWidget(new Button(this.centerX - buttonWidthH, y + 45, buttonWidthH - 1, 20, Component.translatable("reauth.gui.button.authcode"), (b) -> FlowScreen.open(Flows::loginWithAuthCode, saveButton.selected(), false))); - this.addRenderableWidget(new Button(this.centerX + 1, y + 45, buttonWidthH - 1, 20, Component.translatable("reauth.gui.button.devicecode"), (b) -> FlowScreen.open(Flows::loginWithDeviceCode, saveButton.selected(), false))); - this.addRenderableWidget(new Button(this.centerX - buttonWidthH, y + 105, BUTTON_WIDTH, 20, Component.translatable("reauth.gui.button.offline"), (b) -> this.transitionScreen(new OfflineLoginScreen()))); + this.addDrawableChild(new ButtonWidget(this.centerX - buttonWidthH, y + 45, buttonWidthH - 1, 20, + Text.translatable("reauth.gui.button.authcode"), (b) -> FlowScreen.open(Flows::loginWithAuthCode, + saveButton.isChecked()))); + this.addDrawableChild(new ButtonWidget(this.centerX + 1, y + 45, buttonWidthH - 1, 20, Text.translatable( + "reauth.gui.button.devicecode"), (b) -> FlowScreen.open(Flows::loginWithDeviceCode, + saveButton.isChecked()))); + this.addDrawableChild(new ButtonWidget(this.centerX - buttonWidthH, y + 105, BUTTON_WIDTH, 20, + Text.translatable("reauth.gui.button.offline"), + (b) -> this.transitionScreen(new OfflineLoginScreen()))); - VersionChecker.CheckResult result = VersionChecker.getResult(ReAuth.modInfo); - if (result.status() == VersionChecker.Status.OUTDATED) { + VersionChecker.Status result = ReAuth.versionCheck.getStatus(); + if (result == VersionChecker.Status.OUTDATED) { // Cannot be null but is marked as such :( - Map changes = result.changes(); + String changes = ReAuth.versionCheck.getChanges(); if (changes != null) { - String msg = changes.get(result.target()); - if (msg != null) { - this.message = I18n.get("reauth.gui.auth.update", msg); - } + this.message = I18n.translate("reauth.gui.auth.update", changes); } } } @Override - public void render(PoseStack poseStack, int mouseX, int mouseY, float partialTicks) { - super.render(poseStack, mouseX, mouseY, partialTicks); + public void render(MatrixStack matrixStack, int mouseX, int mouseY, float partialTicks) { + super.render(matrixStack, mouseX, mouseY, partialTicks); int x = this.centerX - BUTTON_WIDTH / 2; - this.font.drawShadow(poseStack, I18n.get("reauth.gui.text.profile"), x, this.centerY - 55, 0xFFFFFFFF); - this.font.drawShadow(poseStack, I18n.get("reauth.gui.text.microsoft"), x, this.centerY - 20, 0xFFFFFFFF); - this.font.drawShadow(poseStack, I18n.get("reauth.gui.text.offline"), x, this.centerY + 40, 0xFFFFFFFF); + this.textRenderer.drawWithShadow(matrixStack, I18n.translate("reauth.gui.text.profile"), + x, this.centerY - 55, 0xA0A0A0); + this.textRenderer.drawWithShadow(matrixStack, I18n.translate("reauth.gui.text.microsoft"), + x, this.centerY - 20, 0xA0A0A0); + this.textRenderer.drawWithShadow(matrixStack, I18n.translate("reauth.gui.text.offline"), + x, this.centerY + 40, 0xA0A0A0); if (this.message != null) { - this.font.drawShadow(poseStack, this.message, x, this.baseY + 20, 0xFFFFFFFF); + this.textRenderer.drawWithShadow(matrixStack, this.message, x, this.baseY + 20, 0xFFFFFFFF); } } diff --git a/src/main/java/technicianlp/reauth/gui/OfflineLoginScreen.java b/src/main/java/technicianlp/reauth/gui/OfflineLoginScreen.java index 437a30c..8acea50 100644 --- a/src/main/java/technicianlp/reauth/gui/OfflineLoginScreen.java +++ b/src/main/java/technicianlp/reauth/gui/OfflineLoginScreen.java @@ -1,18 +1,20 @@ package technicianlp.reauth.gui; -import com.mojang.blaze3d.vertex.PoseStack; -import net.minecraft.client.gui.components.Button; -import net.minecraft.client.gui.components.EditBox; -import net.minecraft.client.gui.components.events.GuiEventListener; -import net.minecraft.client.resources.language.I18n; -import net.minecraft.network.chat.Component; +import net.minecraft.client.gui.Element; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.TextFieldWidget; +import net.minecraft.client.resource.language.I18n; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.text.Text; import org.lwjgl.glfw.GLFW; import technicianlp.reauth.session.SessionHelper; +import java.util.Objects; + public final class OfflineLoginScreen extends AbstractScreen { - private EditBox username; - private Button confirm; + private TextFieldWidget username; + private ButtonWidget confirm; public OfflineLoginScreen() { super("reauth.gui.title.offline"); @@ -22,34 +24,37 @@ public OfflineLoginScreen() { public void init() { super.init(); - this.username = new EditBox(this.font, this.centerX - BUTTON_WIDTH / 2, this.centerY - 5, BUTTON_WIDTH, 20, Component.translatable("reauth.gui.auth.username")); + this.username = new TextFieldWidget(this.textRenderer, this.centerX - BUTTON_WIDTH / 2, this.centerY - 5, + BUTTON_WIDTH, 20, Text.translatable("reauth.gui.auth.username")); this.username.setMaxLength(16); - this.addRenderableWidget(this.username); - this.username.setFocus(true); - this.setFocused(this.username); - this.addRenderableWidget(this.username); + this.username.setTextFieldFocused(true); + this.username.setText(Objects.requireNonNull(this.client).getSession().getUsername()); + this.addSelectableChild(this.username); + this.setInitialFocus(this.username); - this.confirm = new Button(this.centerX - BUTTON_WIDTH / 2, this.baseY + this.screenHeight - 42, BUTTON_WIDTH, 20, Component.translatable("reauth.gui.button.username"), (b) -> this.performUsernameChange()); - this.addRenderableWidget(this.confirm); + this.confirm = new ButtonWidget(this.centerX - BUTTON_WIDTH / 2, this.baseY + this.screenHeight - 42, + BUTTON_WIDTH, 20, Text.translatable("reauth.gui.button.username"), (b) -> this.performUsernameChange()); + this.addDrawableChild(this.confirm); } @Override - public void render(PoseStack poseStack, int mouseX, int mouseY, float partialTicks) { - super.render(poseStack, mouseX, mouseY, partialTicks); - - this.font.drawShadow(poseStack, I18n.get("reauth.gui.auth.username"), this.centerX - (BUTTON_WIDTH / 2f), this.centerY - 15, 0xFFFFFFFF); + public void render(MatrixStack matrixStack, int mouseX, int mouseY, float partialTicks) { + super.render(matrixStack, mouseX, mouseY, partialTicks); + this.username.render(matrixStack, mouseX, mouseY, partialTicks); + this.textRenderer.drawWithShadow(matrixStack, I18n.translate("reauth.gui.auth.username"), + this.centerX - (BUTTON_WIDTH / 2f), this.centerY - 15, 0xFFFFFFFF); } @Override public void tick() { super.tick(); - this.confirm.active = SessionHelper.isValidOfflineUsername(this.username.getValue()); + this.confirm.active = SessionHelper.isValidOfflineUsername(this.username.getText()); } @Override public boolean keyPressed(int keyCode, int p_keyPressed_2_, int p_keyPressed_3_) { if (keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_KP_ENTER) { - GuiEventListener focus = this.getFocused(); + Element focus = this.getFocused(); if (focus == this.username) { this.performUsernameChange(); return true; @@ -63,8 +68,8 @@ public boolean keyPressed(int keyCode, int p_keyPressed_2_, int p_keyPressed_3_) * Closes the Screen if successful */ private void performUsernameChange() { - if (SessionHelper.isValidOfflineUsername(this.username.getValue())) { - SessionHelper.setOfflineUsername(this.username.getValue()); + if (SessionHelper.isValidOfflineUsername(this.username.getText())) { + SessionHelper.setOfflineUsername(this.username.getText()); this.requestClose(true); } } diff --git a/src/main/java/technicianlp/reauth/gui/PasswordFieldWidget.java b/src/main/java/technicianlp/reauth/gui/PasswordFieldWidget.java deleted file mode 100644 index 5220567..0000000 --- a/src/main/java/technicianlp/reauth/gui/PasswordFieldWidget.java +++ /dev/null @@ -1,162 +0,0 @@ -package technicianlp.reauth.gui; - -import net.minecraft.SharedConstants; -import net.minecraft.client.font.TextRenderer; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.client.gui.widget.TextFieldWidget; -import net.minecraft.text.Text; -import net.minecraft.util.Util; -import technicianlp.reauth.mixin.TextFieldWidgetMixin; - -import java.util.Arrays; - -final class PasswordFieldWidget extends TextFieldWidget { - - PasswordFieldWidget(TextRenderer renderer, int posx, int posy, int x, int y, Text name) { - super(renderer, posx, posy, x, y, name); - this.setMaxLength(512); - } - - private char[] password = new char[0]; - - /** - * Prevent Cut/Copy; actual logic handled by super - */ - @Override - public final boolean keyPressed(int keyCode, int scanCode, int modifiers) { - if (!this.isActive() || Screen.isCopy(keyCode) || Screen.isCut(keyCode)) - return false; - return super.keyPressed(keyCode, scanCode, modifiers); - } - - /** - * Vanilla filters out "§" therefore a custom filter is use (see {@link #isValidChar(char)}) to allow those - */ - @Override - public boolean charTyped(char chr, int keyCode) { - if (!this.isActive()) { - return false; - } else if (isValidChar(chr)) { - this.write(Character.toString(chr)); - return true; - } else { - return false; - } - } - - /** - * Modified version of {@link TextFieldWidget#write(String)} to allow for displayed text to differ and make the password be array based - */ - @Override - public final void write(String rawInput) { - int selectionEnd = getMixin().reauthGetSelectionEnd(); - int selStart = Math.min(this.getCursor(), selectionEnd); - int selEnd = Math.max(this.getCursor(), selectionEnd); - int selLength = selEnd - selStart; - - char[] input = stripInvalidChars(rawInput); - char[] newPW = new char[password.length - selLength + input.length]; - - if (password.length != 0 && selStart > 0) - System.arraycopy(password, 0, newPW, 0, Math.min(selStart, password.length)); - - System.arraycopy(input, 0, newPW, selStart, input.length); - - if (password.length != 0 && selEnd < password.length) - System.arraycopy(password, selEnd, newPW, selStart + input.length, password.length - selEnd); - - setPassword(newPW); - } - - /** - * Modified version of {@link TextFieldWidget#eraseCharacters(int)} to allow for displayed text to differ and make the password be array based - */ - @Override - public final void eraseCharacters(int characterOffset) { - if (password.length == 0) - return; - if (this.getMixin().reauthGetSelectionEnd() != this.getCursor()) { - this.write(""); - } else { - int cursor = Util.moveCursor(this.getText(), this.getCursor(), characterOffset); - int start = Math.min(cursor, this.getCursor()); - int end = Math.max(cursor, this.getCursor()); - - if(start != end) { - char[] newPW = new char[start + password.length - end]; - - if (start >= 0) - System.arraycopy(password, 0, newPW, 0, start); - - if (end < password.length) - System.arraycopy(password, end, newPW, start, password.length - end); - - setPassword(newPW); - this.setCursor(start); - } - } - } - - final char[] getPassword() { - char[] pw = new char[password.length]; - System.arraycopy(password, 0, pw, 0, password.length); - return pw; - } - - /** - * clear old password and update displayed Text - */ - final void setPassword(char[] password) { - Arrays.fill(this.password, 'f'); - this.password = password; - updateText(); - } - - /** - * Redirect Setter to {@link #setPassword(char[])} - */ - @Override - public final void setText(String textIn) { - setPassword(textIn.toCharArray()); - updateText(); - } - - /** - * Sets the actually displayed Text to all dots - */ - private void updateText() { - char[] chars = new char[password.length]; - Arrays.fill(chars, '\u25CF'); - super.setText(new String(chars)); - } - - /** - * Modified version of {@link SharedConstants#stripInvalidChars(String)} to allow SectionSign to be input into the field - */ - private char[] stripInvalidChars(String input) { - char[] out = new char[input.length()]; - int outInd = 0; - for (int i = 0; i < out.length; i++) { - char in = input.charAt(i); - if (isValidChar(in)) { - out[outInd++] = in; - } - } - char[] ret = new char[outInd]; - System.arraycopy(out, 0, ret, 0, outInd); - Arrays.fill(out, 'f'); - return ret; - } - - /** - * Modified version of {@link SharedConstants#isValidChar(char)} to allow SectionSign to be input into the field - */ - private boolean isValidChar(char in) { - return in == 0xa7 || SharedConstants.isValidChar(in); - } - - @SuppressWarnings("ConstantConditions") - private TextFieldWidgetMixin getMixin() { - return (TextFieldWidgetMixin)(Object) this; - } -} diff --git a/src/main/java/technicianlp/reauth/gui/SaveButton.java b/src/main/java/technicianlp/reauth/gui/SaveButton.java index e705a08..20d20ce 100644 --- a/src/main/java/technicianlp/reauth/gui/SaveButton.java +++ b/src/main/java/technicianlp/reauth/gui/SaveButton.java @@ -1,27 +1,27 @@ package technicianlp.reauth.gui; -import com.mojang.blaze3d.vertex.PoseStack; -import net.minecraft.client.gui.components.Checkbox; -import net.minecraft.network.chat.Component; +import net.minecraft.client.gui.widget.CheckboxWidget; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.text.Text; -public final class SaveButton extends Checkbox { +public final class SaveButton extends CheckboxWidget { private final ITooltip tooltip; - public SaveButton(int x, int y, Component title, ITooltip tooltip) { + public SaveButton(int x, int y, Text title, ITooltip tooltip) { super(x, y, 20, 20, title, false); this.tooltip = tooltip; } @Override - public void renderButton(PoseStack poseStack, int mouseX, int mouseY, float partialTicks) { - super.renderButton(poseStack, mouseX, mouseY, partialTicks); - if (this.isHoveredOrFocused()) { - this.tooltip.onTooltip(this, poseStack, mouseX, mouseY); + public void renderButton(MatrixStack matrixStack, int mouseX, int mouseY, float partialTicks) { + super.renderButton(matrixStack, mouseX, mouseY, partialTicks); + if (this.isHovered()) { + this.tooltip.onTooltip(this, matrixStack, mouseX, mouseY); } } public interface ITooltip { - void onTooltip(SaveButton button, PoseStack poseStack, int mouseX, int mouseY); + void onTooltip(SaveButton button, MatrixStack matrixStack, int mouseX, int mouseY); } } diff --git a/src/main/java/technicianlp/reauth/integration/ClothConfigIntegration.java b/src/main/java/technicianlp/reauth/integration/ClothConfigIntegration.java deleted file mode 100644 index d2a8d23..0000000 --- a/src/main/java/technicianlp/reauth/integration/ClothConfigIntegration.java +++ /dev/null @@ -1,22 +0,0 @@ -package technicianlp.reauth.integration; - - -import me.sargunvohra.mcmods.autoconfig1u.AutoConfig; -import net.fabricmc.loader.api.FabricLoader; -import net.minecraft.client.gui.screen.Screen; -import technicianlp.reauth.Configuration; - -public final class ClothConfigIntegration { - - public static boolean isAvailable() { - return FabricLoader.getInstance().isModLoaded("cloth-config2"); - } - - public static Screen getConfigScreen(Screen parent) { - if(isAvailable()) { - return AutoConfig.getConfigScreen(Configuration.class, parent).get(); - } else { - return null; - } - } -} diff --git a/src/main/java/technicianlp/reauth/integration/ModMenuIntegration.java b/src/main/java/technicianlp/reauth/integration/ModMenuIntegration.java index b01a6a6..0f57489 100644 --- a/src/main/java/technicianlp/reauth/integration/ModMenuIntegration.java +++ b/src/main/java/technicianlp/reauth/integration/ModMenuIntegration.java @@ -1,13 +1,12 @@ package technicianlp.reauth.integration; -import io.github.prospector.modmenu.api.ConfigScreenFactory; -import io.github.prospector.modmenu.api.ModMenuApi; -import me.sargunvohra.mcmods.autoconfig1u.AutoConfig; -import technicianlp.reauth.Configuration; +import com.terraformersmc.modmenu.api.ConfigScreenFactory; +import com.terraformersmc.modmenu.api.ModMenuApi; +import technicianlp.reauth.gui.MainScreen; public final class ModMenuIntegration implements ModMenuApi { @Override public ConfigScreenFactory getModConfigScreenFactory() { - return parent -> AutoConfig.getConfigScreen(Configuration.class, parent).get(); + return MainScreen::new; } } diff --git a/src/main/java/technicianlp/reauth/mixin/ConnectScreenMixin.java b/src/main/java/technicianlp/reauth/mixin/ConnectScreenMixin.java deleted file mode 100644 index fcba67a..0000000 --- a/src/main/java/technicianlp/reauth/mixin/ConnectScreenMixin.java +++ /dev/null @@ -1,33 +0,0 @@ -package technicianlp.reauth.mixin; - -import net.minecraft.client.gui.screen.ConnectScreen; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.network.ClientConnection; -import net.minecraft.text.LiteralText; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.gen.Accessor; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import technicianlp.reauth.mixinUtil.ConnectScreenDuck; -import technicianlp.reauth.mixinUtil.DisconnectUtil; - -@Mixin(ConnectScreen.class) -public abstract class ConnectScreenMixin extends Screen implements ConnectScreenDuck { - protected ConnectScreenMixin() { - super(new LiteralText("Dummy Constructor")); - } - - @Override - @Accessor("connection") - public abstract ClientConnection reauthGetConnection(); - - @Override - @Accessor("parent") - public abstract Screen reauthGetParent(); - - @Inject(at = @At("TAIL"), method = "init()V") - private void reauthInit(CallbackInfo info) { - DisconnectUtil.setConnectScreen(this); - } -} diff --git a/src/main/java/technicianlp/reauth/mixin/DisconnectedScreenMixin.java b/src/main/java/technicianlp/reauth/mixin/DisconnectedScreenMixin.java deleted file mode 100644 index dd470f2..0000000 --- a/src/main/java/technicianlp/reauth/mixin/DisconnectedScreenMixin.java +++ /dev/null @@ -1,58 +0,0 @@ -package technicianlp.reauth.mixin; - -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gui.screen.DisconnectedScreen; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.client.gui.widget.AbstractButtonWidget; -import net.minecraft.client.gui.widget.ButtonWidget; -import net.minecraft.text.LiteralText; -import net.minecraft.text.Text; -import net.minecraft.text.TranslatableText; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.gen.Accessor; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import technicianlp.reauth.ReAuth; -import technicianlp.reauth.gui.AuthScreen; -import technicianlp.reauth.mixinUtil.DisconnectUtil; -import technicianlp.reauth.mixinUtil.DisconnectedScreenDuck; - -import static technicianlp.reauth.mixinUtil.DisconnectUtil.getTranslationKey; - -@Mixin(DisconnectedScreen.class) -public abstract class DisconnectedScreenMixin extends Screen implements DisconnectedScreenDuck { - protected DisconnectedScreenMixin() { - super(new LiteralText("Dummy Constructor")); - } - - @Override - @Accessor("reason") - public abstract Text reauthGetReason(); - - @Override - @Accessor("parent") - public abstract Screen reauthGetParent(); - - @Inject(at = @At("TAIL"), method = "init()V") - private void reauthInit(CallbackInfo info) { - if ("connect.failed".equals(getTranslationKey(this.getTitle()))) { - if (getTranslationKey(reauthGetReason()).startsWith("disconnect.loginFailed")) { - AbstractButtonWidget menu = this.buttons.get(0); - - String key = DisconnectUtil.canRetryLogin() ? "reauth.retry" : "reauth.retry.disabled"; - Text retryText = new TranslatableText(key, ReAuth.config.getProfile()); - ButtonWidget retryButton = new ButtonWidget(menu.x, menu.y + 25, 200, 20, retryText, b -> { - DisconnectUtil.retryLogin(); - }); - if (!DisconnectUtil.canRetryLogin()) { - retryButton.active = false; - } - this.addButton(retryButton); - this.addButton(new ButtonWidget(menu.x, menu.y + 50, 200, 20, new TranslatableText("reauth.open"), b -> { - MinecraftClient.getInstance().openScreen(new AuthScreen(reauthGetParent())); - })); - } - } - } -} diff --git a/src/main/java/technicianlp/reauth/mixin/MinecraftClientMixin.java b/src/main/java/technicianlp/reauth/mixin/MinecraftClientMixin.java deleted file mode 100644 index 2be529e..0000000 --- a/src/main/java/technicianlp/reauth/mixin/MinecraftClientMixin.java +++ /dev/null @@ -1,15 +0,0 @@ -package technicianlp.reauth.mixin; - -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.util.Session; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Mutable; -import org.spongepowered.asm.mixin.gen.Accessor; - -@Mixin(MinecraftClient.class) -public interface MinecraftClientMixin { - - @Accessor("session") - @Mutable - void reauthSetSession(Session session); -} diff --git a/src/main/java/technicianlp/reauth/mixin/MultiplayerScreenMixin.java b/src/main/java/technicianlp/reauth/mixin/MultiplayerScreenMixin.java deleted file mode 100644 index b75ba7c..0000000 --- a/src/main/java/technicianlp/reauth/mixin/MultiplayerScreenMixin.java +++ /dev/null @@ -1,39 +0,0 @@ -package technicianlp.reauth.mixin; - -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.client.gui.screen.multiplayer.MultiplayerScreen; -import net.minecraft.client.gui.widget.ButtonWidget; -import net.minecraft.client.resource.language.I18n; -import net.minecraft.client.util.math.MatrixStack; -import net.minecraft.text.LiteralText; -import net.minecraft.text.TranslatableText; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import technicianlp.reauth.AuthHelper; -import technicianlp.reauth.ReAuth; -import technicianlp.reauth.gui.AuthScreen; - -@Mixin(MultiplayerScreen.class) -public class MultiplayerScreenMixin extends Screen { - protected MultiplayerScreenMixin() { - super(new LiteralText("Dummy Constructor")); - } - - @Inject(at = @At("TAIL"), method = "init()V") - private void reauthInit(CallbackInfo info) { - if(Screen.hasShiftDown()) { - ReAuth.auth.getSessionStatus(true); - } - addButton(new ButtonWidget(5, 5, 100, 20, new TranslatableText("reauth.gui.button"), - b -> MinecraftClient.getInstance().openScreen(new AuthScreen(this)))); - } - - @Inject(at = @At("TAIL"), method = "render(Lnet/minecraft/client/util/math/MatrixStack;IIF)V") - private void reauthRender(MatrixStack matrices, int mouseX, int mouseY, float delta, CallbackInfo info) { - AuthHelper.SessionStatus state = ReAuth.auth.getSessionStatus(false); - textRenderer.draw(matrices, I18n.translate(state.getTranslationKey()), 110, 10, 0xFFFFFFFF); - } -} diff --git a/src/main/java/technicianlp/reauth/mixin/TextFieldWidgetMixin.java b/src/main/java/technicianlp/reauth/mixin/TextFieldWidgetMixin.java deleted file mode 100644 index 12f2eda..0000000 --- a/src/main/java/technicianlp/reauth/mixin/TextFieldWidgetMixin.java +++ /dev/null @@ -1,12 +0,0 @@ -package technicianlp.reauth.mixin; - -import net.minecraft.client.gui.widget.TextFieldWidget; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.gen.Accessor; - -@Mixin(TextFieldWidget.class) -public interface TextFieldWidgetMixin { - - @Accessor("selectionEnd") - int reauthGetSelectionEnd(); -} diff --git a/src/main/java/technicianlp/reauth/mixinUtil/ConnectScreenDuck.java b/src/main/java/technicianlp/reauth/mixinUtil/ConnectScreenDuck.java deleted file mode 100644 index c610f6c..0000000 --- a/src/main/java/technicianlp/reauth/mixinUtil/ConnectScreenDuck.java +++ /dev/null @@ -1,10 +0,0 @@ -package technicianlp.reauth.mixinUtil; - -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.network.ClientConnection; - -public interface ConnectScreenDuck { - ClientConnection reauthGetConnection(); - - Screen reauthGetParent(); -} diff --git a/src/main/java/technicianlp/reauth/mixinUtil/DisconnectUtil.java b/src/main/java/technicianlp/reauth/mixinUtil/DisconnectUtil.java deleted file mode 100644 index 1d86161..0000000 --- a/src/main/java/technicianlp/reauth/mixinUtil/DisconnectUtil.java +++ /dev/null @@ -1,50 +0,0 @@ -package technicianlp.reauth.mixinUtil; - -import com.mojang.authlib.exceptions.AuthenticationException; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gui.screen.ConnectScreen; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.client.resource.language.I18n; -import net.minecraft.text.TranslatableText; -import technicianlp.reauth.ReAuth; -import technicianlp.reauth.gui.AuthScreen; - -import java.net.InetSocketAddress; -import java.net.SocketAddress; - -public class DisconnectUtil { - private static ConnectScreenDuck screen; - - public static String getTranslationKey(Object component) { - if (component instanceof TranslatableText) { - return ((TranslatableText) component).getKey(); - } - return ""; - } - - public static void setConnectScreen(ConnectScreenDuck screen) { - DisconnectUtil.screen = screen; - } - - public static boolean canRetryLogin() { - return screen != null && ReAuth.config.hasCredentials(); - } - - public static void retryLogin() { - try { - ReAuth.auth.login(ReAuth.config.getUsername(), ReAuth.config.getPassword(), true); - if (screen != null) { - SocketAddress add = screen.reauthGetConnection().getAddress(); - if (add instanceof InetSocketAddress) { - InetSocketAddress address = (InetSocketAddress) add; - MinecraftClient client = MinecraftClient.getInstance(); - client.openScreen(new ConnectScreen(screen.reauthGetParent(), client, address.getHostString(), address.getPort())); - } - } - } catch (AuthenticationException exception) { - ReAuth.log.error("Login failed:", exception); - Screen login = new AuthScreen(screen.reauthGetParent(), I18n.translate("reauth.login.fail", exception.getMessage())); - MinecraftClient.getInstance().openScreen(login); - } - } -} diff --git a/src/main/java/technicianlp/reauth/mixinUtil/DisconnectedScreenDuck.java b/src/main/java/technicianlp/reauth/mixinUtil/DisconnectedScreenDuck.java deleted file mode 100644 index 18ea4ca..0000000 --- a/src/main/java/technicianlp/reauth/mixinUtil/DisconnectedScreenDuck.java +++ /dev/null @@ -1,11 +0,0 @@ -package technicianlp.reauth.mixinUtil; - -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.text.Text; - - -public interface DisconnectedScreenDuck { - Text reauthGetReason(); - - Screen reauthGetParent(); -} diff --git a/src/main/java/technicianlp/reauth/session/SessionHelper.java b/src/main/java/technicianlp/reauth/session/SessionHelper.java index 9b9065e..7103afd 100644 --- a/src/main/java/technicianlp/reauth/session/SessionHelper.java +++ b/src/main/java/technicianlp/reauth/session/SessionHelper.java @@ -4,11 +4,11 @@ import com.mojang.authlib.minecraft.UserApiService; import com.mojang.authlib.yggdrasil.YggdrasilAuthenticationService; import com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService; -import net.minecraft.client.Minecraft; -import net.minecraft.client.User; -import net.minecraft.client.gui.screens.social.PlayerSocialManager; -import net.minecraft.client.main.GameConfig; -import net.minecraft.client.resources.SplashManager; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.RunArgs; +import net.minecraft.client.network.SocialInteractionsManager; +import net.minecraft.client.resource.SplashTextResourceSupplier; +import net.minecraft.client.util.Session; import technicianlp.reauth.ReAuth; import technicianlp.reauth.authentication.SessionData; import technicianlp.reauth.util.ReflectionUtils; @@ -21,12 +21,16 @@ public final class SessionHelper { - private static final Field userField = ReflectionUtils.findObfuscatedField(Minecraft.class, "f_90998_", "user"); - private static final Field userApiServiceField = ReflectionUtils.findObfuscatedField(Minecraft.class, "f_193584_", "userApiService"); - private static final Field socialManagerField = ReflectionUtils.findObfuscatedField(Minecraft.class, "f_91006_", "playerSocialManager"); - private static final Field splashesSessionField = ReflectionUtils.findObfuscatedField(SplashManager.class, "f_118863_", "user"); + private static final Field sessionField = ReflectionUtils.findObfuscatedField(MinecraftClient.class, "f_90998_", + "session"); + private static final Field userApiServiceField = ReflectionUtils.findObfuscatedField(MinecraftClient.class, + "f_193584_", "userApiService"); + private static final Field socialManagerField = ReflectionUtils.findObfuscatedField(MinecraftClient.class, + "f_91006_", "socialInteractionsManager"); + private static final Field splashesSessionField = + ReflectionUtils.findObfuscatedField(SplashTextResourceSupplier.class, "f_118863_", "session"); - private static final Pattern usernamePattern = Pattern.compile("[A-Za-z0-9_]{2,16}"); + private static final Pattern usernamePattern = Pattern.compile("\\w{2,16}"); /** * construct a {@link SessionData} for the given offline username @@ -47,31 +51,34 @@ public static void setSession(SessionData data) { * Set the Session and update dependant fields *

    *

  • Clear ProfileProperties and repopulate them - *
  • Recreate {@link UserApiService}, using logic from {@link Minecraft#createUserApiService(YggdrasilAuthenticationService, GameConfig)} - *
  • Recreate {@link PlayerSocialManager} with the new SocialInteractionsService - *
  • Update {@link SplashManager#user} + *
  • Recreate {@link UserApiService}, using logic from + * {@link MinecraftClient#createUserApiService(YggdrasilAuthenticationService, RunArgs)} + *
  • Recreate {@link SocialInteractionsManager} with the new SocialInteractionsService + *
  • Update {@link SplashTextResourceSupplier#session} */ private static void setSession(SessionData data, boolean online) { try { - Minecraft minecraft = Minecraft.getInstance(); + MinecraftClient minecraft = MinecraftClient.getInstance(); - User session = new User(data.username, data.uuid, data.accessToken, Optional.empty(), Optional.empty(), User.Type.byName(data.type)); + Session session = new Session(data.username, data.uuid, data.accessToken, Optional.empty(), + Optional.empty(), Session.AccountType.byName(data.type)); - ReflectionUtils.setField(userField, minecraft, session); + ReflectionUtils.setField(sessionField, minecraft, session); SessionChecker.invalidate(); // Update things depending on the Session. // TODO keep updated across versions // Clear ProfileProperties and repopulate them - minecraft.getProfileProperties().clear(); - minecraft.getProfileProperties(); + minecraft.getSessionProperties().clear(); + minecraft.getSessionProperties(); // UserProperties are unused // Recreate UserApiService UserApiService userApiService = null; if (online) { - YggdrasilMinecraftSessionService sessionService = (YggdrasilMinecraftSessionService) minecraft.getMinecraftSessionService(); + YggdrasilMinecraftSessionService sessionService = + (YggdrasilMinecraftSessionService) minecraft.getSessionService(); YggdrasilAuthenticationService authService = sessionService.getAuthenticationService(); try { userApiService = authService.createUserApiService(session.getAccessToken()); @@ -85,11 +92,11 @@ private static void setSession(SessionData data, boolean online) { ReflectionUtils.setField(userApiServiceField, minecraft, userApiService); // Recreate FilterManager - PlayerSocialManager socialManager = new PlayerSocialManager(minecraft, userApiService); + SocialInteractionsManager socialManager = new SocialInteractionsManager(minecraft, userApiService); ReflectionUtils.setField(socialManagerField, minecraft, socialManager); // Update Splashes session - ReflectionUtils.setField(splashesSessionField, minecraft.getSplashManager(), session); + ReflectionUtils.setField(splashesSessionField, minecraft.getSplashTextLoader(), session); } catch (Exception e) { ReAuth.log.error("Failed to update Session", e); } diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 6d93600..92e50cc 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -2,22 +2,21 @@ "schemaVersion": 1, "id": "reauth", "version": "${version}", - "name": "ReAuth", "description": "A Mod to renew your Session", "authors": [ - "TechnicianLP" + "TechnicianLP", + "NgoKimPhu" ], "contact": { - "homepage": "https://github.com/TechnicianLP/ReAuth", - "sources": "https://github.com/TechnicianLP/ReAuth", - "issues": "https://github.com/TechnicianLP/ReAuth/issues" + "homepage": "https://github.com/NgoKimPhu/ReAuth", + "sources": "https://github.com/NgoKimPhu/ReAuth", + "issues": "https://github.com/NgoKimPhu/ReAuth/issues" }, "license": "All rights reserved", - "environment": "client", "entrypoints": { - "main": [ + "client": [ "technicianlp.reauth.ReAuth" ], "modmenu": [ @@ -27,16 +26,16 @@ "mixins": [ "reauth.mixins.json" ], - "depends": { - "fabricloader": ">=0.7.4", + "fabricloader": ">=0.14.9", "fabric": "*", - "minecraft": "1.16.*", - "autoconfig1u": "*" + "minecraft": "~1.19", + "java": ">=17" }, "recommends": { - "cloth-config2": "*", "modmenu": "*" }, - "custom": { "modmenu:clientsideOnly": true } + "custom": { + "modmenu:clientsideOnly": true + } } diff --git a/src/main/resources/reauth.mixins.json b/src/main/resources/reauth.mixins.json index c35c10d..6ea7549 100644 --- a/src/main/resources/reauth.mixins.json +++ b/src/main/resources/reauth.mixins.json @@ -2,15 +2,9 @@ "required": true, "minVersion": "0.8", "package": "technicianlp.reauth.mixin", - "compatibilityLevel": "JAVA_8", + "compatibilityLevel": "JAVA_17", "mixins": [], - "client": [ - "MultiplayerScreenMixin", - "DisconnectedScreenMixin", - "ConnectScreenMixin", - "MinecraftClientMixin", - "TextFieldWidgetMixin" - ], + "client": [], "injectors": { "defaultRequire": 1 } From 24056201b244d7d49e18df1d6e2e2e97a10a43f2 Mon Sep 17 00:00:00 2001 From: NgoKimPhu Date: Fri, 30 Sep 2022 01:33:08 +0700 Subject: [PATCH 4/8] some code style refactoring --- .editorconfig | 12 +++ build.gradle | 7 +- gradle.properties | 3 +- settings.gradle | 2 +- .../reauth/authentication/MsAuthAPI.java | 47 +++++++----- .../reauth/authentication/SessionData.java | 24 +----- .../reauth/authentication/YggdrasilAPI.java | 10 ++- .../authentication/dto/ResponseObject.java | 4 +- .../MicrosoftAuthRefreshRequest.java | 14 ++-- .../dto/microsoft/MicrosoftAuthResponse.java | 19 ++--- .../code/MicrosoftAuthCodeRequest.java | 11 +-- .../device/MicrosoftAuthDeviceRequest.java | 11 +-- .../device/MicrosoftAuthDeviceResponse.java | 14 ++-- .../MicrosoftAuthDeviceTokenRequest.java | 11 +-- .../dto/mojang/MojangAuthRequest.java | 2 +- .../dto/mojang/MojangAuthResponse.java | 9 +-- .../dto/mojang/ProfileResponse.java | 7 +- .../dto/xbox/XboxAuthResponse.java | 41 +++++------ .../dto/xbox/XboxLiveAuthRequest.java | 5 +- .../dto/xbox/XboxXstsAuthRequest.java | 5 +- .../dto/yggdrasil/AuthenticateRequest.java | 5 +- .../dto/yggdrasil/AuthenticateResponse.java | 12 +-- .../dto/yggdrasil/JoinServerRequest.java | 2 +- .../dto/yggdrasil/JoinServerResponse.java | 10 +-- .../reauth/authentication/flows/Flow.java | 14 ++-- .../reauth/authentication/flows/Flows.java | 25 ++----- .../reauth/authentication/flows/Tokens.java | 4 +- .../authentication/flows/impl/FlowBase.java | 33 ++++----- .../flows/impl/MicrosoftCodeFlow.java | 28 +++---- .../flows/impl/MicrosoftDeviceFlow.java | 44 +++++------ .../flows/impl/MicrosoftProfileFlow.java | 50 ++++++++----- .../flows/impl/MojangAuthenticationFlow.java | 31 +++++--- .../flows/impl/UnknownProfileFlow.java | 14 ++-- .../flows/impl/XboxAuthenticationFlow.java | 35 +++++---- .../flows/impl/util/AuthBiFunction.java | 5 +- .../flows/impl/util/AuthFunction.java | 5 +- .../flows/impl/util/AuthSupplier.java | 5 +- .../flows/impl/util/Futures.java | 10 ++- .../reauth/authentication/http/HttpUtil.java | 43 ++++++----- .../reauth/authentication/http/Response.java | 9 ++- .../http/server/AuthenticationCodeServer.java | 46 +++++++----- .../http/server/CodeHandler.java | 51 ++++++------- .../authentication/http/server/Handler.java | 2 +- .../http/server/PageWriter.java | 49 ++++++------- .../http/server/ResourcesHandler.java | 14 ++-- .../authentication/http/server/Response.java | 14 ++-- .../reauth/configuration/ProfileBuilder.java | 8 +- .../configuration/ProfileConstants.java | 26 +++---- .../technicianlp/reauth/crypto/Crypto.java | 17 ++--- .../reauth/crypto/EncryptionAutomatic.java | 27 +++---- .../reauth/crypto/EncryptionNone.java | 17 ++--- .../reauth/crypto/PkceChallenge.java | 4 +- .../reauth/mojangfix/CertWorkaround.java | 73 +++++++++---------- .../reauth/mojangfix/JceWorkaround.java | 46 ++++++------ .../reauth/mojangfix/MojangJavaFix.java | 6 +- .../reauth/session/SessionChecker.java | 15 ++-- .../reauth/util/ReflectionUtils.java | 48 ++---------- .../resources/resources/reauth/reauth.html | 2 +- .../technicianlp/reauth/EventHandler.java | 23 +++--- src/main/java/technicianlp/reauth/ReAuth.java | 13 ++-- .../technicianlp/reauth/ReconnectHelper.java | 29 ++++---- .../technicianlp/reauth/VersionChecker.java | 61 +++++++++------- .../reauth/configuration/Config.java | 30 ++++---- .../reauth/configuration/Profile.java | 8 +- .../reauth/configuration/ProfileList.java | 38 +++------- .../reauth/gui/AbstractScreen.java | 27 +++---- .../technicianlp/reauth/gui/FlowScreen.java | 42 ++++++----- .../technicianlp/reauth/gui/MainScreen.java | 48 ++++++------ .../reauth/gui/OfflineLoginScreen.java | 23 +++--- .../technicianlp/reauth/gui/SaveButton.java | 10 +-- .../reauth/session/SessionHelper.java | 24 +++--- .../resources/assets/reauth/lang/en_us.json | 12 +-- 72 files changed, 743 insertions(+), 752 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..2430d85 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +indent_size = 4 +indent_style = space +insert_final_newline = true +max_line_length = 120 +tab_width = 4 + +[*.json] +indent_size = 2 diff --git a/build.gradle b/build.gradle index 7e19d42..c021d46 100644 --- a/build.gradle +++ b/build.gradle @@ -33,14 +33,11 @@ dependencies { modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" modImplementation "com.terraformersmc:modmenu:${project.modmenu_version}" + // PSA: Some older mods, compiled on Loom 0.2.1, might have outdated Maven POMs. + // You may need to force-disable transitiveness on them. modApi("me.shedaniel.cloth:cloth-config-fabric:${project.cloth_config_version}") { exclude(group: "net.fabricmc.fabric-api") } - // modApi ("me.shedaniel.cloth:config-2:4.7.0-unstable") { - // exclude(group: "net.fabricmc.fabric-api") - // } - // PSA: Some older mods, compiled on Loom 0.2.1, might have outdated Maven POMs. - // You may need to force-disable transitiveness on them. implementation 'com.electronwill.night-config:toml:3.6.6' } diff --git a/gradle.properties b/gradle.properties index 90af8ac..81ff0cf 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,7 +12,8 @@ mod_version=4.1.0-Fabric maven_group=technicianlp.reauth archives_base_name=ReAuth-1.19.2-Fabric # Dependencies -# currently not on the main fabric site, check on the maven: https://maven.fabricmc.net/net/fabricmc/fabric-api/fabric-api +# currently not on the main fabric site, check on the maven: +# https://maven.fabricmc.net/net/fabricmc/fabric-api/fabric-api fabric_version=0.61.0+1.19.2 cloth_config_version=8.2.88 modmenu_version=4.0.6 diff --git a/settings.gradle b/settings.gradle index d2a5a93..f47b416 100644 --- a/settings.gradle +++ b/settings.gradle @@ -9,4 +9,4 @@ pluginManagement { } } -rootProject.name='ReAuth Fabric 1.19.2' +rootProject.name = 'ReAuth Fabric 1.19.2' diff --git a/src/common/java/technicianlp/reauth/authentication/MsAuthAPI.java b/src/common/java/technicianlp/reauth/authentication/MsAuthAPI.java index a567aa4..4e57eb9 100644 --- a/src/common/java/technicianlp/reauth/authentication/MsAuthAPI.java +++ b/src/common/java/technicianlp/reauth/authentication/MsAuthAPI.java @@ -18,21 +18,25 @@ import technicianlp.reauth.authentication.http.UnreachableServiceException; -public final class MsAuthAPI { +public enum MsAuthAPI { + ; public static final String clientId = "fa861065-c46c-4ac9-a4da-59a7d40b8a72"; public static final int port = 52371; public static final String redirectUri = "http://127.0.0.1:" + port; + @SuppressWarnings("StringConcatenationMissingWhitespace") private static final String redirectUriEncoded = "http%3A%2F%2F127%2E0%2E0%2E1%3A" + port; private static final String scopeBasic = "XboxLive.signin"; private static final String scopePersist = "XboxLive.signin XboxLive.offline_access"; private static final String scopePersistUrl = "XboxLive.signin+XboxLive.offline_access"; - private static final String urlMicrosoftAuthorize = "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize"; + private static final String urlMicrosoftAuthorize = + "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize"; private static final String urlMicrosoftToken = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"; - private static final String urlMicrosoftDevice = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode"; + private static final String urlMicrosoftDevice = + "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode"; private static final String urlXboxLive = "https://user.auth.xboxlive.com/user/authenticate"; private static final String urlXsts = "https://xsts.auth.xboxlive.com/xsts/authorize"; private static final String urlMojangLogin = "https://api.minecraftservices.com/authentication/login_with_xbox"; @@ -40,37 +44,42 @@ public final class MsAuthAPI { public static String getLoginUrl(boolean persist, String pkceChallenge) { return urlMicrosoftAuthorize + - "?client_id=" + clientId + - "&redirect_uri=" + redirectUriEncoded + - "&scope=" + (persist ? scopePersistUrl : scopeBasic) + - "&response_type=code" + - "&response_mode=form_post" + - "&prompt=select_account" + - "&code_challenge=" + pkceChallenge + - "&code_challenge_method=S256"; + "?client_id=" + clientId + + "&redirect_uri=" + redirectUriEncoded + + "&scope=" + (persist ? scopePersistUrl : scopeBasic) + + "&response_type=code" + + "&response_mode=form_post" + + "&prompt=select_account" + + "&code_challenge=" + pkceChallenge + + "&code_challenge_method=S256"; } - public static MicrosoftAuthResponse redeemAuthorizationCode(String code, String pkceVerifier) throws UnreachableServiceException, InvalidResponseException { + public static MicrosoftAuthResponse redeemAuthorizationCode(String code, String pkceVerifier) throws + UnreachableServiceException, InvalidResponseException { MicrosoftAuthCodeRequest request = new MicrosoftAuthCodeRequest(code, pkceVerifier); return HttpUtil.performFormRequest(urlMicrosoftToken, request); } - public static MicrosoftAuthDeviceResponse requestDeviceCode(boolean persist) throws UnreachableServiceException, InvalidResponseException { + public static MicrosoftAuthDeviceResponse requestDeviceCode(boolean persist) throws UnreachableServiceException, + InvalidResponseException { MicrosoftAuthDeviceRequest request = new MicrosoftAuthDeviceRequest(persist ? scopePersist : scopeBasic); return HttpUtil.performFormRequest(urlMicrosoftDevice, request); } - public static Response redeemDeviceCode(String deviceCode) throws UnreachableServiceException { + public static Response redeemDeviceCode(String deviceCode) throws + UnreachableServiceException { MicrosoftAuthDeviceTokenRequest request = new MicrosoftAuthDeviceTokenRequest(deviceCode); return HttpUtil.performWrappedFormRequest(urlMicrosoftToken, request); } - public static MicrosoftAuthResponse redeemRefreshToken(String refreshToken) throws UnreachableServiceException, InvalidResponseException { + public static MicrosoftAuthResponse redeemRefreshToken(String refreshToken) throws UnreachableServiceException, + InvalidResponseException { MicrosoftAuthRefreshRequest request = new MicrosoftAuthRefreshRequest(refreshToken); return HttpUtil.performFormRequest(urlMicrosoftToken, request); } - public static XboxAuthResponse authenticateXASU(String token) throws UnreachableServiceException, InvalidResponseException { + public static XboxAuthResponse authenticateXASU(String token) throws UnreachableServiceException, + InvalidResponseException { XboxLiveAuthRequest request = new XboxLiveAuthRequest(token); return HttpUtil.performJsonRequest(urlXboxLive, request); } @@ -80,12 +89,14 @@ public static Response authenticateXSTS(String xblToken) throw return HttpUtil.performWrappedJsonRequest(urlXsts, request); } - public static MojangAuthResponse authenticateMojang(String xstsToken, String uhs) throws UnreachableServiceException, InvalidResponseException { + public static MojangAuthResponse authenticateMojang(String xstsToken, String uhs) throws + UnreachableServiceException, InvalidResponseException { MojangAuthRequest request = new MojangAuthRequest(xstsToken, uhs); return HttpUtil.performJsonRequest(urlMojangLogin, request); } - public static ProfileResponse fetchProfile(String accessToken) throws UnreachableServiceException, InvalidResponseException { + public static ProfileResponse fetchProfile(String accessToken) throws UnreachableServiceException, + InvalidResponseException { return HttpUtil.performGetRequest(urlMojangProfile, accessToken, ProfileResponse.class); } } diff --git a/src/common/java/technicianlp/reauth/authentication/SessionData.java b/src/common/java/technicianlp/reauth/authentication/SessionData.java index d95190c..f70f8bf 100644 --- a/src/common/java/technicianlp/reauth/authentication/SessionData.java +++ b/src/common/java/technicianlp/reauth/authentication/SessionData.java @@ -1,26 +1,4 @@ package technicianlp.reauth.authentication; -public final class SessionData { - - public final String username; - public final String uuid; - public final String accessToken; - public final String type; - - public SessionData(String username, String uuid, String accessToken, String type) { - this.username = username; - this.uuid = uuid; - this.accessToken = accessToken; - this.type = type; - } - - @Override - public final String toString() { - return "SessionData{" + - "username='" + this.username + '\'' + - ", uuid='" + this.uuid + '\'' + - ", accessToken='" + this.accessToken + '\'' + - ", type='" + this.type + '\'' + - '}'; - } +public record SessionData(String username, String uuid, String accessToken, String type) { } diff --git a/src/common/java/technicianlp/reauth/authentication/YggdrasilAPI.java b/src/common/java/technicianlp/reauth/authentication/YggdrasilAPI.java index 0cba051..02635b0 100644 --- a/src/common/java/technicianlp/reauth/authentication/YggdrasilAPI.java +++ b/src/common/java/technicianlp/reauth/authentication/YggdrasilAPI.java @@ -19,7 +19,8 @@ /** * cut down reimplementation version of {@link YggdrasilUserAuthentication} */ -public final class YggdrasilAPI { +public enum YggdrasilAPI { + ; private static final String urlAuthenticate = "https://authserver.mojang.com/authenticate"; private static final String urlJoin = "https://sessionserver.mojang.com/session/minecraft/join"; @@ -27,7 +28,8 @@ public final class YggdrasilAPI { /** * reimplementation of {@link YggdrasilUserAuthentication#logInWithPassword()} */ - public static SessionData login(String username, String password) throws UnreachableServiceException, InvalidResponseException { + public static SessionData login(String username, String password) throws UnreachableServiceException, + InvalidResponseException { AuthenticateRequest request = new AuthenticateRequest(username, password, UUID.randomUUID().toString()); AuthenticateResponse response = HttpUtil.performJsonRequest(urlAuthenticate, request); return response != null ? response.getSession() : null; @@ -36,8 +38,8 @@ public static SessionData login(String username, String password) throws Unreach /** * checks validity of accessToken by invoking the joinServer endpoint *

    - * reimplementation of {@link YggdrasilMinecraftSessionService#joinServer(GameProfile, String, String)} - * Server hash is generated like during standard login sequence + * reimplementation of {@link YggdrasilMinecraftSessionService#joinServer(GameProfile, String, String)} Server hash + * is generated like during standard login sequence */ public static boolean validate(String accessToken, String uuid) throws UnreachableServiceException { String hash = new BigInteger(Crypto.randomBytes(20)).toString(16); diff --git a/src/common/java/technicianlp/reauth/authentication/dto/ResponseObject.java b/src/common/java/technicianlp/reauth/authentication/dto/ResponseObject.java index dd74a12..482cc7a 100644 --- a/src/common/java/technicianlp/reauth/authentication/dto/ResponseObject.java +++ b/src/common/java/technicianlp/reauth/authentication/dto/ResponseObject.java @@ -1,7 +1,5 @@ package technicianlp.reauth.authentication.dto; -import org.jetbrains.annotations.Nullable; - /** * Interface for response payloads */ @@ -15,5 +13,5 @@ public interface ResponseObject { /** * returns the errormessage returned by the service for a failed request */ - @Nullable String getError(); + String getError(); } diff --git a/src/common/java/technicianlp/reauth/authentication/dto/microsoft/MicrosoftAuthRefreshRequest.java b/src/common/java/technicianlp/reauth/authentication/dto/microsoft/MicrosoftAuthRefreshRequest.java index 005d487..ddcafa0 100644 --- a/src/common/java/technicianlp/reauth/authentication/dto/microsoft/MicrosoftAuthRefreshRequest.java +++ b/src/common/java/technicianlp/reauth/authentication/dto/microsoft/MicrosoftAuthRefreshRequest.java @@ -8,12 +8,14 @@ import java.util.Map; /** - * Request Payload for the /token Endpoint of the Microsoft Identity Platform - * Payload is used for refreshing the oauth tokens. + * Request Payload for the /token Endpoint of the Microsoft Identity Platform Payload is used for refreshing the oauth + * tokens. * * @see Microsoft Authentication Scheme on wiki.vg - * @see Microsoft Auth Code Flow - * @see Microsoft Device Code Flow + * @see Microsoft Auth + * Code Flow + * @see Microsoft Device + * Code Flow */ public final class MicrosoftAuthRefreshRequest implements RequestObject.Form { @@ -31,12 +33,12 @@ public MicrosoftAuthRefreshRequest(String refreshToken) { } @Override - public final Class getResponseClass() { + public Class getResponseClass() { return MicrosoftAuthResponse.class; } @Override - public final Map getFields() { + public Map getFields() { return this.fields; } } diff --git a/src/common/java/technicianlp/reauth/authentication/dto/microsoft/MicrosoftAuthResponse.java b/src/common/java/technicianlp/reauth/authentication/dto/microsoft/MicrosoftAuthResponse.java index c13ae72..41f72d0 100644 --- a/src/common/java/technicianlp/reauth/authentication/dto/microsoft/MicrosoftAuthResponse.java +++ b/src/common/java/technicianlp/reauth/authentication/dto/microsoft/MicrosoftAuthResponse.java @@ -1,7 +1,6 @@ package technicianlp.reauth.authentication.dto.microsoft; import com.google.gson.annotations.SerializedName; -import org.jetbrains.annotations.Nullable; import technicianlp.reauth.authentication.dto.ResponseObject; /** @@ -10,8 +9,10 @@ * Only relevant fields are deserialized * * @see Microsoft Authentication Scheme on wiki.vg - * @see Microsoft Auth Code Flow - * @see Microsoft Device Code Flow + * @see Microsoft Auth + * Code Flow + * @see Microsoft Device + * Code Flow */ public final class MicrosoftAuthResponse implements ResponseObject { @@ -21,11 +22,11 @@ public final class MicrosoftAuthResponse implements ResponseObject { @SerializedName("access_token") public final String accessToken; @SerializedName("refresh_token") - public final @Nullable String refreshToken; + public final String refreshToken; // Error Handling @SerializedName("error") - public final @Nullable String error; + public final String error; private MicrosoftAuthResponse() { this.expires_in = null; @@ -36,20 +37,20 @@ private MicrosoftAuthResponse() { } @Override - public final boolean isValid() { + public boolean isValid() { return this.error == null && this.expires_in != null && this.accessToken != null; } @Override - public final @Nullable String getError() { + public String getError() { return this.error; } - public final String getAccessToken() { + public String getAccessToken() { return this.accessToken; } - public final String getRefreshToken() { + public String getRefreshToken() { return this.refreshToken; } } diff --git a/src/common/java/technicianlp/reauth/authentication/dto/microsoft/code/MicrosoftAuthCodeRequest.java b/src/common/java/technicianlp/reauth/authentication/dto/microsoft/code/MicrosoftAuthCodeRequest.java index 8f6cbb9..db2a6fe 100644 --- a/src/common/java/technicianlp/reauth/authentication/dto/microsoft/code/MicrosoftAuthCodeRequest.java +++ b/src/common/java/technicianlp/reauth/authentication/dto/microsoft/code/MicrosoftAuthCodeRequest.java @@ -9,11 +9,12 @@ import java.util.Map; /** - * Request Payload for the /token Endpoint of the Microsoft Identity Platform. - * Payload is used for redeeming the code received in the auth code grant flow. + * Request Payload for the /token Endpoint of the Microsoft Identity Platform. Payload is used for redeeming the code + * received in the auth code grant flow. * * @see Microsoft Authentication Scheme on wiki.vg - * @see Microsoft Auth Code Flow + * @see Microsoft Auth + * Code Flow */ public final class MicrosoftAuthCodeRequest implements RequestObject.Form { @@ -32,12 +33,12 @@ public MicrosoftAuthCodeRequest(String authCode, String pkceVerifier) { } @Override - public final Class getResponseClass() { + public Class getResponseClass() { return MicrosoftAuthResponse.class; } @Override - public final Map getFields() { + public Map getFields() { return this.fields; } } diff --git a/src/common/java/technicianlp/reauth/authentication/dto/microsoft/device/MicrosoftAuthDeviceRequest.java b/src/common/java/technicianlp/reauth/authentication/dto/microsoft/device/MicrosoftAuthDeviceRequest.java index 1fe75cc..d894dff 100644 --- a/src/common/java/technicianlp/reauth/authentication/dto/microsoft/device/MicrosoftAuthDeviceRequest.java +++ b/src/common/java/technicianlp/reauth/authentication/dto/microsoft/device/MicrosoftAuthDeviceRequest.java @@ -8,11 +8,12 @@ import java.util.Map; /** - * Request Payload for the /devicecode Endpoint of the Microsoft Identity Platform. - * Payload is used for requesting a devicecode for the device code flow. + * Request Payload for the /devicecode Endpoint of the Microsoft Identity Platform. Payload is used for requesting a + * devicecode for the device code flow. * * @see Microsoft Authentication Scheme on wiki.vg - * @see Microsoft Device Code Flow + * @see Microsoft Device + * Code Flow */ public final class MicrosoftAuthDeviceRequest implements RequestObject.Form { @@ -28,12 +29,12 @@ public MicrosoftAuthDeviceRequest(String scope) { } @Override - public final Class getResponseClass() { + public Class getResponseClass() { return MicrosoftAuthDeviceResponse.class; } @Override - public final Map getFields() { + public Map getFields() { return this.fields; } } diff --git a/src/common/java/technicianlp/reauth/authentication/dto/microsoft/device/MicrosoftAuthDeviceResponse.java b/src/common/java/technicianlp/reauth/authentication/dto/microsoft/device/MicrosoftAuthDeviceResponse.java index 76544f0..eb8c60a 100644 --- a/src/common/java/technicianlp/reauth/authentication/dto/microsoft/device/MicrosoftAuthDeviceResponse.java +++ b/src/common/java/technicianlp/reauth/authentication/dto/microsoft/device/MicrosoftAuthDeviceResponse.java @@ -1,7 +1,6 @@ package technicianlp.reauth.authentication.dto.microsoft.device; import com.google.gson.annotations.SerializedName; -import org.jetbrains.annotations.Nullable; import technicianlp.reauth.authentication.dto.ResponseObject; /** @@ -10,7 +9,8 @@ * Only relevant fields are deserialized * * @see Microsoft Authentication Scheme on wiki.vg - * @see Microsoft Device Code Flow + * @see Microsoft Device + * Code Flow */ public final class MicrosoftAuthDeviceResponse implements ResponseObject { @@ -27,7 +27,7 @@ public final class MicrosoftAuthDeviceResponse implements ResponseObject { // Error Handling @SerializedName("error") - public final @Nullable String error; + public final String error; private MicrosoftAuthDeviceResponse() { this.deviceCode = null; @@ -40,20 +40,20 @@ private MicrosoftAuthDeviceResponse() { } @Override - public final boolean isValid() { + public boolean isValid() { return this.error == null && this.deviceCode != null && this.userCode != null && this.verificationUri != null; } @Override - public final @Nullable String getError() { + public String getError() { return this.error; } - public final String getUserCode() { + public String getUserCode() { return this.userCode; } - public final String getVerificationUri() { + public String getVerificationUri() { return this.verificationUri; } } diff --git a/src/common/java/technicianlp/reauth/authentication/dto/microsoft/device/MicrosoftAuthDeviceTokenRequest.java b/src/common/java/technicianlp/reauth/authentication/dto/microsoft/device/MicrosoftAuthDeviceTokenRequest.java index d825387..6bd2c13 100644 --- a/src/common/java/technicianlp/reauth/authentication/dto/microsoft/device/MicrosoftAuthDeviceTokenRequest.java +++ b/src/common/java/technicianlp/reauth/authentication/dto/microsoft/device/MicrosoftAuthDeviceTokenRequest.java @@ -9,11 +9,12 @@ import java.util.Map; /** - * Request Payload for the /token Endpoint of the Microsoft Identity Platform. - * Payload is used for polling the endpoint as described in the device code flow. + * Request Payload for the /token Endpoint of the Microsoft Identity Platform. Payload is used for polling the endpoint + * as described in the device code flow. * * @see Microsoft Authentication Scheme on wiki.vg - * @see Microsoft Device Code Flow + * @see Microsoft Device + * Code Flow */ public final class MicrosoftAuthDeviceTokenRequest implements RequestObject.Form { @@ -30,12 +31,12 @@ public MicrosoftAuthDeviceTokenRequest(String deviceCode) { } @Override - public final Class getResponseClass() { + public Class getResponseClass() { return MicrosoftAuthResponse.class; } @Override - public final Map getFields() { + public Map getFields() { return this.fields; } } diff --git a/src/common/java/technicianlp/reauth/authentication/dto/mojang/MojangAuthRequest.java b/src/common/java/technicianlp/reauth/authentication/dto/mojang/MojangAuthRequest.java index 97a30f5..29c1b2c 100644 --- a/src/common/java/technicianlp/reauth/authentication/dto/mojang/MojangAuthRequest.java +++ b/src/common/java/technicianlp/reauth/authentication/dto/mojang/MojangAuthRequest.java @@ -18,7 +18,7 @@ public MojangAuthRequest(String xToken, String userHash) { } @Override - public final Class getResponseClass() { + public Class getResponseClass() { return MojangAuthResponse.class; } } diff --git a/src/common/java/technicianlp/reauth/authentication/dto/mojang/MojangAuthResponse.java b/src/common/java/technicianlp/reauth/authentication/dto/mojang/MojangAuthResponse.java index d23ff71..2a51446 100644 --- a/src/common/java/technicianlp/reauth/authentication/dto/mojang/MojangAuthResponse.java +++ b/src/common/java/technicianlp/reauth/authentication/dto/mojang/MojangAuthResponse.java @@ -1,7 +1,6 @@ package technicianlp.reauth.authentication.dto.mojang; import com.google.gson.annotations.SerializedName; -import org.jetbrains.annotations.Nullable; import technicianlp.reauth.authentication.dto.ResponseObject; /** @@ -21,7 +20,7 @@ public final class MojangAuthResponse implements ResponseObject { // Error Handling @SerializedName("error") - public final @Nullable String error; + public final String error; private MojangAuthResponse() { this.token = null; @@ -30,16 +29,16 @@ private MojangAuthResponse() { } @Override - public final boolean isValid() { + public boolean isValid() { return this.error == null && this.token != null && this.expiry != null; } @Override - public final @Nullable String getError() { + public String getError() { return this.error; } - public final String getToken() { + public String getToken() { return this.token; } } diff --git a/src/common/java/technicianlp/reauth/authentication/dto/mojang/ProfileResponse.java b/src/common/java/technicianlp/reauth/authentication/dto/mojang/ProfileResponse.java index e493f54..e2da96d 100644 --- a/src/common/java/technicianlp/reauth/authentication/dto/mojang/ProfileResponse.java +++ b/src/common/java/technicianlp/reauth/authentication/dto/mojang/ProfileResponse.java @@ -1,7 +1,6 @@ package technicianlp.reauth.authentication.dto.mojang; import com.google.gson.annotations.SerializedName; -import org.jetbrains.annotations.Nullable; import technicianlp.reauth.authentication.dto.ResponseObject; /** @@ -21,7 +20,7 @@ public final class ProfileResponse implements ResponseObject { // Error Handling @SerializedName("error") - public final @Nullable String error; + public final String error; private ProfileResponse() { this.uuid = null; @@ -30,12 +29,12 @@ private ProfileResponse() { } @Override - public final boolean isValid() { + public boolean isValid() { return this.error == null && this.uuid != null && this.name != null; } @Override - public final @Nullable String getError() { + public String getError() { return this.error; } } diff --git a/src/common/java/technicianlp/reauth/authentication/dto/xbox/XboxAuthResponse.java b/src/common/java/technicianlp/reauth/authentication/dto/xbox/XboxAuthResponse.java index 92186cd..bcdb917 100644 --- a/src/common/java/technicianlp/reauth/authentication/dto/xbox/XboxAuthResponse.java +++ b/src/common/java/technicianlp/reauth/authentication/dto/xbox/XboxAuthResponse.java @@ -1,21 +1,13 @@ package technicianlp.reauth.authentication.dto.xbox; -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.JsonPrimitive; -import org.jetbrains.annotations.Nullable; +import com.google.gson.*; import technicianlp.reauth.authentication.dto.ResponseObject; import java.lang.reflect.Type; /** - * Response Payload for: - * the /user/authenticate Endpoint of the Xbox Live user service
    - * the /xsts/authorize Endpoint of the Xbox Live xsts service
    + * Response Payload for: the /user/authenticate Endpoint of the Xbox Live user service
    the /xsts/authorize Endpoint + * of the Xbox Live xsts service
    *
    * Only relevant fields are deserialized * @@ -29,7 +21,7 @@ public final class XboxAuthResponse implements ResponseObject { public final String userHash; // Error Handling - public final @Nullable String error; + public final String error; private XboxAuthResponse(String validUntil, String token, String userHash, String error) { this.validUntil = validUntil; @@ -39,16 +31,16 @@ private XboxAuthResponse(String validUntil, String token, String userHash, Strin } @Override - public final boolean isValid() { + public boolean isValid() { return this.error == null && this.validUntil != null && this.token != null && this.userHash != null; } @Override - public final @Nullable String getError() { + public String getError() { return this.error; } - public final String getToken() { + public String getToken() { return this.token; } @@ -58,15 +50,18 @@ public final String getToken() { public static final class Deserializer implements JsonDeserializer { @Override - public final XboxAuthResponse deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + public XboxAuthResponse deserialize(JsonElement jsonElement, Type type, + @SuppressWarnings("ParameterNameDiffersFromOverriddenParameter") + JsonDeserializationContext context) throws + JsonParseException { try { - JsonObject root = json.getAsJsonObject(); + JsonObject root = jsonElement.getAsJsonObject(); - String validUntil = this.getString(root, "NotAfter"); - String token = this.getString(root, "Token"); - String userHash = this.extractUserHash(root.getAsJsonObject("DisplayClaims")); + String validUntil = getString(root, "NotAfter"); + String token = getString(root, "Token"); + String userHash = extractUserHash(root.getAsJsonObject("DisplayClaims")); - String error = this.getString(root, "XErr"); + String error = getString(root, "XErr"); return new XboxAuthResponse(validUntil, token, userHash, error); } catch (IllegalStateException | ClassCastException e) { @@ -74,7 +69,7 @@ public final XboxAuthResponse deserialize(JsonElement json, Type typeOfT, JsonDe } } - private String getString(JsonObject root, String name) { + private static String getString(JsonObject root, String name) { JsonPrimitive primitive = root.getAsJsonPrimitive(name); if (primitive != null) { return primitive.getAsString(); @@ -83,7 +78,7 @@ private String getString(JsonObject root, String name) { } } - private String extractUserHash(JsonObject displayClaims) { + private static String extractUserHash(JsonObject displayClaims) { if (displayClaims != null) { JsonArray xui = displayClaims.getAsJsonArray("xui"); if (xui != null) { diff --git a/src/common/java/technicianlp/reauth/authentication/dto/xbox/XboxLiveAuthRequest.java b/src/common/java/technicianlp/reauth/authentication/dto/xbox/XboxLiveAuthRequest.java index cd3524a..f7e984b 100644 --- a/src/common/java/technicianlp/reauth/authentication/dto/xbox/XboxLiveAuthRequest.java +++ b/src/common/java/technicianlp/reauth/authentication/dto/xbox/XboxLiveAuthRequest.java @@ -22,7 +22,7 @@ public XboxLiveAuthRequest(String token) { } @Override - public final Class getResponseClass() { + public Class getResponseClass() { return XboxAuthResponse.class; } @@ -31,8 +31,9 @@ public final Class getResponseClass() { */ public static final class Serializer implements JsonSerializer { + @SuppressWarnings("ParameterNameDiffersFromOverriddenParameter") @Override - public final JsonElement serialize(XboxLiveAuthRequest src, Type typeOfSrc, JsonSerializationContext context) { + public JsonElement serialize(XboxLiveAuthRequest src, Type typeOfSrc, JsonSerializationContext context) { JsonObject properties = new JsonObject(); properties.addProperty("AuthMethod", "RPS"); properties.addProperty("SiteName", "user.auth.xboxlive.com"); diff --git a/src/common/java/technicianlp/reauth/authentication/dto/xbox/XboxXstsAuthRequest.java b/src/common/java/technicianlp/reauth/authentication/dto/xbox/XboxXstsAuthRequest.java index 9fd8e7f..0dd4740 100644 --- a/src/common/java/technicianlp/reauth/authentication/dto/xbox/XboxXstsAuthRequest.java +++ b/src/common/java/technicianlp/reauth/authentication/dto/xbox/XboxXstsAuthRequest.java @@ -22,7 +22,7 @@ public XboxXstsAuthRequest(String token) { } @Override - public final Class getResponseClass() { + public Class getResponseClass() { return XboxAuthResponse.class; } @@ -31,8 +31,9 @@ public final Class getResponseClass() { */ public static final class Serializer implements JsonSerializer { + @SuppressWarnings("ParameterNameDiffersFromOverriddenParameter") @Override - public final JsonElement serialize(XboxXstsAuthRequest src, Type typeOfSrc, JsonSerializationContext context) { + public JsonElement serialize(XboxXstsAuthRequest src, Type typeOfSrc, JsonSerializationContext context) { JsonObject properties = new JsonObject(); properties.addProperty("SandboxId", "RETAIL"); properties.add("UserTokens", context.serialize(new String[]{src.token})); diff --git a/src/common/java/technicianlp/reauth/authentication/dto/yggdrasil/AuthenticateRequest.java b/src/common/java/technicianlp/reauth/authentication/dto/yggdrasil/AuthenticateRequest.java index b1d0b5b..ad8ce05 100644 --- a/src/common/java/technicianlp/reauth/authentication/dto/yggdrasil/AuthenticateRequest.java +++ b/src/common/java/technicianlp/reauth/authentication/dto/yggdrasil/AuthenticateRequest.java @@ -26,7 +26,7 @@ public AuthenticateRequest(String username, String password, String clientToken) } @Override - public final Class getResponseClass() { + public Class getResponseClass() { return AuthenticateResponse.class; } @@ -35,8 +35,9 @@ public final Class getResponseClass() { */ public static final class Serializer implements JsonSerializer { + @SuppressWarnings("ParameterNameDiffersFromOverriddenParameter") @Override - public final JsonElement serialize(AuthenticateRequest src, Type typeOfSrc, JsonSerializationContext context) { + public JsonElement serialize(AuthenticateRequest src, Type typeOfSrc, JsonSerializationContext context) { JsonObject agent = new JsonObject(); agent.addProperty("name", "Minecraft"); agent.addProperty("version", 1); diff --git a/src/common/java/technicianlp/reauth/authentication/dto/yggdrasil/AuthenticateResponse.java b/src/common/java/technicianlp/reauth/authentication/dto/yggdrasil/AuthenticateResponse.java index 12587d2..0b166b2 100644 --- a/src/common/java/technicianlp/reauth/authentication/dto/yggdrasil/AuthenticateResponse.java +++ b/src/common/java/technicianlp/reauth/authentication/dto/yggdrasil/AuthenticateResponse.java @@ -1,7 +1,6 @@ package technicianlp.reauth.authentication.dto.yggdrasil; import com.google.gson.annotations.SerializedName; -import org.jetbrains.annotations.Nullable; import technicianlp.reauth.authentication.SessionData; import technicianlp.reauth.authentication.dto.ResponseObject; @@ -20,7 +19,7 @@ public final class AuthenticateResponse implements ResponseObject { public final Profile profile; @SerializedName("error") - public final @Nullable String error; + public final String error; private AuthenticateResponse() { this.accessToken = null; @@ -29,16 +28,17 @@ private AuthenticateResponse() { } @Override - public final boolean isValid() { - return this.error == null && this.accessToken != null && this.profile != null && this.profile.name != null && this.profile.uuid != null; + public boolean isValid() { + return this.error == null && this.accessToken != null && this.profile != null && this.profile.name != null + && this.profile.uuid != null; } @Override - public final @Nullable String getError() { + public String getError() { return this.error; } - public final SessionData getSession() { + public SessionData getSession() { if (this.profile == null) { return null; } diff --git a/src/common/java/technicianlp/reauth/authentication/dto/yggdrasil/JoinServerRequest.java b/src/common/java/technicianlp/reauth/authentication/dto/yggdrasil/JoinServerRequest.java index 5db9a2d..8a79249 100644 --- a/src/common/java/technicianlp/reauth/authentication/dto/yggdrasil/JoinServerRequest.java +++ b/src/common/java/technicianlp/reauth/authentication/dto/yggdrasil/JoinServerRequest.java @@ -24,7 +24,7 @@ public JoinServerRequest(String accessToken, String uuid, String hash) { } @Override - public final Class getResponseClass() { + public Class getResponseClass() { return JoinServerResponse.class; } } diff --git a/src/common/java/technicianlp/reauth/authentication/dto/yggdrasil/JoinServerResponse.java b/src/common/java/technicianlp/reauth/authentication/dto/yggdrasil/JoinServerResponse.java index 8ececfc..85b35f4 100644 --- a/src/common/java/technicianlp/reauth/authentication/dto/yggdrasil/JoinServerResponse.java +++ b/src/common/java/technicianlp/reauth/authentication/dto/yggdrasil/JoinServerResponse.java @@ -1,31 +1,29 @@ package technicianlp.reauth.authentication.dto.yggdrasil; import com.google.gson.annotations.SerializedName; -import org.jetbrains.annotations.Nullable; import technicianlp.reauth.authentication.dto.ResponseObject; /** - * Response Payload for the /session/minecraft/join of the Mojang Session API - * Payload is only returned in case of error + * Response Payload for the /session/minecraft/join of the Mojang Session API Payload is only returned in case of error * * @see https://wiki.vg/Authentication */ public final class JoinServerResponse implements ResponseObject { @SerializedName("error") - public final @Nullable String error; + public final String error; private JoinServerResponse() { this.error = null; } @Override - public final boolean isValid() { + public boolean isValid() { return this.error == null; } @Override - public final @Nullable String getError() { + public String getError() { return this.error; } } diff --git a/src/common/java/technicianlp/reauth/authentication/flows/Flow.java b/src/common/java/technicianlp/reauth/authentication/flows/Flow.java index 3d920bb..76e7eb5 100644 --- a/src/common/java/technicianlp/reauth/authentication/flows/Flow.java +++ b/src/common/java/technicianlp/reauth/authentication/flows/Flow.java @@ -10,7 +10,7 @@ public interface Flow { void cancel(); - CompletableFuture getSession(); + CompletableFuture getSessionFuture(); boolean hasProfile(); @@ -19,15 +19,15 @@ public interface Flow { * * @throws IllegalStateException if a profile cannot be created */ - CompletableFuture getProfile(); + CompletableFuture getProfileFuture(); - default void thenRunAsync(Runnable runnable, Executor executor) { + default void thenRunAsync(Runnable action, Executor executor) { CompletableFuture target; - if (!this.hasProfile()) { - target = this.getSession(); + if (this.hasProfile()) { + target = CompletableFuture.allOf(this.getSessionFuture(), this.getProfileFuture()); } else { - target = CompletableFuture.allOf(this.getSession(), this.getProfile()); + target = this.getSessionFuture(); } - target.thenRunAsync(runnable, executor); + target.thenRunAsync(action, executor); } } diff --git a/src/common/java/technicianlp/reauth/authentication/flows/Flows.java b/src/common/java/technicianlp/reauth/authentication/flows/Flows.java index 56b27f9..e9a34ab 100644 --- a/src/common/java/technicianlp/reauth/authentication/flows/Flows.java +++ b/src/common/java/technicianlp/reauth/authentication/flows/Flows.java @@ -1,24 +1,18 @@ package technicianlp.reauth.authentication.flows; -import technicianlp.reauth.authentication.flows.impl.MicrosoftCodeFlow; -import technicianlp.reauth.authentication.flows.impl.MicrosoftDeviceFlow; -import technicianlp.reauth.authentication.flows.impl.MicrosoftProfileFlow; -import technicianlp.reauth.authentication.flows.impl.MojangAuthenticationFlow; -import technicianlp.reauth.authentication.flows.impl.UnknownProfileFlow; +import technicianlp.reauth.authentication.flows.impl.*; import technicianlp.reauth.configuration.Profile; import technicianlp.reauth.configuration.ProfileConstants; -public final class Flows { +public enum Flows { + ; public static Flow loginWithProfile(Profile profile, FlowCallback callback) { - switch (profile.getValue(ProfileConstants.PROFILE_TYPE)) { - case ProfileConstants.PROFILE_TYPE_MICROSOFT: - return new MicrosoftProfileFlow(profile, callback); - case ProfileConstants.PROFILE_TYPE_MOJANG: - return new MojangAuthenticationFlow(profile, callback); - default: - return new UnknownProfileFlow(callback); - } + return switch (profile.getValue(ProfileConstants.PROFILE_TYPE)) { + case ProfileConstants.PROFILE_TYPE_MICROSOFT -> new MicrosoftProfileFlow(profile, callback); + case ProfileConstants.PROFILE_TYPE_MOJANG -> new MojangAuthenticationFlow(profile, callback); + default -> new UnknownProfileFlow(callback); + }; } public static AuthorizationCodeFlow loginWithAuthCode(boolean persist, FlowCallback callback) { @@ -29,7 +23,4 @@ public static DeviceCodeFlow loginWithDeviceCode(boolean persist, FlowCallback c return new MicrosoftDeviceFlow(persist, callback); } - public static Flow loginWithMojang(String username, String password, boolean persist, FlowCallback callback) { - return new MojangAuthenticationFlow(username, password, persist, callback); - } } diff --git a/src/common/java/technicianlp/reauth/authentication/flows/Tokens.java b/src/common/java/technicianlp/reauth/authentication/flows/Tokens.java index 144c70f..319de4b 100644 --- a/src/common/java/technicianlp/reauth/authentication/flows/Tokens.java +++ b/src/common/java/technicianlp/reauth/authentication/flows/Tokens.java @@ -13,11 +13,11 @@ public Tokens(MicrosoftAuthResponse microsoft, XboxAuthResponse xasu) { this.refreshToken = microsoft.getRefreshToken(); } - public final String getXblToken() { + public String getXblToken() { return this.xblToken; } - public final String getRefreshToken() { + public String getRefreshToken() { return this.refreshToken; } } diff --git a/src/common/java/technicianlp/reauth/authentication/flows/impl/FlowBase.java b/src/common/java/technicianlp/reauth/authentication/flows/impl/FlowBase.java index 38f9b49..6524062 100644 --- a/src/common/java/technicianlp/reauth/authentication/flows/impl/FlowBase.java +++ b/src/common/java/technicianlp/reauth/authentication/flows/impl/FlowBase.java @@ -33,37 +33,36 @@ protected FlowBase(FlowCallback callback) { } /** - * Register dependant {@link CompletionStage} for cancellation handling. - * Every major stage should be registered for handling as cancelling a completed stage does no propagate to its dependants. + * Register dependant {@link CompletionStage} for cancellation handling. Every major stage should be registered for + * handling as cancelling a completed stage does not propagate to its dependants. */ final void registerDependantStages(CompletableFuture... stages) { Collections.addAll(this.stages, stages); } /** - * Register a dependant {@link FlowBase} for cancellation handling. - * Used to handle potential cleanup operations as the flows stages should already have been cancelled + * Register a dependant {@link FlowBase} for cancellation handling. Used to handle potential cleanup operations as + * the flows stages should already have been cancelled */ final void registerDependantFlow(FlowBase flow) { - this.stages.add(flow.getSession()); + this.stages.add(flow.getSessionFuture()); this.flows.add(flow); } /** - * Cancel the flow and its dependants. - * Cancels registered dependant stages in order. - * Requests cancellation of dependant flows. + * Cancel the flow and its dependants. Cancels registered dependant stages in order. Requests cancellation of + * dependant flows. */ @Override public void cancel() { this.stages.forEach(stage -> stage.cancel(true)); this.flows.forEach(FlowBase::cancel); - this.getSession().cancel(true); + this.getSessionFuture().cancel(true); } - final void onSessionComplete(SessionData data, Throwable throwable) { + final void onSessionComplete(SessionData session, Throwable throwable) { if (throwable == null) { - if (this.hasProfile() && !this.getProfile().isDone()) { + if (this.hasProfile() && !this.getProfileFuture().isDone()) { this.step(FlowStage.PROFILE); } else { this.step(FlowStage.FINISHED); @@ -71,40 +70,40 @@ final void onSessionComplete(SessionData data, Throwable throwable) { } else { this.step(FlowStage.FAILED); } - this.callback.onSessionComplete(data, throwable); + this.callback.onSessionComplete(session, throwable); } final void onProfileComplete(Profile profile, Throwable throwable) { if (throwable == null) { - if (this.getSession().isDone() && !this.getSession().isCompletedExceptionally()) { + if (this.getSessionFuture().isDone() && !this.getSessionFuture().isCompletedExceptionally()) { this.step(FlowStage.FINISHED); } } this.callback.onProfileComplete(profile, throwable); } - final BiFunction wrapStep(FlowStage stage, AuthBiFunction step) { + final BiFunction wrapStep(FlowStage stage, AuthBiFunction step) { return (t, u) -> { this.step(stage); return step.apply(t, u); }; } - final Function wrapStep(FlowStage stage, AuthFunction step) { + final Function wrapStep(FlowStage stage, AuthFunction step) { return (t) -> { this.step(stage); return step.apply(t); }; } - final Supplier wrapStep(FlowStage stage, AuthSupplier step) { + final Supplier wrapStep(FlowStage stage, AuthSupplier step) { return () -> { this.step(stage); return step.get(); }; } - final Function wrap(AuthFunction step) { + static Function wrap(AuthFunction step) { return step; } diff --git a/src/common/java/technicianlp/reauth/authentication/flows/impl/MicrosoftCodeFlow.java b/src/common/java/technicianlp/reauth/authentication/flows/impl/MicrosoftCodeFlow.java index dd58df3..a024e0f 100644 --- a/src/common/java/technicianlp/reauth/authentication/flows/impl/MicrosoftCodeFlow.java +++ b/src/common/java/technicianlp/reauth/authentication/flows/impl/MicrosoftCodeFlow.java @@ -22,7 +22,7 @@ public final class MicrosoftCodeFlow extends FlowBase implements AuthorizationCo private final String loginUrl; - private final CompletableFuture session; + private final CompletableFuture sessionFuture; private final CompletableFuture profile; private AuthenticationCodeServer codeServer; @@ -34,19 +34,21 @@ public MicrosoftCodeFlow(boolean persist, FlowCallback callback) { CompletableFuture codeStage = new CompletableFuture<>(); CompletableFuture pkceVerifier = CompletableFuture.completedFuture(pkceChallenge.getVerifier()); - CompletableFuture ms = codeStage.thenCombineAsync(pkceVerifier, this.wrapStep(FlowStage.MS_REDEEM_AUTH_CODE, MsAuthAPI::redeemAuthorizationCode), this.executor); + CompletableFuture ms = codeStage.thenCombineAsync(pkceVerifier, + this.wrapStep(FlowStage.MS_REDEEM_AUTH_CODE, MsAuthAPI::redeemAuthorizationCode), this.executor); CompletableFuture xasu = ms.thenApply(MicrosoftAuthResponse::getAccessToken) - .thenApplyAsync(this.wrapStep(FlowStage.MS_AUTH_XASU, MsAuthAPI::authenticateXASU), this.executor); + .thenApplyAsync(this.wrapStep(FlowStage.MS_AUTH_XASU, MsAuthAPI::authenticateXASU), this.executor); XboxAuthenticationFlow flow = new XboxAuthenticationFlow(xasu.thenApply(XboxAuthResponse::getToken), callback); - this.session = flow.getSession(); - this.session.whenComplete(this::onSessionComplete); - this.registerDependantStages(codeStage, ms, xasu, this.session); + this.sessionFuture = flow.getSessionFuture(); + this.sessionFuture.whenComplete(this::onSessionComplete); + this.registerDependantStages(codeStage, ms, xasu, this.sessionFuture); this.registerDependantFlow(flow); if (persist) { CompletableFuture tokens = ms.thenCombine(xasu, Tokens::new); - CompletableFuture encryption = CompletableFuture.supplyAsync(Crypto::newEncryption, this.executor); - CompletableFuture builder = this.session.thenCombine(encryption, ProfileBuilder::new); + CompletableFuture encryption = + CompletableFuture.supplyAsync(Crypto::newEncryption, this.executor); + CompletableFuture builder = this.sessionFuture.thenCombine(encryption, ProfileBuilder::new); this.profile = builder.thenCombine(tokens, ProfileBuilder::buildMicrosoft); this.profile.whenComplete(this::onProfileComplete); } else { @@ -64,8 +66,8 @@ public MicrosoftCodeFlow(boolean persist, FlowCallback callback) { } @Override - public final CompletableFuture getSession() { - return this.session; + public CompletableFuture getSessionFuture() { + return this.sessionFuture; } @Override @@ -74,7 +76,7 @@ public boolean hasProfile() { } @Override - public final CompletableFuture getProfile() { + public CompletableFuture getProfileFuture() { if (this.profile != null) { return this.profile; } else { @@ -83,12 +85,12 @@ public final CompletableFuture getProfile() { } @Override - public final String getLoginUrl() { + public String getLoginUrl() { return this.loginUrl; } @Override - public final void cancel() { + public void cancel() { super.cancel(); this.executor.execute(() -> this.codeServer.stop(true)); } diff --git a/src/common/java/technicianlp/reauth/authentication/flows/impl/MicrosoftDeviceFlow.java b/src/common/java/technicianlp/reauth/authentication/flows/impl/MicrosoftDeviceFlow.java index d67cc0b..3f51a2a 100644 --- a/src/common/java/technicianlp/reauth/authentication/flows/impl/MicrosoftDeviceFlow.java +++ b/src/common/java/technicianlp/reauth/authentication/flows/impl/MicrosoftDeviceFlow.java @@ -24,10 +24,10 @@ public final class MicrosoftDeviceFlow extends FlowBase implements DeviceCodeFlow { - private final CompletableFuture session; + private final CompletableFuture sessionFuture; private final CompletableFuture profile; - private final CompletableFuture url; + private final CompletableFuture loginUrl; private final CompletableFuture code; private final CompletableFuture auth; @@ -36,23 +36,25 @@ public MicrosoftDeviceFlow(boolean persist, FlowCallback callback) { super(callback); CompletableFuture deviceResponse = CompletableFuture.completedFuture(persist) - .thenApplyAsync(this.wrapStep(FlowStage.MS_REQUEST_DEVICE_CODE, MsAuthAPI::requestDeviceCode), this.executor); - this.url = deviceResponse.thenApply(MicrosoftAuthDeviceResponse::getVerificationUri); + .thenApplyAsync(this.wrapStep(FlowStage.MS_REQUEST_DEVICE_CODE, MsAuthAPI::requestDeviceCode), + this.executor); + this.loginUrl = deviceResponse.thenApply(MicrosoftAuthDeviceResponse::getVerificationUri); this.code = deviceResponse.thenApply(MicrosoftAuthDeviceResponse::getUserCode); this.auth = deviceResponse.thenApplyAsync(this::pollForCode, this.executor); CompletableFuture xasu = this.auth.thenApply(MicrosoftAuthResponse::getAccessToken) - .thenApplyAsync(this.wrapStep(FlowStage.MS_AUTH_XASU, MsAuthAPI::authenticateXASU), this.executor); + .thenApplyAsync(this.wrapStep(FlowStage.MS_AUTH_XASU, MsAuthAPI::authenticateXASU), this.executor); XboxAuthenticationFlow flow = new XboxAuthenticationFlow(xasu.thenApply(XboxAuthResponse::getToken), callback); - this.session = flow.getSession(); - this.session.whenComplete(this::onSessionComplete); - this.registerDependantStages(deviceResponse, this.auth, xasu, this.session); + this.sessionFuture = flow.getSessionFuture(); + this.sessionFuture.whenComplete(this::onSessionComplete); + this.registerDependantStages(deviceResponse, this.auth, xasu, this.sessionFuture); this.registerDependantFlow(flow); if (persist) { CompletableFuture tokens = this.auth.thenCombine(xasu, Tokens::new); - CompletableFuture encryption = CompletableFuture.supplyAsync(Crypto::newEncryption, this.executor); - CompletableFuture builder = this.session.thenCombine(encryption, ProfileBuilder::new); + CompletableFuture encryption = + CompletableFuture.supplyAsync(Crypto::newEncryption, this.executor); + CompletableFuture builder = this.sessionFuture.thenCombine(encryption, ProfileBuilder::new); this.profile = builder.thenCombine(tokens, ProfileBuilder::buildMicrosoft); this.profile.whenComplete(this::onProfileComplete); } else { @@ -61,8 +63,8 @@ public MicrosoftDeviceFlow(boolean persist, FlowCallback callback) { } /** - * Poll the Microsoft Token Endpoint until the user has completed authentication. - * Interval between Polls is specified by {@link MicrosoftAuthDeviceResponse#interval}. + * Poll the Microsoft Token Endpoint until the user has completed authentication. Interval between Polls is + * specified by {@link MicrosoftAuthDeviceResponse#interval}. */ private MicrosoftAuthResponse pollForCode(MicrosoftAuthDeviceResponse deviceResponse) { this.step(FlowStage.MS_POLL_DEVICE_CODE); @@ -74,11 +76,11 @@ private MicrosoftAuthResponse pollForCode(MicrosoftAuthDeviceResponse deviceResp ReAuth.log.info("Authorization received"); return response.get(); } else { - MicrosoftAuthResponse responseError = response.getUnchecked(); + MicrosoftAuthResponse responseError = response.getResponse(); if ("authorization_pending".equals(responseError.getError())) { ReAuth.log.debug("Authorization is still pending - continue polling"); } else { - ReAuth.log.info("Authorization failed: " + responseError.getError()); + ReAuth.log.info("Authorization failed: {}", responseError.getError()); // will throw InvalidResponseException return response.get(); } @@ -97,17 +99,17 @@ private MicrosoftAuthResponse pollForCode(MicrosoftAuthDeviceResponse deviceResp } @Override - public final CompletableFuture getSession() { - return this.session; + public CompletableFuture getSessionFuture() { + return this.sessionFuture; } @Override - public final boolean hasProfile() { + public boolean hasProfile() { return this.profile != null; } @Override - public final CompletableFuture getProfile() { + public CompletableFuture getProfileFuture() { if (this.profile != null) { return this.profile; } else { @@ -116,12 +118,12 @@ public final CompletableFuture getProfile() { } @Override - public final CompletableFuture getLoginUrl() { - return this.url; + public CompletableFuture getLoginUrl() { + return this.loginUrl; } @Override - public final CompletableFuture getCode() { + public CompletableFuture getCode() { return this.code; } } diff --git a/src/common/java/technicianlp/reauth/authentication/flows/impl/MicrosoftProfileFlow.java b/src/common/java/technicianlp/reauth/authentication/flows/impl/MicrosoftProfileFlow.java index 66b9259..81290c5 100644 --- a/src/common/java/technicianlp/reauth/authentication/flows/impl/MicrosoftProfileFlow.java +++ b/src/common/java/technicianlp/reauth/authentication/flows/impl/MicrosoftProfileFlow.java @@ -30,8 +30,8 @@ public final class MicrosoftProfileFlow extends FlowBase { private final CompletableFuture profileFuture; /** - * tries to log in using the stored accessToken. - * When authentication fails due to an expired token, the stored refreshToken is used to acquire a new accessToken followed by a second login attempt + * tries to log in using the stored accessToken. When authentication fails due to an expired token, the stored + * refreshToken is used to acquire a new accessToken followed by a second login attempt * * @see XboxAuthenticationFlow#hasExpiredTokenError() */ @@ -41,37 +41,48 @@ public MicrosoftProfileFlow(Profile profile, FlowCallback callback) { this.session = new CompletableFuture<>(); this.session.whenComplete(this::onSessionComplete); CompletableFuture profileFuture = CompletableFuture.completedFuture(profile); - this.encryption = profileFuture.thenApplyAsync(this.wrapStep(FlowStage.CRYPTO_INIT, Crypto::getProfileEncryption), this.executor); + this.encryption = + profileFuture.thenApplyAsync(this.wrapStep(FlowStage.CRYPTO_INIT, Crypto::getProfileEncryption), + this.executor); this.refreshRequired = new CompletableFuture<>(); - CompletableFuture xblTokenDec = this.encryption.thenCombineAsync(profile.get(ProfileConstants.XBL_TOKEN), ProfileEncryption::decryptFieldOne, this.executor); + CompletableFuture xblTokenDec = + this.encryption.thenCombineAsync(profile.get(ProfileConstants.XBL_TOKEN), + ProfileEncryption::decryptFieldOne, this.executor); this.xboxFlow1 = new XboxAuthenticationFlow(xblTokenDec, callback); - this.xboxFlow1.getSession().whenComplete(this::onComplete); + this.xboxFlow1.getSessionFuture().whenComplete(this::onComplete); this.registerDependantStages(this.encryption, xblTokenDec); this.registerDependantFlow(this.xboxFlow1); - CompletableFuture refreshTokenEnc = this.refreshRequired.thenCompose(Futures.conditional(profile.get(ProfileConstants.REFRESH_TOKEN), Futures.cancelled())); - CompletableFuture refreshTokenDec = this.encryption.thenCombineAsync(refreshTokenEnc, ProfileEncryption::decryptFieldTwo, this.executor); - CompletableFuture auth = refreshTokenDec.thenApplyAsync(this.wrapStep(FlowStage.MS_REDEEM_REFRESH_TOKEN, MsAuthAPI::redeemRefreshToken), this.executor); + CompletableFuture refreshTokenEnc = this.refreshRequired.thenCompose( + Futures.conditional(profile.get(ProfileConstants.REFRESH_TOKEN), Futures.cancelled())); + CompletableFuture refreshTokenDec = + this.encryption.thenCombineAsync(refreshTokenEnc, ProfileEncryption::decryptFieldTwo, this.executor); + CompletableFuture auth = refreshTokenDec.thenApplyAsync( + this.wrapStep(FlowStage.MS_REDEEM_REFRESH_TOKEN, MsAuthAPI::redeemRefreshToken), this.executor); CompletableFuture xasu = auth.thenApply(MicrosoftAuthResponse::getAccessToken) - .thenApplyAsync(this.wrapStep(FlowStage.MS_AUTH_XASU, MsAuthAPI::authenticateXASU), this.executor); + .thenApplyAsync(this.wrapStep(FlowStage.MS_AUTH_XASU, MsAuthAPI::authenticateXASU), this.executor); CompletableFuture xblToken = xasu.thenApply(XboxAuthResponse::getToken); XboxAuthenticationFlow xboxFlow2 = new XboxAuthenticationFlow(xblToken, callback); - xboxFlow2.getSession().whenComplete(this::onComplete); + xboxFlow2.getSessionFuture().whenComplete(this::onComplete); this.registerDependantStages(this.refreshRequired, refreshTokenDec, auth); this.registerDependantFlow(xboxFlow2); CompletableFuture tokens = auth.thenCombine(xasu, Tokens::new); - this.profileFuture = this.refreshRequired.thenComposeAsync(Futures.conditional(() -> this.constructProfile(tokens), CompletableFuture.completedFuture(profile)), this.executor); + this.profileFuture = this.refreshRequired.thenComposeAsync( + Futures.conditional(() -> this.constructProfile(tokens), CompletableFuture.completedFuture(profile)), + this.executor); this.profileFuture.whenComplete(this::onProfileComplete); } /** - * A reimagined version of {@link CompletableFuture#applyToEither(CompletionStage, Function)} that makes guarantees on exceptional behaviour and triggers the fallback computation if required. + * A reimagined version of {@link CompletableFuture#applyToEither(CompletionStage, Function)} that makes guarantees + * on exceptional behaviour and triggers the fallback computation if required. *

    - * completes {@link MicrosoftProfileFlow#session} with the provided session if completed normally (throwable = null)
    - * if the first {@link XboxAuthenticationFlow} completed exceptionally caused by an expired token the refresh-flow is triggered by completing {@link MicrosoftProfileFlow#refreshRequired}
    - * otherwise {@link MicrosoftProfileFlow#session} is completed exceptionally with the supplied throwable. + * completes {@link MicrosoftProfileFlow#session} with the provided session if completed normally (throwable = + * null)
    if the first {@link XboxAuthenticationFlow} completed exceptionally caused by an expired token the + * refresh-flow is triggered by completing {@link MicrosoftProfileFlow#refreshRequired}
    otherwise + * {@link MicrosoftProfileFlow#session} is completed exceptionally with the supplied throwable. * * @see CompletionStage#whenComplete(BiConsumer) * @see CompletableFuture#applyToEither(CompletionStage, Function) @@ -94,22 +105,23 @@ private void onComplete(SessionData sessionData, Throwable throwable) { } @Override - public final CompletableFuture getSession() { + public CompletableFuture getSessionFuture() { return this.session; } @Override - public final boolean hasProfile() { + public boolean hasProfile() { return true; } @Override - public final CompletableFuture getProfile() { + public CompletableFuture getProfileFuture() { return this.profileFuture; } private CompletableFuture constructProfile(CompletableFuture tokens) { - CompletableFuture encryption = this.encryption.thenApplyAsync(ProfileEncryption::randomizedCopy, this.executor); + CompletableFuture encryption = + this.encryption.thenApplyAsync(ProfileEncryption::randomizedCopy, this.executor); CompletableFuture builder = this.session.thenCombine(encryption, ProfileBuilder::new); return builder.thenCombineAsync(tokens, ProfileBuilder::buildMicrosoft, this.executor); } diff --git a/src/common/java/technicianlp/reauth/authentication/flows/impl/MojangAuthenticationFlow.java b/src/common/java/technicianlp/reauth/authentication/flows/impl/MojangAuthenticationFlow.java index 1e4f9c7..0d78f0d 100644 --- a/src/common/java/technicianlp/reauth/authentication/flows/impl/MojangAuthenticationFlow.java +++ b/src/common/java/technicianlp/reauth/authentication/flows/impl/MojangAuthenticationFlow.java @@ -20,14 +20,16 @@ public final class MojangAuthenticationFlow extends FlowBase { public MojangAuthenticationFlow(String username, String password, boolean save, FlowCallback callback) { super(callback); - this.session = CompletableFuture.supplyAsync(this.wrapStep(FlowStage.YGG_AUTH, () -> YggdrasilAPI.login(username, password)), this.executor); + this.session = CompletableFuture.supplyAsync( + this.wrapStep(FlowStage.YGG_AUTH, () -> YggdrasilAPI.login(username, password)), this.executor); this.session.whenComplete(this::onSessionComplete); this.registerDependantStages(this.session); if (save) { - CompletableFuture encryption = CompletableFuture.supplyAsync(Crypto::newEncryption, this.executor); - CompletableFuture builder = this.session.thenCombine(encryption, ProfileBuilder::new); - this.profile = builder.thenApply(b -> b.buildMojang(username, password)); + CompletableFuture encryption = + CompletableFuture.supplyAsync(Crypto::newEncryption, this.executor); + CompletableFuture builderFuture = this.session.thenCombine(encryption, ProfileBuilder::new); + this.profile = builderFuture.thenApply(builder -> builder.buildMojang(username, password)); } else { this.profile = null; } @@ -36,11 +38,18 @@ public MojangAuthenticationFlow(String username, String password, boolean save, public MojangAuthenticationFlow(Profile profile, FlowCallback callback) { super(callback); CompletableFuture profileFuture = CompletableFuture.completedFuture(profile); - CompletableFuture encryption = profileFuture.thenApplyAsync(this.wrapStep(FlowStage.CRYPTO_INIT, Crypto::getProfileEncryption), this.executor); - CompletableFuture usernameDec = encryption.thenCombineAsync(profile.get(ProfileConstants.USERNAME), ProfileEncryption::decryptFieldOne, this.executor); - CompletableFuture passwordDec = encryption.thenCombineAsync(profile.get(ProfileConstants.PASSWORD), ProfileEncryption::decryptFieldTwo, this.executor); + CompletableFuture encryption = + profileFuture.thenApplyAsync(this.wrapStep(FlowStage.CRYPTO_INIT, Crypto::getProfileEncryption), + this.executor); + CompletableFuture usernameDec = + encryption.thenCombineAsync(profile.get(ProfileConstants.USERNAME), ProfileEncryption::decryptFieldOne, + this.executor); + CompletableFuture passwordDec = + encryption.thenCombineAsync(profile.get(ProfileConstants.PASSWORD), ProfileEncryption::decryptFieldTwo, + this.executor); - this.session = usernameDec.thenCombineAsync(passwordDec, this.wrapStep(FlowStage.YGG_AUTH, YggdrasilAPI::login), this.executor); + this.session = usernameDec.thenCombineAsync(passwordDec, this.wrapStep(FlowStage.YGG_AUTH, YggdrasilAPI::login), + this.executor); this.registerDependantStages(encryption, usernameDec, passwordDec, this.session); this.profile = CompletableFuture.completedFuture(profile); @@ -48,17 +57,17 @@ public MojangAuthenticationFlow(Profile profile, FlowCallback callback) { } @Override - public final CompletableFuture getSession() { + public CompletableFuture getSessionFuture() { return this.session; } @Override - public final boolean hasProfile() { + public boolean hasProfile() { return this.profile != null; } @Override - public final CompletableFuture getProfile() { + public CompletableFuture getProfileFuture() { if (this.profile != null) { return this.profile; } else { diff --git a/src/common/java/technicianlp/reauth/authentication/flows/impl/UnknownProfileFlow.java b/src/common/java/technicianlp/reauth/authentication/flows/impl/UnknownProfileFlow.java index 73339d8..c5f1d21 100644 --- a/src/common/java/technicianlp/reauth/authentication/flows/impl/UnknownProfileFlow.java +++ b/src/common/java/technicianlp/reauth/authentication/flows/impl/UnknownProfileFlow.java @@ -9,26 +9,26 @@ public final class UnknownProfileFlow extends FlowBase { - private final CompletableFuture future; + private final CompletableFuture sessionFuture; public UnknownProfileFlow(FlowCallback callback) { super(callback); - this.future = Futures.failed(new IllegalArgumentException("Unknown Profile Type")); - this.future.whenComplete(this::onSessionComplete); + this.sessionFuture = Futures.failed(new IllegalArgumentException("Unknown Profile Type")); + this.sessionFuture.whenComplete(this::onSessionComplete); } @Override - public final CompletableFuture getSession() { - return this.future; + public CompletableFuture getSessionFuture() { + return this.sessionFuture; } @Override - public final boolean hasProfile() { + public boolean hasProfile() { return false; } @Override - public final CompletableFuture getProfile() { + public CompletableFuture getProfileFuture() { throw new IllegalStateException("Profile creation not supported"); } } diff --git a/src/common/java/technicianlp/reauth/authentication/flows/impl/XboxAuthenticationFlow.java b/src/common/java/technicianlp/reauth/authentication/flows/impl/XboxAuthenticationFlow.java index 40ab405..13cfdaf 100644 --- a/src/common/java/technicianlp/reauth/authentication/flows/impl/XboxAuthenticationFlow.java +++ b/src/common/java/technicianlp/reauth/authentication/flows/impl/XboxAuthenticationFlow.java @@ -23,37 +23,42 @@ final class XboxAuthenticationFlow extends FlowBase { private final CompletableFuture session; private final CompletableFuture> xstsAuthResponse; - public XboxAuthenticationFlow(CompletableFuture xblToken, FlowCallback callback) { + XboxAuthenticationFlow(CompletableFuture xblToken, FlowCallback callback) { super(callback); - this.xstsAuthResponse = xblToken.thenApplyAsync(this.wrapStep(FlowStage.MS_AUTH_XSTS, MsAuthAPI::authenticateXSTS), this.executor); - CompletableFuture xsts = this.xstsAuthResponse.thenApply(this.wrap(Response::get)); - CompletableFuture mojang = xsts.thenApplyAsync(this.wrapStep(FlowStage.MS_AUTH_MOJANG, this::authenticateMojang), this.executor); + this.xstsAuthResponse = + xblToken.thenApplyAsync(this.wrapStep(FlowStage.MS_AUTH_XSTS, MsAuthAPI::authenticateXSTS), this.executor); + CompletableFuture xsts = this.xstsAuthResponse.thenApply(FlowBase.wrap(Response::get)); + CompletableFuture mojang = + xsts.thenApplyAsync(this.wrapStep(FlowStage.MS_AUTH_MOJANG, XboxAuthenticationFlow::authenticateMojang), + this.executor); CompletableFuture token = mojang.thenApply(MojangAuthResponse::getToken); - CompletableFuture profile = token.thenApplyAsync(this.wrapStep(FlowStage.MS_FETCH_PROFILE, MsAuthAPI::fetchProfile), this.executor); - this.session = token.thenCombine(profile, this::makeSession); + CompletableFuture profile = + token.thenApplyAsync(this.wrapStep(FlowStage.MS_FETCH_PROFILE, MsAuthAPI::fetchProfile), this.executor); + this.session = token.thenCombine(profile, XboxAuthenticationFlow::makeSession); this.registerDependantStages(this.xstsAuthResponse, xsts, mojang, profile, this.session); } - private MojangAuthResponse authenticateMojang(XboxAuthResponse xsts) throws UnreachableServiceException, InvalidResponseException { + private static MojangAuthResponse authenticateMojang(XboxAuthResponse xsts) throws UnreachableServiceException, + InvalidResponseException { return MsAuthAPI.authenticateMojang(xsts.token, xsts.userHash); } - private SessionData makeSession(String token, ProfileResponse profile) { + private static SessionData makeSession(String token, ProfileResponse profile) { return new SessionData(profile.name, profile.uuid, token, "msa"); } @Override - public final CompletableFuture getSession() { + public CompletableFuture getSessionFuture() { return this.session; } /** - * checks whether the passed response contains an error caused expired/invalid XASU-Token. - * If a valid response is passed or the request failed with an exception false is returned. + * checks whether the passed response contains an error caused expired/invalid XASU-Token. If a valid response is + * passed or the request failed with an exception false is returned. */ - final boolean hasExpiredTokenError() { + boolean hasExpiredTokenError() { if (!this.xstsAuthResponse.isDone()) { ReAuth.log.warn("Cant determine token expiration on unfinished request"); return false; @@ -65,7 +70,7 @@ final boolean hasExpiredTokenError() { if (response.isValid()) { return false; } - XboxAuthResponse rawResponse = response.getUnchecked(); + XboxAuthResponse rawResponse = response.getResponse(); if (rawResponse != null) { return XSTS_ERR_TOKEN_EXPIRED.equals(rawResponse.error) || XSTS_ERR_TOKEN_INVALID.equals(rawResponse.error); } @@ -73,12 +78,12 @@ final boolean hasExpiredTokenError() { } @Override - public final boolean hasProfile() { + public boolean hasProfile() { return false; } @Override - public final CompletableFuture getProfile() { + public CompletableFuture getProfileFuture() { throw new IllegalStateException("Profile creation not supported"); } } diff --git a/src/common/java/technicianlp/reauth/authentication/flows/impl/util/AuthBiFunction.java b/src/common/java/technicianlp/reauth/authentication/flows/impl/util/AuthBiFunction.java index 31b022d..fd375d6 100644 --- a/src/common/java/technicianlp/reauth/authentication/flows/impl/util/AuthBiFunction.java +++ b/src/common/java/technicianlp/reauth/authentication/flows/impl/util/AuthBiFunction.java @@ -8,8 +8,9 @@ import java.util.function.BiFunction; /** - * Functional interface that allows a method reference to throw {@link UnreachableServiceException} or {@link InvalidResponseException}. - * Thrown Exceptions will be wrapped with a {@link CompletionException} for use with {@link CompletableFuture}. + * Functional interface that allows a method reference to throw {@link UnreachableServiceException} or + * {@link InvalidResponseException}. Thrown Exceptions will be wrapped with a {@link CompletionException} for use with + * {@link CompletableFuture}. */ @FunctionalInterface public interface AuthBiFunction extends BiFunction { diff --git a/src/common/java/technicianlp/reauth/authentication/flows/impl/util/AuthFunction.java b/src/common/java/technicianlp/reauth/authentication/flows/impl/util/AuthFunction.java index 9a243fc..af2a1f6 100644 --- a/src/common/java/technicianlp/reauth/authentication/flows/impl/util/AuthFunction.java +++ b/src/common/java/technicianlp/reauth/authentication/flows/impl/util/AuthFunction.java @@ -8,8 +8,9 @@ import java.util.function.Function; /** - * Functional interface that allows a method reference to throw {@link UnreachableServiceException} or {@link InvalidResponseException}. - * Thrown Exceptions will be wrapped with a {@link CompletionException} for use with {@link CompletableFuture}. + * Functional interface that allows a method reference to throw {@link UnreachableServiceException} or + * {@link InvalidResponseException}. Thrown Exceptions will be wrapped with a {@link CompletionException} for use with + * {@link CompletableFuture}. */ @FunctionalInterface public interface AuthFunction extends Function { diff --git a/src/common/java/technicianlp/reauth/authentication/flows/impl/util/AuthSupplier.java b/src/common/java/technicianlp/reauth/authentication/flows/impl/util/AuthSupplier.java index ace2003..412d45e 100644 --- a/src/common/java/technicianlp/reauth/authentication/flows/impl/util/AuthSupplier.java +++ b/src/common/java/technicianlp/reauth/authentication/flows/impl/util/AuthSupplier.java @@ -8,8 +8,9 @@ import java.util.function.Supplier; /** - * Functional interface that allows a method reference to throw {@link UnreachableServiceException} or {@link InvalidResponseException}. - * Thrown Exceptions will be wrapped with a {@link CompletionException} for use with {@link CompletableFuture}. + * Functional interface that allows a method reference to throw {@link UnreachableServiceException} or + * {@link InvalidResponseException}. Thrown Exceptions will be wrapped with a {@link CompletionException} for use with + * {@link CompletableFuture}. */ @FunctionalInterface public interface AuthSupplier extends Supplier { diff --git a/src/common/java/technicianlp/reauth/authentication/flows/impl/util/Futures.java b/src/common/java/technicianlp/reauth/authentication/flows/impl/util/Futures.java index 83257e2..e734e75 100644 --- a/src/common/java/technicianlp/reauth/authentication/flows/impl/util/Futures.java +++ b/src/common/java/technicianlp/reauth/authentication/flows/impl/util/Futures.java @@ -4,13 +4,17 @@ import java.util.function.Function; import java.util.function.Supplier; -public final class Futures { +public enum Futures { + ; - public static Function> conditional(CompletableFuture value, CompletableFuture fallback) { + public static Function> conditional(CompletableFuture value, + CompletableFuture fallback) { return condition -> condition ? value : fallback; } - public static Function> conditional(Supplier> value, CompletableFuture fallback) { + public static Function> conditional( + Supplier> value, + CompletableFuture fallback) { return condition -> condition ? value.get() : fallback; } diff --git a/src/common/java/technicianlp/reauth/authentication/http/HttpUtil.java b/src/common/java/technicianlp/reauth/authentication/http/HttpUtil.java index 79a5325..c478203 100644 --- a/src/common/java/technicianlp/reauth/authentication/http/HttpUtil.java +++ b/src/common/java/technicianlp/reauth/authentication/http/HttpUtil.java @@ -13,19 +13,15 @@ import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLSocketFactory; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.Reader; -import java.io.UnsupportedEncodingException; +import java.io.*; import java.net.URL; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.stream.Collectors; -public final class HttpUtil { +public enum HttpUtil { + ; private static final Gson GSON; @@ -38,28 +34,38 @@ public final class HttpUtil { GSON = builder.create(); } - public static R performFormRequest(String url, RequestObject.Form form) throws UnreachableServiceException, InvalidResponseException { + public static R performFormRequest(String url, RequestObject.Form form) throws + UnreachableServiceException, InvalidResponseException { return performWrappedFormRequest(url, form).get(); } - public static Response performWrappedFormRequest(String url, RequestObject.Form form) throws UnreachableServiceException { + public static Response performWrappedFormRequest(String url, + RequestObject.Form form) throws + UnreachableServiceException { String body = form.getFields().entrySet().stream().map(HttpUtil::urlEncode).collect(Collectors.joining("&")); return performRequest(url, body, "application/x-www-form-urlencoded", null, form.getResponseClass()); } - public static R performJsonRequest(String url, RequestObject.JSON payload) throws UnreachableServiceException, InvalidResponseException { + public static R performJsonRequest(String url, RequestObject.JSON payload) throws + UnreachableServiceException, InvalidResponseException { return performWrappedJsonRequest(url, payload).get(); } - public static Response performWrappedJsonRequest(String url, RequestObject.JSON payload) throws UnreachableServiceException { + public static Response performWrappedJsonRequest(String url, + RequestObject.JSON payload) throws + UnreachableServiceException { return performRequest(url, GSON.toJson(payload), "application/json", null, payload.getResponseClass()); } - public static R performGetRequest(String url, String bearer, Class responseType) throws UnreachableServiceException, InvalidResponseException { + public static R performGetRequest(String url, String bearer, + Class responseType) throws + UnreachableServiceException, InvalidResponseException { return performWrappedGetRequest(url, bearer, responseType).get(); } - public static Response performWrappedGetRequest(String url, String bearer, Class responseType) throws UnreachableServiceException { + public static Response performWrappedGetRequest(String url, String bearer, + Class responseType) throws + UnreachableServiceException { return performRequest(url, null, null, bearer, responseType); } @@ -68,7 +74,9 @@ public static Response performWrappedGetRequest(St *

    * {@link IllegalStateException} can occur in GSON when type-mismatches are encountered */ - private static Response performRequest(String url, String body, String contentType, String token, Class responseType) throws UnreachableServiceException { + private static Response performRequest(String url, String body, String contentType, + String token, Class responseType) throws + UnreachableServiceException { try { HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection(); @@ -117,10 +125,7 @@ private static Response performRequest(String url, } private static String urlEncode(Map.Entry entry) { - try { - return URLEncoder.encode(entry.getKey(), "UTF-8") + "=" + URLEncoder.encode(entry.getValue(), "UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException("UTF-8 unsupported", e); - } + return URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8) + "=" + URLEncoder.encode(entry.getValue(), + StandardCharsets.UTF_8); } } diff --git a/src/common/java/technicianlp/reauth/authentication/http/Response.java b/src/common/java/technicianlp/reauth/authentication/http/Response.java index 9e8e18d..cb1f595 100644 --- a/src/common/java/technicianlp/reauth/authentication/http/Response.java +++ b/src/common/java/technicianlp/reauth/authentication/http/Response.java @@ -14,7 +14,7 @@ public Response(int statusCode, R response) { this.response = response; } - public final boolean isValid() { + public boolean isValid() { if (!(200 <= this.statusCode && this.statusCode < 300)) { return false; } @@ -25,17 +25,18 @@ public final boolean isValid() { } } - public final R get() throws InvalidResponseException { + public R get() throws InvalidResponseException { if (this.isValid()) { return this.response; } else if (this.response != null) { - throw new InvalidResponseException("Error received with code " + this.statusCode + ": " + this.response.getError()); + throw new InvalidResponseException( + "Error received with code " + this.statusCode + ": " + this.response.getError()); } else { throw new InvalidResponseException("Received error code " + this.statusCode); } } - public final R getUnchecked() { + public R getResponse() { return this.response; } } diff --git a/src/common/java/technicianlp/reauth/authentication/http/server/AuthenticationCodeServer.java b/src/common/java/technicianlp/reauth/authentication/http/server/AuthenticationCodeServer.java index 7692ff1..aa74e4e 100644 --- a/src/common/java/technicianlp/reauth/authentication/http/server/AuthenticationCodeServer.java +++ b/src/common/java/technicianlp/reauth/authentication/http/server/AuthenticationCodeServer.java @@ -14,13 +14,14 @@ /** * Authentication with Microsoft requires a webserver on localhost to receive the issued authentication code. *
    - * Class provides encapsulation of the jdk-builtin {@link HttpServer}. - * The API provided by the {@link com.sun.net.httpserver} package is an official and documented API since Java 6. - * While the API should be available in every major java distribution, it is an optional API and may therefore be missing in rare cases. - * This is denoted by both the {@link jdk.Exported} Annotation and online documentation. + * Class provides encapsulation of the jdk-builtin {@link HttpServer}. The API provided by the + * {@link com.sun.net.httpserver} package is an official and documented API since Java 6. While the API should be + * available in every major java distribution, it is an optional API and may therefore be missing in rare cases. This is + * denoted by both the {@code jdk.Exported} Annotation and + * online documentation. *
    - * The HttpServer is started on the supplied Executor. - * After the code has been received the future is completed synchronously and the server automatically {@link #stop(boolean) stops} asynchronously. + * The HttpServer is started on the supplied Executor. After the code has been received the future is completed + * synchronously and the server automatically {@link #stop(boolean) stops} asynchronously. */ public final class AuthenticationCodeServer { @@ -28,7 +29,8 @@ public final class AuthenticationCodeServer { private boolean running = true; - public AuthenticationCodeServer(int port, String loginUrl, CompletableFuture codeFuture, Executor executor) throws IOException, NoClassDefFoundError { + public AuthenticationCodeServer(int port, String loginUrl, CompletableFuture codeFuture, + Executor executor) throws IOException, NoClassDefFoundError { InetSocketAddress localAddress = new InetSocketAddress(InetAddress.getLoopbackAddress(), port); HttpServer server = HttpServer.create(localAddress, 0); server.setExecutor(executor); @@ -46,20 +48,24 @@ public AuthenticationCodeServer(int port, String loginUrl, CompletableFuture server.stop(immediate ? 0 : 1); } - public final synchronized void stop(boolean immediate) { - if (this.running) { - this.running = false; - if (!immediate) { - ReAuth.log.info("About to stop local endpoint"); - try { - TimeUnit.SECONDS.sleep(1); - } catch (InterruptedException exception) { - ReAuth.log.warn("Interrupted while waiting to stop local endpoint", exception); - } + public void stop(boolean immediate) { + synchronized (this) { + if (this.running) { + this.running = false; + } else { + return; } - ReAuth.log.info("Stopping local endpoint"); - this.stopServer.accept(immediate); - ReAuth.log.info("Stopped local endpoint"); } + if (!immediate) { + ReAuth.log.info("About to stop local endpoint"); + try { + TimeUnit.SECONDS.sleep(1); + } catch (InterruptedException exception) { + ReAuth.log.warn("Interrupted while waiting to stop local endpoint", exception); + } + } + ReAuth.log.info("Stopping local endpoint"); + this.stopServer.accept(immediate); + ReAuth.log.info("Stopped local endpoint"); } } diff --git a/src/common/java/technicianlp/reauth/authentication/http/server/CodeHandler.java b/src/common/java/technicianlp/reauth/authentication/http/server/CodeHandler.java index 59df3a9..4687fbd 100644 --- a/src/common/java/technicianlp/reauth/authentication/http/server/CodeHandler.java +++ b/src/common/java/technicianlp/reauth/authentication/http/server/CodeHandler.java @@ -5,7 +5,6 @@ import technicianlp.reauth.ReAuth; import java.io.IOException; -import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.HashMap; @@ -17,13 +16,13 @@ final class CodeHandler extends Handler { private final CompletableFuture codeFuture; - CodeHandler(PageWriter writer, CompletableFuture codeFuture) { - super(writer); + CodeHandler(PageWriter pageWriter, CompletableFuture codeFuture) { + super(pageWriter); this.codeFuture = codeFuture; } @Override - public final void handle(HttpExchange exchange) throws IOException { + public void handle(HttpExchange exchange) throws IOException { try { String method = exchange.getRequestMethod().toUpperCase(Locale.ROOT); @@ -50,45 +49,43 @@ private Response handlePostRequest(HttpExchange exchange) throws IOException { } String body = IOUtils.toString(exchange.getRequestBody(), StandardCharsets.UTF_8); - Map formFields = (this.parseFormFields(body)); + Map formFields = (parseFormFields(body)); if (formFields.containsKey("code")) { ReAuth.log.info("Received Microsoft Authentication Code"); this.codeFuture.complete(formFields.get("code")); - return new Response(HttpStatus.OK).setContent(CONTENT_TYPE_HTML, this.pageWriter.createSuccessResponsePage()); + return new Response(HttpStatus.OK).setContent(CONTENT_TYPE_HTML, + PageWriter.createSuccessResponsePage()); } else { String error = formFields.getOrDefault("error", "unknown"); - ReAuth.log.error("Received Error from Microsoft Authentication: " + error); - return new Response(HttpStatus.Bad_Request).setContent(CONTENT_TYPE_HTML, this.pageWriter.createErrorResponsePage(error)); + ReAuth.log.error("Received Error from Microsoft Authentication: {}", error); + return new Response(HttpStatus.Bad_Request).setContent(CONTENT_TYPE_HTML, + this.pageWriter.createErrorResponsePage(error)); } } /** - * Decodes the Contents of a application/x-www-form-urlencoded request. - * Duplicate field names are discarded. + * Decodes the Contents of an application/x-www-form-urlencoded request. Duplicate field names are discarded. */ - private Map parseFormFields(String formUrlEncoded) { + private static Map parseFormFields(String formUrlEncoded) { Map formFields = new HashMap<>(); String[] fields = formUrlEncoded.split("&"); - try { - for (String field : fields) { - if (field.isEmpty()) { - continue; - } - String key = field; - String value = ""; - - int delimiter = field.indexOf('='); - if (delimiter != -1) { - key = field.substring(0, delimiter); - value = field.substring(delimiter + 1); - } + for (String field : fields) { + if (field.isEmpty()) { + continue; + } + String key = field; + String value = ""; - formFields.putIfAbsent(URLDecoder.decode(key, "UTF-8"), URLDecoder.decode(value, "UTF-8")); + int delimiter = field.indexOf('='); + if (delimiter != -1) { + key = field.substring(0, delimiter); + value = field.substring(delimiter + 1); } - } catch (UnsupportedEncodingException exception) { - throw new RuntimeException("UTF-8 unsupported", exception); + + formFields.putIfAbsent(URLDecoder.decode(key, StandardCharsets.UTF_8), URLDecoder.decode(value, + StandardCharsets.UTF_8)); } return formFields; } diff --git a/src/common/java/technicianlp/reauth/authentication/http/server/Handler.java b/src/common/java/technicianlp/reauth/authentication/http/server/Handler.java index 0251925..812bf0d 100644 --- a/src/common/java/technicianlp/reauth/authentication/http/server/Handler.java +++ b/src/common/java/technicianlp/reauth/authentication/http/server/Handler.java @@ -23,7 +23,7 @@ final void sendResponse(HttpExchange exchange, Response response) throws IOExcep Headers responseHeaders = exchange.getResponseHeaders(); response.getHeaders().forEach(responseHeaders::set); - if (exchange.getRequestMethod().equals("HEAD")) { + if ("HEAD".equals(exchange.getRequestMethod())) { exchange.sendResponseHeaders(response.getHttpStatus().code, -1); } else { if (!response.hasContent()) { diff --git a/src/common/java/technicianlp/reauth/authentication/http/server/PageWriter.java b/src/common/java/technicianlp/reauth/authentication/http/server/PageWriter.java index ffbb397..aaee8c7 100644 --- a/src/common/java/technicianlp/reauth/authentication/http/server/PageWriter.java +++ b/src/common/java/technicianlp/reauth/authentication/http/server/PageWriter.java @@ -19,25 +19,27 @@ final class PageWriter { this.loginUrl = loginUrl; } - final ByteBuffer createSuccessResponsePage() throws IOException { - String successMessage = this.formatAndEscape("reauth.msauth.code.success"); - String closeMessage = this.formatAndEscape("reauth.msauth.code.success.close"); - return this.createPage(successMessage, closeMessage); + static ByteBuffer createSuccessResponsePage() throws IOException { + String successMessage = formatAndEscape("reauth.msauth.code.success"); + String closeMessage = formatAndEscape("reauth.msauth.code.success.close"); + return createPage(successMessage, closeMessage); } - final ByteBuffer createErrorResponsePage(String errorCode) throws IOException { - String errorMessage = this.formatAndEscape(this.getErrorMessage(errorCode)); - String retryMessage = this.createLink(this.formatAndEscape("reauth.msauth.code.retry"), this.loginUrl); - return this.createPage(errorMessage, retryMessage); + ByteBuffer createErrorResponsePage(String errorCode) throws IOException { + String errorMessage = formatAndEscape(getErrorMessage(errorCode)); + String retryMessage = + createLink(formatAndEscape("reauth.msauth.code.retry"), this.loginUrl); + return createPage(errorMessage, retryMessage); } - final ByteBuffer createHttpErrorResponsePage(HttpStatus error) throws IOException { - String errorMessage = this.formatAndEscape("reauth.msauth.code.error.http" + error.code); - String retryMessage = this.createLink(this.formatAndEscape("reauth.msauth.code.retry"), this.loginUrl); - return this.createPage(errorMessage, retryMessage); + ByteBuffer createHttpErrorResponsePage(HttpStatus error) throws IOException { + String errorMessage = formatAndEscape("reauth.msauth.code.error.http." + error.code); + String retryMessage = + createLink(formatAndEscape("reauth.msauth.code.retry"), this.loginUrl); + return createPage(errorMessage, retryMessage); } - private ByteBuffer createPage(String text1, String text2) throws IOException { + private static ByteBuffer createPage(String text1, String text2) throws IOException { try (InputStream is = AuthenticationCodeServer.class.getResourceAsStream("/resources/reauth/reauth.html")) { if (is != null) { String page = IOUtils.toString(is, StandardCharsets.UTF_8); @@ -51,25 +53,20 @@ private ByteBuffer createPage(String text1, String text2) throws IOException { } } - private String createLink(String content, String href) { + private static String createLink(String content, String href) { return "" + content + ""; } - private String getErrorMessage(String authError) { - String type = "unknown"; - switch (authError) { - case "access_denied": - type = "cancelled"; - break; - case "server_error": - case "temporarily_unavailable": - type = "server"; - break; - } + private static String getErrorMessage(String authError) { + String type = switch (authError) { + case "access_denied" -> "cancelled"; + case "server_error", "temporarily_unavailable" -> "server"; + default -> "unknown"; + }; return "reauth.msauth.code.fail." + type; } - private String formatAndEscape(String key, Object... arguments) { + private static String formatAndEscape(String key, Object... arguments) { String text = ReAuth.i18n.apply(key, arguments); text = text.replaceAll("&", "&"); text = text.replaceAll("<", "<"); diff --git a/src/common/java/technicianlp/reauth/authentication/http/server/ResourcesHandler.java b/src/common/java/technicianlp/reauth/authentication/http/server/ResourcesHandler.java index e4d819e..d452c82 100644 --- a/src/common/java/technicianlp/reauth/authentication/http/server/ResourcesHandler.java +++ b/src/common/java/technicianlp/reauth/authentication/http/server/ResourcesHandler.java @@ -12,19 +12,19 @@ final class ResourcesHandler extends Handler { - ResourcesHandler(PageWriter writer) { - super(writer); + ResourcesHandler(PageWriter pageWriter) { + super(pageWriter); } @Override - public final void handle(HttpExchange exchange) throws IOException { + public void handle(HttpExchange exchange) throws IOException { try { String method = exchange.getRequestMethod().toUpperCase(Locale.ROOT); Response response; - if (method.equals("GET") || method.equals("HEAD")) { - response = this.handleResourceGet(exchange.getRequestURI().getPath()); - } else if (method.equals("POST")) { + if ("GET".equals(method) || "HEAD".equals(method)) { + response = handleResourceGet(exchange.getRequestURI().getPath()); + } else if ("POST".equals(method)) { response = new Response(HttpStatus.Method_Not_Allowed).setHeader("Allow", "GET, HEAD"); } else { response = new Response(HttpStatus.Not_Implemented); @@ -37,7 +37,7 @@ public final void handle(HttpExchange exchange) throws IOException { } } - private Response handleResourceGet(String path) throws IOException { + private static Response handleResourceGet(String path) throws IOException { String contentType = null; String resource = null; if ("/res/icon.png".equals(path)) { diff --git a/src/common/java/technicianlp/reauth/authentication/http/server/Response.java b/src/common/java/technicianlp/reauth/authentication/http/server/Response.java index ae041e5..0763090 100644 --- a/src/common/java/technicianlp/reauth/authentication/http/server/Response.java +++ b/src/common/java/technicianlp/reauth/authentication/http/server/Response.java @@ -15,30 +15,30 @@ final class Response { this.headers = new HashMap<>(); } - final Response setContent(String contentType, ByteBuffer content) { + Response setContent(String contentType, ByteBuffer content) { this.setHeader("Content-Type", contentType); this.pageContent = content; return this; } - final Response setHeader(String name, String value) { - this.getHeaders().put(name, value); + Response setHeader(String name, String value) { + this.headers.put(name, value); return this; } - final HttpStatus getHttpStatus() { + HttpStatus getHttpStatus() { return this.httpStatus; } - final boolean hasContent() { + boolean hasContent() { return this.pageContent != null; } - final ByteBuffer getPageContent() { + ByteBuffer getPageContent() { return this.pageContent; } - final Map getHeaders() { + Map getHeaders() { return this.headers; } } diff --git a/src/common/java/technicianlp/reauth/configuration/ProfileBuilder.java b/src/common/java/technicianlp/reauth/configuration/ProfileBuilder.java index 50b1cbb..ea51ae1 100644 --- a/src/common/java/technicianlp/reauth/configuration/ProfileBuilder.java +++ b/src/common/java/technicianlp/reauth/configuration/ProfileBuilder.java @@ -15,12 +15,12 @@ public final class ProfileBuilder { public ProfileBuilder(SessionData session, ProfileEncryption encryption) { this.encryption = encryption; - this.profile.put(ProfileConstants.NAME, session.username); - this.profile.put(ProfileConstants.UUID, session.uuid); + this.profile.put(ProfileConstants.NAME, session.username()); + this.profile.put(ProfileConstants.UUID, session.uuid()); encryption.saveToProfile(this.profile); } - public final Profile buildMicrosoft(Tokens tokens) { + public Profile buildMicrosoft(Tokens tokens) { this.profile.put(ProfileConstants.PROFILE_TYPE, ProfileConstants.PROFILE_TYPE_MICROSOFT); this.profile.put(ProfileConstants.XBL_TOKEN, this.encryption.encryptFieldOne(tokens.getXblToken())); this.profile.put(ProfileConstants.REFRESH_TOKEN, this.encryption.encryptFieldTwo(tokens.getRefreshToken())); @@ -28,7 +28,7 @@ public final Profile buildMicrosoft(Tokens tokens) { return ReAuth.profiles.createProfile(this.profile); } - public final Profile buildMojang(String username, String password) { + public Profile buildMojang(String username, String password) { this.profile.put(ProfileConstants.PROFILE_TYPE, ProfileConstants.PROFILE_TYPE_MOJANG); this.profile.put(ProfileConstants.USERNAME, this.encryption.encryptFieldOne(username)); this.profile.put(ProfileConstants.PASSWORD, this.encryption.encryptFieldTwo(password)); diff --git a/src/common/java/technicianlp/reauth/configuration/ProfileConstants.java b/src/common/java/technicianlp/reauth/configuration/ProfileConstants.java index 78703ac..5c51d3f 100644 --- a/src/common/java/technicianlp/reauth/configuration/ProfileConstants.java +++ b/src/common/java/technicianlp/reauth/configuration/ProfileConstants.java @@ -2,10 +2,10 @@ import com.google.common.collect.ImmutableList; -import java.util.ArrayList; import java.util.List; -public final class ProfileConstants { +public enum ProfileConstants { + ; // profile type public static final String PROFILE_TYPE = "type"; @@ -35,19 +35,15 @@ public final class ProfileConstants { * Used to provide consistent ordering for config entries. */ private static final List propertyOrder = ImmutableList.of( - ProfileConstants.PROFILE_TYPE, - ProfileConstants.NAME, - ProfileConstants.UUID, - ProfileConstants.USERNAME, - ProfileConstants.XBL_TOKEN, - ProfileConstants.PASSWORD, - ProfileConstants.REFRESH_TOKEN, - ProfileConstants.KEY, - ProfileConstants.SALT); - - static List getOrderedProfileKeys() { - return new ArrayList<>(propertyOrder); - } + PROFILE_TYPE, + NAME, + UUID, + USERNAME, + XBL_TOKEN, + PASSWORD, + REFRESH_TOKEN, + KEY, + SALT); static int compareProfileKeys(String key1, String key2) { if (key1.equals(key2)) { diff --git a/src/common/java/technicianlp/reauth/crypto/Crypto.java b/src/common/java/technicianlp/reauth/crypto/Crypto.java index a61da5f..9b4c9e1 100644 --- a/src/common/java/technicianlp/reauth/crypto/Crypto.java +++ b/src/common/java/technicianlp/reauth/crypto/Crypto.java @@ -5,19 +5,18 @@ import java.security.SecureRandom; -public final class Crypto { +public enum Crypto { + ; private static String configPath = ""; public static ProfileEncryption getProfileEncryption(Profile profile) { - switch (profile.getValue(ProfileConstants.KEY)) { - case ProfileConstants.KEY_AUTO: - return new EncryptionAutomatic(configPath, profile.getValue(ProfileConstants.SALT)); - case ProfileConstants.KEY_NONE: - return new EncryptionNone(); - default: - throw new IllegalArgumentException("Unknown Encryption Type"); - } + return switch (profile.getValue(ProfileConstants.KEY)) { + case ProfileConstants.KEY_AUTO -> + new EncryptionAutomatic(configPath, profile.getValue(ProfileConstants.SALT)); + case ProfileConstants.KEY_NONE -> new EncryptionNone(); + default -> throw new IllegalArgumentException("Unknown Encryption Type"); + }; } public static ProfileEncryption newEncryption() { diff --git a/src/common/java/technicianlp/reauth/crypto/EncryptionAutomatic.java b/src/common/java/technicianlp/reauth/crypto/EncryptionAutomatic.java index 9978038..7a5e01d 100644 --- a/src/common/java/technicianlp/reauth/crypto/EncryptionAutomatic.java +++ b/src/common/java/technicianlp/reauth/crypto/EncryptionAutomatic.java @@ -1,6 +1,5 @@ package technicianlp.reauth.crypto; -import technicianlp.reauth.configuration.Profile; import technicianlp.reauth.configuration.ProfileConstants; import javax.crypto.Cipher; @@ -11,7 +10,9 @@ import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; +import java.security.Key; import java.security.NoSuchAlgorithmException; +import java.security.spec.AlgorithmParameterSpec; import java.security.spec.InvalidKeySpecException; import java.util.Base64; import java.util.Map; @@ -27,7 +28,7 @@ final class EncryptionAutomatic implements ProfileEncryption { private final String path; private final byte[] salt; - public EncryptionAutomatic(String path) { + EncryptionAutomatic(String path) { this(path, Crypto.randomBytes(16)); } @@ -48,19 +49,19 @@ public EncryptionAutomatic(String path) { } @Override - public final String decryptFieldOne(String encrypted) throws CryptoException { + public String decryptFieldOne(String encrypted) throws CryptoException { return this.decrypt(encrypted, IV1_OFFSET); } @Override - public final String decryptFieldTwo(String encrypted) throws CryptoException { + public String decryptFieldTwo(String encrypted) throws CryptoException { return this.decrypt(encrypted, IV2_OFFSET); } private String decrypt(String encrypted, int ivOffset) throws CryptoException { try { byte[] raw = Base64.getDecoder().decode(encrypted); - byte[] dec = this.crypt(raw, Cipher.DECRYPT_MODE, this.keyData, ivOffset); + byte[] dec = crypt(raw, Cipher.DECRYPT_MODE, this.keyData, ivOffset); return new String(dec, StandardCharsets.UTF_8); } catch (GeneralSecurityException e) { throw new CryptoException("Decryption failed", e); @@ -68,19 +69,19 @@ private String decrypt(String encrypted, int ivOffset) throws CryptoException { } @Override - public final String encryptFieldOne(String value) throws CryptoException { + public String encryptFieldOne(String value) throws CryptoException { return this.encrypt(value, IV1_OFFSET); } @Override - public final String encryptFieldTwo(String value) throws CryptoException { + public String encryptFieldTwo(String value) throws CryptoException { return this.encrypt(value, IV2_OFFSET); } private String encrypt(String value, int ivOffset) throws CryptoException { try { byte[] raw = value.getBytes(StandardCharsets.UTF_8); - byte[] enc = this.crypt(raw, Cipher.ENCRYPT_MODE, this.keyData, ivOffset); + byte[] enc = crypt(raw, Cipher.ENCRYPT_MODE, this.keyData, ivOffset); return Base64.getEncoder().encodeToString(enc); } catch (GeneralSecurityException e) { throw new CryptoException("Encryption failed", e); @@ -90,9 +91,9 @@ private String encrypt(String value, int ivOffset) throws CryptoException { /** * Encrypt or decrypt the supplied data with the given Key and IV */ - private byte[] crypt(byte[] data, int mode, byte[] keyData, int ivOffset) throws GeneralSecurityException { - SecretKeySpec secretKey = new SecretKeySpec(keyData, 0, 32, "AES"); - IvParameterSpec ivParameterSpec = new IvParameterSpec(keyData, ivOffset, 16); + private static byte[] crypt(byte[] data, int mode, byte[] keyData, int ivOffset) throws GeneralSecurityException { + Key secretKey = new SecretKeySpec(keyData, 0, 32, "AES"); + AlgorithmParameterSpec ivParameterSpec = new IvParameterSpec(keyData, ivOffset, 16); Cipher aes = Cipher.getInstance("AES/CBC/PKCS5Padding"); aes.init(mode, secretKey, ivParameterSpec); @@ -100,13 +101,13 @@ private byte[] crypt(byte[] data, int mode, byte[] keyData, int ivOffset) throws } @Override - public final void saveToProfile(Map profile) { + public void saveToProfile(Map profile) { profile.put(ProfileConstants.KEY, ProfileConstants.KEY_AUTO); profile.put(ProfileConstants.SALT, Base64.getEncoder().encodeToString(this.salt)); } @Override - public final ProfileEncryption randomizedCopy() { + public ProfileEncryption randomizedCopy() { return new EncryptionAutomatic(this.path); } } diff --git a/src/common/java/technicianlp/reauth/crypto/EncryptionNone.java b/src/common/java/technicianlp/reauth/crypto/EncryptionNone.java index 1c4327c..c71b7ac 100644 --- a/src/common/java/technicianlp/reauth/crypto/EncryptionNone.java +++ b/src/common/java/technicianlp/reauth/crypto/EncryptionNone.java @@ -1,6 +1,5 @@ package technicianlp.reauth.crypto; -import technicianlp.reauth.configuration.Profile; import technicianlp.reauth.configuration.ProfileConstants; import java.util.Map; @@ -8,32 +7,32 @@ final class EncryptionNone implements ProfileEncryption { @Override - public final String decryptFieldOne(String value) { - return value; + public String decryptFieldOne(String encrypted) { + return encrypted; } @Override - public final String decryptFieldTwo(String value) { - return value; + public String decryptFieldTwo(String encrypted) { + return encrypted; } @Override - public final String encryptFieldOne(String value) { + public String encryptFieldOne(String value) { return value; } @Override - public final String encryptFieldTwo(String value) { + public String encryptFieldTwo(String value) { return value; } @Override - public final void saveToProfile(Map profile) { + public void saveToProfile(Map profile) { profile.put(ProfileConstants.KEY, ProfileConstants.KEY_NONE); } @Override - public final ProfileEncryption randomizedCopy() { + public ProfileEncryption randomizedCopy() { return new EncryptionNone(); } } diff --git a/src/common/java/technicianlp/reauth/crypto/PkceChallenge.java b/src/common/java/technicianlp/reauth/crypto/PkceChallenge.java index 017d077..43c76f3 100644 --- a/src/common/java/technicianlp/reauth/crypto/PkceChallenge.java +++ b/src/common/java/technicianlp/reauth/crypto/PkceChallenge.java @@ -30,11 +30,11 @@ public final class PkceChallenge { } } - public final String getChallenge() { + public String getChallenge() { return this.challenge; } - public final String getVerifier() { + public String getVerifier() { return this.verifier; } } diff --git a/src/common/java/technicianlp/reauth/mojangfix/CertWorkaround.java b/src/common/java/technicianlp/reauth/mojangfix/CertWorkaround.java index 2faba9b..51a9f3f 100644 --- a/src/common/java/technicianlp/reauth/mojangfix/CertWorkaround.java +++ b/src/common/java/technicianlp/reauth/mojangfix/CertWorkaround.java @@ -2,39 +2,26 @@ import technicianlp.reauth.ReAuth; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLContextSpi; -import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509ExtendedTrustManager; -import javax.net.ssl.X509TrustManager; +import javax.net.ssl.*; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.Socket; import java.security.GeneralSecurityException; import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Deque; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; +import java.util.*; -public final class CertWorkaround { +public enum CertWorkaround { + ; private static final String MICROSOFT2017 = "microsoftrsarootcertificateauthority2017"; private static final String AMAZON1 = "amazonrootca1"; private static final String DIGICERT2 = "digicertglobalrootg2"; - private static SSLSocketFactory socketFactory = null; + private static SSLSocketFactory socketFactory; public static SSLSocketFactory getSocketFactory() { return socketFactory; @@ -44,10 +31,8 @@ public static SSLSocketFactory getSocketFactory() { * The default truststore is missing some CA-Certificates required during authentication with Microsoft/XBox/Mojang * because Mojang for some insane reason ships the 7 years old Java 8 Update 51 (July 14, 2015). *

    - * The following Certificates are installed if they are missing: - * - Microsoft RSA Root Certificate Authority 2017 - * - DigiCert Global Root G2 - * - Amazon Root CA 1 + * The following Certificates are installed if they are missing: - Microsoft RSA Root Certificate Authority 2017 - + * DigiCert Global Root G2 - Amazon Root CA 1 */ static void checkCertificates() { try { @@ -56,7 +41,8 @@ static void checkCertificates() { X509Certificate amazon1 = loadCertificate(cf, AMAZON1); X509Certificate digicert2 = loadCertificate(cf, DIGICERT2); - TrustManagerFactory defaultTrust = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + TrustManagerFactory defaultTrust = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); defaultTrust.init((KeyStore) null); List trustedCerts = getTrustedCerts(defaultTrust); @@ -79,20 +65,23 @@ static void checkCertificates() { } X509ExtendedTrustManager defaultTrustManager = findX509ExtendedTrustManager(defaultTrust); - X509ExtendedTrustManager missingTrustManager = findX509ExtendedTrustManager(createTrustFactory(missingCerts)); - X509ExtendedTrustManager combinedTrustManager = new CombinedX509ExtendedTrustManager(defaultTrustManager, missingTrustManager); + X509ExtendedTrustManager missingTrustManager = + findX509ExtendedTrustManager(createTrustFactory(missingCerts)); + X509ExtendedTrustManager combinedTrustManager = + new CombinedX509ExtendedTrustManager(defaultTrustManager, missingTrustManager); SSLContext context = SSLContext.getInstance("TLS"); context.init(null, new X509ExtendedTrustManager[]{combinedTrustManager}, null); - CertWorkaround.socketFactory = context.getSocketFactory(); + socketFactory = context.getSocketFactory(); ReAuth.log.info("Successfully built SSLSocketFactory with required Certificates"); } catch (GeneralSecurityException | IOException e) { throw new RuntimeException(e); } } - private static X509Certificate loadCertificate(CertificateFactory certFactory, String name) throws CertificateException, IOException { + private static X509Certificate loadCertificate(CertificateFactory certFactory, String name) throws + CertificateException, IOException { try (InputStream is = CertWorkaround.class.getResourceAsStream("/resources/reauth/certs/" + name + ".pem")) { if (is != null) { return (X509Certificate) certFactory.generateCertificate(is); @@ -111,7 +100,8 @@ private static List getTrustedCerts(TrustManagerFactory trustMa } } - private static TrustManagerFactory createTrustFactory(Map certificates) throws KeyStoreException, CertificateException, IOException, NoSuchAlgorithmException { + private static TrustManagerFactory createTrustFactory(Map certificates) throws + GeneralSecurityException, IOException { KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); ks.load(null); for (Map.Entry certificate : certificates.entrySet()) { @@ -125,15 +115,15 @@ private static TrustManagerFactory createTrustFactory(Map tm.checkClientTrusted(chain, authType, socket)); } @Override - public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException { + public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws + CertificateException { this.check(tm -> tm.checkServerTrusted(chain, authType, socket)); } @Override - public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException { + public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws + CertificateException { this.check(tm -> tm.checkClientTrusted(chain, authType, engine)); } @Override - public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException { + public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws + CertificateException { this.check(tm -> tm.checkServerTrusted(chain, authType, engine)); } @@ -181,7 +175,8 @@ public void checkServerTrusted(X509Certificate[] chain, String authType) throws @Override public X509Certificate[] getAcceptedIssuers() { - return this.trustManagers.stream().map(X509TrustManager::getAcceptedIssuers).flatMap(Arrays::stream).toArray(X509Certificate[]::new); + return this.trustManagers.stream().map(X509TrustManager::getAcceptedIssuers).flatMap(Arrays::stream) + .toArray(X509Certificate[]::new); } /** diff --git a/src/common/java/technicianlp/reauth/mojangfix/JceWorkaround.java b/src/common/java/technicianlp/reauth/mojangfix/JceWorkaround.java index 9ad0ef6..0a15da9 100644 --- a/src/common/java/technicianlp/reauth/mojangfix/JceWorkaround.java +++ b/src/common/java/technicianlp/reauth/mojangfix/JceWorkaround.java @@ -10,7 +10,8 @@ import java.security.PermissionCollection; import java.util.Map; -final class JceWorkaround { +enum JceWorkaround { + ; /** * check if CryptoAllPermission is in effect and try to remove Jce restrictions otherwise @@ -23,10 +24,10 @@ static void ensureUnlimitedCryptography() { ReAuth.log.warn("Cryptography is likely deliberately restricted!"); } removeCryptographyRestrictions(); - if (Cipher.getMaxAllowedKeyLength("AES") != Integer.MAX_VALUE) { - ReAuth.log.error("Failed to remove cryptography restriction"); - } else { + if (Cipher.getMaxAllowedKeyLength("AES") == Integer.MAX_VALUE) { ReAuth.log.info("Cryptography restriction removed successfully"); + } else { + ReAuth.log.error("Failed to remove cryptography restriction"); } } } catch (NoSuchAlgorithmException e) { @@ -35,43 +36,42 @@ static void ensureUnlimitedCryptography() { } /** - * Java had for legal reasons limited the allowed strength of cryptographic algorithms. - * Historically to disable this restriction the so called "Java Cryptography Extension (JCE) Unlimited Strength - * Jurisdiction Policy Files" have to be installed within the JRE directory. - * Since update 151 (October 17, 2017) these restrictions can be disabled programmatically - * and have since been disabled by default in update 161 (January 16, 2018). + * Java had for legal reasons limited the allowed strength of cryptographic algorithms. Historically to disable this + * restriction the so-called "Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files" have + * to be installed within the JRE directory. Since update 151 (October 17, 2017) these restrictions can be disabled + * programmatically and have since been disabled by default in update 161 (January 16, 2018). *

    - * Since Mojang for some insane reason ships the 7 years old update 51 (July 14, 2015), installation of the policy files - * would be necessary. Since installation of those files cannot be required of the user, a workaround has been found in - * https://stackoverflow.com/questions/1179672 and is used to disable this restriction at runtime: + * Since Mojang for some insane reason ships the 7 years old update 51 (July 14, 2015), installation of the policy + * files would be necessary. Since installation of those files cannot be required of the user, a workaround has been + * found in https://stackoverflow.com/questions/1179672 + * and is used to disable this restriction at runtime: *

    - * JceSecurity.isRestricted = false; - * JceSecurity.defaultPolicy.perms.clear(); + * JceSecurity.isRestricted = false; JceSecurity.defaultPolicy.perms.clear(); * JceSecurity.defaultPolicy.add(CryptoAllPermission.INSTANCE); *

    * The alternative to this workaround would have been to drop the AES key-length from 256 bits to 128 bits. */ private static void removeCryptographyRestrictions() { try { - final Class jceSecurity = Class.forName("javax.crypto.JceSecurity"); - final Field isRestricted = ReflectionUtils.findField(jceSecurity, "isRestricted"); + Class jceSecurity = Class.forName("javax.crypto.JceSecurity"); + Field isRestricted = ReflectionUtils.findField(jceSecurity, "isRestricted"); ReflectionUtils.unlockFinalField(isRestricted); - final Field defaultPolicy = ReflectionUtils.findField(jceSecurity, "defaultPolicy"); + Field defaultPolicy = ReflectionUtils.findField(jceSecurity, "defaultPolicy"); - final Class cryptoPermissions = Class.forName("javax.crypto.CryptoPermissions"); - final Field perms = ReflectionUtils.findField(cryptoPermissions, "perms"); + Class cryptoPermissions = Class.forName("javax.crypto.CryptoPermissions"); + Field perms = ReflectionUtils.findField(cryptoPermissions, "perms"); - final Class cryptoAllPermission = Class.forName("javax.crypto.CryptoAllPermission"); - final Field instance = ReflectionUtils.findField(cryptoAllPermission, "INSTANCE"); + Class cryptoAllPermission = Class.forName("javax.crypto.CryptoAllPermission"); + Field instance = ReflectionUtils.findField(cryptoAllPermission, "INSTANCE"); ReflectionUtils.setField(isRestricted, null, false); - final PermissionCollection permissionCollection = ReflectionUtils.getField(defaultPolicy, null); + PermissionCollection permissionCollection = ReflectionUtils.getField(defaultPolicy, null); ((Map) ReflectionUtils.getField(perms, permissionCollection)).clear(); permissionCollection.add(ReflectionUtils.getField(instance, null)); - } catch (final Exception e) { + } catch (Exception e) { ReAuth.log.error("Exception removing cryptography restrictions", e); } } diff --git a/src/common/java/technicianlp/reauth/mojangfix/MojangJavaFix.java b/src/common/java/technicianlp/reauth/mojangfix/MojangJavaFix.java index bc97720..398318b 100644 --- a/src/common/java/technicianlp/reauth/mojangfix/MojangJavaFix.java +++ b/src/common/java/technicianlp/reauth/mojangfix/MojangJavaFix.java @@ -7,7 +7,8 @@ import java.time.Period; import java.time.ZoneOffset; -public final class MojangJavaFix { +public enum MojangJavaFix { + ; public static final boolean mojangJava; public static final boolean java8; @@ -23,7 +24,8 @@ public static void fixMojangJava() { Period age = Period.between(LocalDate.of(2015, Month.JULY, 14), LocalDate.now(ZoneOffset.UTC)); ReAuth.log.warn("+------------------------------------------------------------------+"); ReAuth.log.warn("| Please complain to Mojang for shipping an ancient Java version |"); - ReAuth.log.warn("| Java 8 Update 51 is {} years {} months and {} days old |", age.getYears(), age.getMonths(), age.getDays()); + ReAuth.log.warn("| Java 8 Update 51 is {} years {} months and {} days old |", age.getYears(), + age.getMonths(), age.getDays()); ReAuth.log.warn("| Updating would avoid several issues and vulnerabilities |"); ReAuth.log.warn("+------------------------------------------------------------------+"); } diff --git a/src/common/java/technicianlp/reauth/session/SessionChecker.java b/src/common/java/technicianlp/reauth/session/SessionChecker.java index c312806..6459785 100644 --- a/src/common/java/technicianlp/reauth/session/SessionChecker.java +++ b/src/common/java/technicianlp/reauth/session/SessionChecker.java @@ -6,7 +6,8 @@ import java.util.concurrent.CompletableFuture; -public final class SessionChecker { +public enum SessionChecker { + ; /** * Time for which the Validity gets cached (5 Minutes) @@ -17,15 +18,15 @@ public final class SessionChecker { * Current cached Session Validity */ private static SessionStatus status = SessionStatus.UNKNOWN; - private static long lastCheck = 0; + private static long lastCheck; /** - * Get the cached Validity Status of the accessToken - * Re-Validation is done if the cache expires + * Get the cached Validity Status of the accessToken Re-Validation is done if the cache expires */ public static SessionStatus getSessionStatus(String token, String uuid) { - if (lastCheck + cacheTime < System.currentTimeMillis()) + if (lastCheck + cacheTime < System.currentTimeMillis()) { status = SessionStatus.UNKNOWN; + } if (status == SessionStatus.UNKNOWN) { status = SessionStatus.REFRESHING; @@ -34,7 +35,7 @@ public static SessionStatus getSessionStatus(String token, String uuid) { CompletableFuture tokenFuture = CompletableFuture.completedFuture(token); CompletableFuture uuidFuture = CompletableFuture.completedFuture(uuid); tokenFuture.thenCombineAsync(uuidFuture, SessionChecker::getSessionStatus0, ReAuth.executor) - .thenAccept(SessionChecker::setStatus); + .thenAccept(SessionChecker::setStatus); } return status; } @@ -53,6 +54,6 @@ private static SessionStatus getSessionStatus0(String accessToken, String uuid) } private static void setStatus(SessionStatus newStatus) { - SessionChecker.status = newStatus; + status = newStatus; } } diff --git a/src/common/java/technicianlp/reauth/util/ReflectionUtils.java b/src/common/java/technicianlp/reauth/util/ReflectionUtils.java index 7a49b7e..71b4c47 100644 --- a/src/common/java/technicianlp/reauth/util/ReflectionUtils.java +++ b/src/common/java/technicianlp/reauth/util/ReflectionUtils.java @@ -1,46 +1,10 @@ package technicianlp.reauth.util; import java.lang.reflect.Field; -import java.lang.reflect.Method; import java.lang.reflect.Modifier; -public final class ReflectionUtils { - - private static Method findMethodInternal(Class clz, String name, Class... parameterTypes) throws NoSuchMethodException { - Method method = clz.getDeclaredMethod(name, parameterTypes); - method.setAccessible(true); - return method; - } - - public static Method findMethod(Class clz, String name, Class... parameterTypes) { - try { - return findMethodInternal(clz, name, parameterTypes); - } catch (ReflectiveOperationException exception) { - throw new UncheckedReflectiveOperationException("Unable to find Method: " + name, exception); - } - } - - public static Method findObfuscatedMethod(Class clz, String obfName, String name, Class... parameterTypes) { - try { - return findMethodInternal(clz, obfName, parameterTypes); - } catch (NoSuchMethodException suppressed) { - try { - return findMethodInternal(clz, name, parameterTypes); - } catch (NoSuchMethodException exception) { - exception.addSuppressed(suppressed); - throw new UncheckedReflectiveOperationException("Unable to find Obfuscated Method: " + name, exception); - } - } - } - - public static T callMethod(Method method, Object target, Object... args) { - try { - //noinspection unchecked - return (T) method.invoke(target, args); - } catch (ReflectiveOperationException exception) { - throw new UncheckedReflectiveOperationException("Failed reflective Method call", exception); - } - } +public enum ReflectionUtils { + ; private static Field findFieldInternal(Class clz, String name) throws NoSuchFieldException { Field field = clz.getDeclaredField(name); @@ -51,7 +15,7 @@ private static Field findFieldInternal(Class clz, String name) throws NoSuchF public static Field findField(Class clz, String name) { try { return findFieldInternal(clz, name); - } catch (ReflectiveOperationException exception) { + } catch (NoSuchFieldException exception) { throw new UncheckedReflectiveOperationException("Unable to find Field: " + name, exception); } } @@ -75,7 +39,7 @@ public static void unlockFinalField(Field field) { try { Field fieldModifiers = findField(Field.class, "modifiers"); fieldModifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL); - } catch (ReflectiveOperationException exception) { + } catch (IllegalAccessException exception) { throw new UncheckedReflectiveOperationException("Unable to unlock final field", exception); } } @@ -83,7 +47,7 @@ public static void unlockFinalField(Field field) { public static void setField(Field field, Object target, Object value) { try { field.set(target, value); - } catch (ReflectiveOperationException exception) { + } catch (IllegalAccessException exception) { throw new UncheckedReflectiveOperationException("Failed Reflective set", exception); } } @@ -92,7 +56,7 @@ public static T getField(Field field, Object target) { try { //noinspection unchecked return (T) field.get(target); - } catch (ReflectiveOperationException exception) { + } catch (IllegalAccessException exception) { throw new UncheckedReflectiveOperationException("Failed Reflective get", exception); } } diff --git a/src/common/resources/resources/reauth/reauth.html b/src/common/resources/resources/reauth/reauth.html index 5209835..48aee85 100644 --- a/src/common/resources/resources/reauth/reauth.html +++ b/src/common/resources/resources/reauth/reauth.html @@ -54,4 +54,4 @@

    $text1

    $text2

    - \ No newline at end of file + diff --git a/src/main/java/technicianlp/reauth/EventHandler.java b/src/main/java/technicianlp/reauth/EventHandler.java index 5c7cae3..6eb3927 100644 --- a/src/main/java/technicianlp/reauth/EventHandler.java +++ b/src/main/java/technicianlp/reauth/EventHandler.java @@ -24,10 +24,11 @@ import java.lang.reflect.Field; import java.util.List; -public final class EventHandler { +public enum EventHandler { + ; private static final Field disconnectMessage = ReflectionUtils.findObfuscatedField(DisconnectedScreen.class, - "f_95988_", "reason"); + "f_95988_", "reason"); public static void register() { ScreenEvents.AFTER_INIT.register(EventHandler::afterInit); @@ -38,12 +39,12 @@ public static void afterInit(MinecraftClient client, Screen screen, int scaledWi if (screen instanceof MultiplayerScreen) { // Add Button to MultiplayerScreen Screens.getButtons(screen).add(new ButtonWidget(5, 5, 100, 20, - Text.translatable("reauth.gui.button"), b -> openAuthenticationScreen(screen))); + Text.translatable("reauth.gui.button"), button -> openAuthenticationScreen(screen))); ScreenEvents.afterRender(screen).register(EventHandler::afterRender); } else if (screen instanceof TitleScreen) { // Support for Custom Main Menu (add button outside of viewport) Screens.getButtons(screen).add(new ButtonWidget(-50, -50, 20, 20, - Text.translatable("reauth.gui.button"), b -> openAuthenticationScreen(screen))); + Text.translatable("reauth.gui.button"), button -> openAuthenticationScreen(screen))); } else if (screen instanceof DisconnectedScreen) { // Add Buttons to DisconnectedScreen if its reason is an invalid session handleDisconnectScreen(screen); @@ -56,7 +57,7 @@ public static void afterInit(MinecraftClient client, Screen screen, int scaledWi private static void handleDisconnectScreen(Screen screen) { if ("connect.failed".equals(ReconnectHelper.getTranslationKey(screen.getTitle(), false))) { if (ReconnectHelper.getTranslationKey(ReflectionUtils.getField(disconnectMessage, screen), true) - .startsWith("disconnect.loginFailed")) { + .startsWith("disconnect.loginFailed")) { List buttons = Screens.getButtons(screen); ClickableWidget menu = buttons.get(0); @@ -64,13 +65,13 @@ private static void handleDisconnectScreen(Screen screen) { Text retryText; if (profile != null) { retryText = Text.translatable("reauth.retry", profile.getValue(ProfileConstants.NAME, - "Steve")); + "Steve")); } else { retryText = Text.translatable("reauth.retry.disabled"); } ButtonWidget retryButton = new ButtonWidget(menu.x, menu.y + 25, 200, 20, retryText, - b -> ReconnectHelper - .retryLogin(profile)); + button -> ReconnectHelper + .retryLogin(profile)); if (profile == null || !ReconnectHelper.hasConnectionInfo()) { retryButton.active = false; } @@ -89,15 +90,15 @@ public static void afterRender(Screen screen, MatrixStack matrices, int mouseX, Session session = client.getSession(); SessionStatus state = SessionChecker.getSessionStatus(session.getAccessToken(), session.getUuid()); Screens.getTextRenderer(screen).drawWithShadow(matrices, I18n.translate(state.getTranslationKey()), - 110, 10, - 0xFFFFFFFF); + 110, 10, + 0xFFFFFFFF); } } // TODO: add refresh on clicking status public static void beforeInit(MinecraftClient client, Screen screen, int scaledWidth, int scaledHeight) { if (screen instanceof MultiplayerScreen && MinecraftClient.getInstance().currentScreen instanceof - MultiplayerScreen && Screen.hasShiftDown()) { + MultiplayerScreen && Screen.hasShiftDown()) { SessionChecker.invalidate(); } } diff --git a/src/main/java/technicianlp/reauth/ReAuth.java b/src/main/java/technicianlp/reauth/ReAuth.java index c008267..1e1713e 100644 --- a/src/main/java/technicianlp/reauth/ReAuth.java +++ b/src/main/java/technicianlp/reauth/ReAuth.java @@ -24,8 +24,8 @@ public final class ReAuth implements ClientModInitializer { public static final VersionChecker versionCheck = new VersionChecker(); public static final Config config = new Config(); public static ProfileList profiles; - public static BiFunction i18n = I18n::translate; - public static ModContainer container = FabricLoader.getInstance().getModContainer("reauth").orElse(null); + public static final BiFunction i18n = I18n::translate; + public static final ModContainer container = FabricLoader.getInstance().getModContainer("reauth").orElse(null); @Override public void onInitializeClient() { @@ -40,12 +40,15 @@ private static final class ReAuthThreadFactory implements ThreadFactory { private final ThreadGroup group = new ThreadGroup("ReAuth"); @Override - public Thread newThread(@NotNull Runnable runnable) { + public Thread newThread( + @SuppressWarnings("ParameterNameDiffersFromOverriddenParameter") @NotNull Runnable runnable) { Thread t = new Thread(this.group, runnable, "ReAuth-" + this.threadNumber.getAndIncrement()); - if (t.isDaemon()) + if (t.isDaemon()) { t.setDaemon(false); - if (t.getPriority() != Thread.NORM_PRIORITY) + } + if (t.getPriority() != Thread.NORM_PRIORITY) { t.setPriority(Thread.NORM_PRIORITY); + } return t; } } diff --git a/src/main/java/technicianlp/reauth/ReconnectHelper.java b/src/main/java/technicianlp/reauth/ReconnectHelper.java index 7fb3a13..91c2ba2 100644 --- a/src/main/java/technicianlp/reauth/ReconnectHelper.java +++ b/src/main/java/technicianlp/reauth/ReconnectHelper.java @@ -18,13 +18,14 @@ import java.net.InetSocketAddress; import java.net.SocketAddress; -public final class ReconnectHelper { +public enum ReconnectHelper { + ; private static final Field managerField = ReflectionUtils.findObfuscatedField(ConnectScreen.class, "f_95684_", - "connection"); + "connection"); private static final Field previousField = ReflectionUtils.findObfuscatedField(ConnectScreen.class, "f_95686_", - "parent"); - private static ConnectScreen screen; + "parent"); + private static ConnectScreen connectScreen; /** * Extract the translationKey from the supplied {@link Text} @@ -34,26 +35,26 @@ public final class ReconnectHelper { public static String getTranslationKey(Object component, boolean nested) { if (component instanceof MutableText mutableText) { TextContent contents = mutableText.getContent(); - if (contents instanceof TranslatableTextContent translatableTextContent) { + if (contents instanceof TranslatableTextContent translatable) { if (nested) { - Object[] args = translatableTextContent.getArgs(); + Object[] args = translatable.getArgs(); if (args.length >= 1) { return getTranslationKey(args[0], false); } } else { - return translatableTextContent.getKey(); + return translatable.getKey(); } } } return ""; } - public static void setConnectScreen(ConnectScreen screen) { - ReconnectHelper.screen = screen; + public static void setConnectScreen(ConnectScreen connectScreen) { + ReconnectHelper.connectScreen = connectScreen; } public static boolean hasConnectionInfo() { - return screen != null; + return connectScreen != null; } public static void retryLogin(Profile profile) { @@ -62,13 +63,13 @@ public static void retryLogin(Profile profile) { } private static void connect() { - if (screen != null) { - SocketAddress add = ReflectionUtils.getField(managerField, screen).getAddress(); + if (connectScreen != null) { + SocketAddress add = ReflectionUtils.getField(managerField, connectScreen).getAddress(); if (add instanceof InetSocketAddress address) { MinecraftClient minecraft = MinecraftClient.getInstance(); ServerAddress server = new ServerAddress(address.getHostString(), address.getPort()); - ConnectScreen.connect(ReflectionUtils.getField(previousField, screen), minecraft, server, - minecraft.getCurrentServerEntry()); + ConnectScreen.connect(ReflectionUtils.getField(previousField, connectScreen), minecraft, server, + minecraft.getCurrentServerEntry()); } } } diff --git a/src/main/java/technicianlp/reauth/VersionChecker.java b/src/main/java/technicianlp/reauth/VersionChecker.java index 5596e43..56d08ab 100644 --- a/src/main/java/technicianlp/reauth/VersionChecker.java +++ b/src/main/java/technicianlp/reauth/VersionChecker.java @@ -12,6 +12,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import java.util.regex.Pattern; public final class VersionChecker implements Runnable { @@ -19,21 +20,22 @@ public final class VersionChecker implements Runnable { private static final String JSON_URL = "https://github.com/NgoKimPhu/ReAuth/raw/master/update.json"; private static final Type mapType = new TypeToken>() { }.getType(); + private static final Pattern PATTERN_VERSION_DELIMS = Pattern.compile("[.-]"); private Status status = Status.UNKNOWN; - private String changes = null; + private String changes; private final Gson gson; public VersionChecker() { - gson = new GsonBuilder() - .registerTypeAdapter(VersionJson.class, new VersionJsonDeserializer()) - .create(); + this.gson = new GsonBuilder() + .registerTypeAdapter(VersionJson.class, new VersionJsonDeserializer()) + .create(); } public void runVersionCheck() { - status = Status.UNKNOWN; - changes = null; + this.status = Status.UNKNOWN; + this.changes = null; new Thread(this, "ReAuth Version Check").start(); } @@ -46,7 +48,7 @@ public void run() { String data = IOUtils.toString(inputstream, StandardCharsets.UTF_8); inputstream.close(); - VersionJson json = gson.fromJson(data, VersionJson.class); + VersionJson json = this.gson.fromJson(data, VersionJson.class); String latest = json.versions.get(MC_VERSION + "-recommended"); String current = ReAuth.container.getMetadata().getVersion().getFriendlyString(); @@ -55,9 +57,9 @@ public void run() { if (latest != null) { int latestVer = versionToInt(latest); if (currentId < latestVer) { - status = Status.OUTDATED; + this.status = Status.OUTDATED; } else { - status = Status.OK; + this.status = Status.OK; } } @@ -66,48 +68,55 @@ public void run() { int versionId = versionToInt(entry.getKey()); if (versionId > currentLatest) { currentLatest = versionId; - changes = entry.getValue(); + this.changes = entry.getValue(); } } ReAuth.log.info("Version check complete"); } catch (Exception e) { ReAuth.log.warn("Failed to process update information", e); - status = Status.FAILED; + this.status = Status.FAILED; } } public Status getStatus() { - return status; + return this.status; } public String getChanges() { - return changes; + return this.changes; } private static int versionToInt(String version) { - return Arrays.stream(version.split("[.-]")).limit(3) - .mapToInt(Integer::parseInt).reduce(0, (ver, i) -> (ver << 8) | i); + return Arrays.stream(PATTERN_VERSION_DELIMS.split(version)).limit(3) + .mapToInt(Integer::parseInt).reduce(0, (ver, i) -> (ver << 8) | i); } - private static class VersionJson { - Map versions = new HashMap<>(); - Map changelog = new HashMap<>(); + private static final class VersionJson { + final Map versions = new HashMap<>(); + final Map changelog = new HashMap<>(); - public VersionJson(Map versions, Map changelog) { - if (versions != null) + private VersionJson(Map versions, Map changelog) { + if (versions != null) { this.versions.putAll(versions); - if (changelog != null) + } + if (changelog != null) { this.changelog.putAll(changelog); + } } } private static class VersionJsonDeserializer implements JsonDeserializer { @Override - public VersionJson deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - if (json.isJsonObject()) { - JsonObject object = (JsonObject) json; - Map versions = context.deserialize(object.get("promos-fabric"), mapType); - Map changelog = context.deserialize(object.get(MC_VERSION + "-fabric"), mapType); + public VersionJson deserialize(JsonElement jsonElement, Type type, + @SuppressWarnings("ParameterNameDiffersFromOverriddenParameter") + JsonDeserializationContext context) throws + JsonParseException { + if (jsonElement.isJsonObject()) { + JsonObject object = (JsonObject) jsonElement; + Map versions = + context.deserialize(object.get("promos-fabric"), mapType); + Map changelog = + context.deserialize(object.get(MC_VERSION + "-fabric"), mapType); return new VersionJson(versions, changelog); } return null; diff --git a/src/main/java/technicianlp/reauth/configuration/Config.java b/src/main/java/technicianlp/reauth/configuration/Config.java index 6621f7c..b4cfde5 100644 --- a/src/main/java/technicianlp/reauth/configuration/Config.java +++ b/src/main/java/technicianlp/reauth/configuration/Config.java @@ -1,7 +1,7 @@ package technicianlp.reauth.configuration; -import com.electronwill.nightconfig.core.CommentedConfig; import com.electronwill.nightconfig.core.file.CommentedFileConfig; +import com.electronwill.nightconfig.core.file.FileConfig; import com.electronwill.nightconfig.toml.TomlFormat; import net.fabricmc.loader.api.FabricLoader; import technicianlp.reauth.ReAuth; @@ -11,36 +11,40 @@ public final class Config { public static final String CONFIG_NAME = "reauth"; + public static final String VERSION_PATH = "version"; + private final CommentedFileConfig config; private final ProfileList profileList; public Config() { this.config = CommentedFileConfig.builder( - FabricLoader.getInstance().getConfigDir().resolve(CONFIG_NAME + ".toml"), - TomlFormat.instance()).autosave().build(); - config.load(); - Crypto.updateConfigPath(this.getPath(config)); - this.profileList = new ProfileList(this, config); + FabricLoader.getInstance().getConfigDir().resolve(CONFIG_NAME + ".toml"), + TomlFormat.instance()).autosave().build(); + this.config.load(); + Crypto.updateConfigPath(getPath(this.config)); + this.config.set(VERSION_PATH, 3); + this.config.setComment(VERSION_PATH, "Version Number of the Configuration File"); + this.profileList = new ProfileList(this, this.config); } - public final void save() { + public void save() { this.config.save(); } - public final ProfileList getProfileList() { + public ProfileList getProfileList() { return this.profileList; } /** - * Get the Absolute path of the Config with symlinks resolved. - * Fall back to "local" path if that lookup fails (somehow) + * Get the Absolute path of the Config with symlinks resolved. Fall back to "local" path if that lookup fails + * (somehow) */ - private String getPath(CommentedConfig config) { + private static String getPath(FileConfig config) { try { - return ((CommentedFileConfig) config).getNioPath().toRealPath().toString(); + return config.getNioPath().toRealPath().toString(); } catch (IOException e) { ReAuth.log.error("Could not resolve real path", e); - return ((CommentedFileConfig) config).getNioPath().toString(); + return config.getNioPath().toString(); } } } diff --git a/src/main/java/technicianlp/reauth/configuration/Profile.java b/src/main/java/technicianlp/reauth/configuration/Profile.java index a309765..9ba600e 100644 --- a/src/main/java/technicianlp/reauth/configuration/Profile.java +++ b/src/main/java/technicianlp/reauth/configuration/Profile.java @@ -12,19 +12,19 @@ public final class Profile { this.config = config; } - public final String getValue(String key) { + public String getValue(String key) { return this.config.get(key); } - public final String getValue(String key, String defaultValue) { + public String getValue(String key, String defaultValue) { return this.config.getOrElse(key, defaultValue); } - public final CompletableFuture get(String key) { + public CompletableFuture get(String key) { return CompletableFuture.completedFuture(this.getValue(key)); } - final CommentedConfig getConfig() { + CommentedConfig getConfig() { return this.config; } } diff --git a/src/main/java/technicianlp/reauth/configuration/ProfileList.java b/src/main/java/technicianlp/reauth/configuration/ProfileList.java index 8eb098e..0611486 100644 --- a/src/main/java/technicianlp/reauth/configuration/ProfileList.java +++ b/src/main/java/technicianlp/reauth/configuration/ProfileList.java @@ -17,12 +17,13 @@ public final class ProfileList { this.configuration = configuration; this.configSupplier = config::createSubConfig; this.profilesProperty = config.getOrElse(PROFILES_PATH, - () -> config.set(PROFILES_PATH, new ArrayList<>())); - this.correctProfiles(this.profilesProperty); + () -> config.set(PROFILES_PATH, new ArrayList<>())); + config.setComment(PROFILES_PATH, "Saved Profiles. Check Documentation for Info & Syntax"); + correctProfiles(this.profilesProperty); this.saveProfiles(); } - public final void storeProfile(Profile profile) { + public void storeProfile(Profile profile) { List list = this.profilesProperty; if (list.isEmpty()) { list.add(profile.getConfig()); @@ -32,21 +33,21 @@ public final void storeProfile(Profile profile) { this.saveProfiles(); } - public final Profile getProfile() { + public Profile getProfile() { List list = this.profilesProperty; if (list.isEmpty()) { return null; } CommentedConfig config = list.get(0); String profileType = config.getOrElse(ProfileConstants.PROFILE_TYPE, ProfileConstants.PROFILE_TYPE_NONE); - if (!ProfileConstants.PROFILE_TYPE_NONE.equals(profileType)) { - return new Profile(config); - } else { + if (ProfileConstants.PROFILE_TYPE_NONE.equals(profileType)) { return null; + } else { + return new Profile(config); } } - final Profile createProfile(Map data) { + Profile createProfile(Map data) { Map orderedData = new TreeMap<>(ProfileConstants::compareProfileKeys); orderedData.putAll(data); @@ -56,7 +57,7 @@ final Profile createProfile(Map data) { return new Profile(config); } - private void correctProfiles(List profileList) { + private static void correctProfiles(Collection profileList) { for (CommentedConfig profile : profileList) { Iterator iterator; for (iterator = profile.entrySet().iterator(); iterator.hasNext(); ) { @@ -73,8 +74,7 @@ private void correctProfiles(List profileList) { } /** - * Save the list of Profiles to config - * Add a dummy profile if the list is empty + * Save the list of Profiles to config Add a dummy profile if the list is empty */ private void saveProfiles() { List list = this.profilesProperty; @@ -90,20 +90,4 @@ private CommentedConfig createPlaceholderConfig() { return config; } - - private List createDefaultProfileList() { - List list = new ArrayList<>(); - list.add(this.createPlaceholderConfig()); - return list; - } - - /** - * checks whether the given Object is a {@link List} and all it's elements are an instance of - * {@link CommentedConfig} - * This effectively checks whether the provided {@link Object} is an Array of Tables as described by the TOML - * specification - */ - private boolean validateProfileList(Object el) { - return el instanceof List && ((List) el).stream().allMatch(CommentedConfig.class::isInstance); - } } diff --git a/src/main/java/technicianlp/reauth/gui/AbstractScreen.java b/src/main/java/technicianlp/reauth/gui/AbstractScreen.java index c1ba6b3..df6adb1 100644 --- a/src/main/java/technicianlp/reauth/gui/AbstractScreen.java +++ b/src/main/java/technicianlp/reauth/gui/AbstractScreen.java @@ -3,7 +3,6 @@ import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.gui.widget.ButtonWidget; -import net.minecraft.client.resource.language.I18n; import net.minecraft.client.util.math.MatrixStack; import net.minecraft.text.Text; @@ -19,14 +18,12 @@ abstract class AbstractScreen extends Screen { private final CompletableFuture closed = new CompletableFuture<>(); - private final String title; - protected int baseX; protected int centerX; protected int baseY; protected int centerY; - protected int screenWidth = 300; - protected int screenHeight = 175; + protected static final int screenWidth = 300; + protected static final int screenHeight = 175; AbstractScreen(String title) { this(title, MinecraftClient.getInstance().currentScreen); @@ -34,7 +31,6 @@ abstract class AbstractScreen extends Screen { AbstractScreen(String title, Screen parent) { super(Text.translatable(title)); - this.title = title; this.parent = parent; } @@ -44,17 +40,17 @@ public void init() { Objects.requireNonNull(this.client).keyboard.setRepeatEvents(true); this.centerX = this.width / 2; - this.baseX = this.centerX - this.screenWidth / 2; + this.baseX = this.centerX - screenWidth / 2; this.centerY = this.height / 2; - this.baseY = this.centerY - this.screenHeight / 2; + this.baseY = this.centerY - screenHeight / 2; - ButtonWidget cancel = new ButtonWidget(this.centerX + this.screenWidth / 2 - 22, this.baseY + 2, 20, 20, - Text.translatable("reauth.gui.close"), (b) -> this.close()); + ButtonWidget cancel = new ButtonWidget(this.centerX + screenWidth / 2 - 22, this.baseY + 2, 20, 20, + Text.translatable("reauth.gui.close"), button -> this.close()); this.addDrawableChild(cancel); } @Override - public void render(MatrixStack matrixStack, int mouseX, int mouseY, float partialTicks) { + public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) { if (this.closed.isDone()) { try { this.requestClose(this.closed.get()); @@ -64,11 +60,10 @@ public void render(MatrixStack matrixStack, int mouseX, int mouseY, float partia return; } - this.renderBackground(matrixStack); - super.render(matrixStack, mouseX, mouseY, partialTicks); + this.renderBackground(matrices); + super.render(matrices, mouseX, mouseY, delta); - AbstractScreen.drawCenteredText(matrixStack, this.textRenderer, I18n.translate(this.title), - this.centerX, this.baseY + 8, 0xFFFFFF); + drawCenteredText(matrices, this.textRenderer, this.title, this.centerX, this.baseY + 8, 0xFFFFFF); } protected final void transitionScreen(Screen newScreen) { @@ -87,7 +82,7 @@ protected void requestClose(boolean completely) { parent = abstractScreen.parent; } } - transitionScreen(parent); + this.transitionScreen(parent); } /** diff --git a/src/main/java/technicianlp/reauth/gui/FlowScreen.java b/src/main/java/technicianlp/reauth/gui/FlowScreen.java index 9c4aac3..c8ce57a 100644 --- a/src/main/java/technicianlp/reauth/gui/FlowScreen.java +++ b/src/main/java/technicianlp/reauth/gui/FlowScreen.java @@ -18,10 +18,14 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.function.BiFunction; +import java.util.regex.Pattern; public final class FlowScreen extends AbstractScreen implements FlowCallback { - public static F open(BiFunction flowConstructor, P param) { + private static final String[] EMPTY_STRINGS_ARRAY = new String[0]; + private static final Pattern PATTERN_LINE_BREAK = Pattern.compile("\\R"); + + public static F open(BiFunction flowConstructor, P param) { FlowScreen screen = new FlowScreen(); F flow = flowConstructor.apply(param, screen); screen.flow = flow; @@ -31,7 +35,7 @@ public static F open(BiFunction flowCons private Flow flow; private FlowStage stage = FlowStage.INITIAL; - private String[] formatArgs = new String[0]; + private String[] formatArgs = EMPTY_STRINGS_ARRAY; public FlowScreen() { super("reauth.gui.title.flow"); @@ -44,26 +48,26 @@ public void init() { int buttonWidth = 196; int buttonWidthH = buttonWidth / 2; - this.formatArgs = new String[0]; + this.formatArgs = EMPTY_STRINGS_ARRAY; if (this.stage == FlowStage.MS_AWAIT_AUTH_CODE && this.flow instanceof AuthorizationCodeFlow) { try { URL url = new URL(((AuthorizationCodeFlow) this.flow).getLoginUrl()); this.addDrawableChild(new ButtonWidget(this.centerX - buttonWidthH, - this.baseY + this.screenHeight - 42, buttonWidth, 20, Text.translatable("reauth.msauth.button" + - ".browser"), b -> Util.getOperatingSystem().open(url))); + this.baseY + screenHeight - 42, buttonWidth, 20, Text.translatable("reauth.msauth.button" + + ".browser"), button -> Util.getOperatingSystem().open(url))); } catch (MalformedURLException e) { ReAuth.log.error("Browser button failed", e); } - } else if (this.stage == FlowStage.MS_POLL_DEVICE_CODE && this.flow instanceof DeviceCodeFlow) { + } else if (this.stage == FlowStage.MS_POLL_DEVICE_CODE && this.flow instanceof DeviceCodeFlow flow) { try { - DeviceCodeFlow flow = (DeviceCodeFlow) this.flow; if (CompletableFuture.allOf(flow.getLoginUrl(), flow.getCode()).isDone()) { String urlString = flow.getLoginUrl().join(); String code = flow.getCode().join(); URL url = new URL(urlString); this.addDrawableChild(new ButtonWidget(this.centerX - buttonWidthH, - this.baseY + this.screenHeight - 42, buttonWidth, 20, Text.translatable("reauth.msauth" + - ".button.browser"), b -> Util.getOperatingSystem().open(url))); + this.baseY + screenHeight - 42, buttonWidth, 20, + Text.translatable("reauth.msauth.button.browser"), + button -> Util.getOperatingSystem().open(url))); this.formatArgs = new String[]{urlString, code}; } } catch (MalformedURLException e) { @@ -73,11 +77,11 @@ public void init() { } @Override - public void render(MatrixStack matrixStack, int mouseX, int mouseY, float partialTicks) { - super.render(matrixStack, mouseX, mouseY, partialTicks); + public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) { + super.render(matrices, mouseX, mouseY, delta); String text = I18n.translate(this.stage.getRawName(), (Object[]) this.formatArgs); - String[] lines = text.split("\\R"); + String[] lines = PATTERN_LINE_BREAK.split(text); int height = lines.length * 9; for (String s : lines) { if (s.startsWith("$")) { @@ -89,15 +93,15 @@ public void render(MatrixStack matrixStack, int mouseX, int mouseY, float partia for (String line : lines) { if (line.startsWith("$")) { line = line.substring(1); - matrixStack.push(); - matrixStack.scale(2, 2, 1); - this.textRenderer.drawWithShadow(matrixStack, line, - (float) (this.centerX - this.textRenderer.getWidth(line)) / 2, (float) y / 2, 0xFFFFFFFF); + matrices.push(); + matrices.scale(2, 2, 1); + this.textRenderer.drawWithShadow(matrices, line, + (this.centerX - this.textRenderer.getWidth(line)) / 2f, y / 2f, 0xFFFFFFFF); y += 18; - matrixStack.pop(); + matrices.pop(); } else { - this.textRenderer.drawWithShadow(matrixStack, line, - (float) (this.centerX - this.textRenderer.getWidth(line) / 2), (float) y, 0xFFFFFFFF); + this.textRenderer.drawWithShadow(matrices, line, + this.centerX - this.textRenderer.getWidth(line) / 2f, y, 0xFFFFFFFF); y += 9; } } diff --git a/src/main/java/technicianlp/reauth/gui/MainScreen.java b/src/main/java/technicianlp/reauth/gui/MainScreen.java index 9184041..cfedc84 100644 --- a/src/main/java/technicianlp/reauth/gui/MainScreen.java +++ b/src/main/java/technicianlp/reauth/gui/MainScreen.java @@ -13,10 +13,10 @@ public final class MainScreen extends AbstractScreen { - private String message = null; + private String message; - public MainScreen(Screen screen) { - super("reauth.gui.title.main", screen); + public MainScreen(Screen parent) { + super("reauth.gui.title.main", parent); } @Override @@ -27,35 +27,35 @@ public void init() { int y = this.centerY - 55; SaveButton.ITooltip saveButtonTooltip = - (button, matrixStack, mouseX, mouseY) -> this.renderOrderedTooltip(matrixStack, - this.textRenderer.wrapLines(Text.translatable("reauth.gui.button.save.tooltip"), 250), - mouseX, mouseY); + (button, matrixStack, mouseX, mouseY) -> this.renderOrderedTooltip(matrixStack, + this.textRenderer.wrapLines(Text.translatable("reauth.gui.button.save.tooltip"), 250), + mouseX, mouseY); SaveButton saveButton = new SaveButton(this.centerX - buttonWidthH, y + 70, - Text.translatable("reauth.gui.button.save"), saveButtonTooltip); + Text.translatable("reauth.gui.button.save"), saveButtonTooltip); this.addDrawableChild(saveButton); Profile profile = ReAuth.profiles.getProfile(); if (profile != null) { Text text = Text.translatable("reauth.gui.profile", profile.getValue(ProfileConstants.NAME, "Steve")); this.addDrawableChild(new ButtonWidget(this.centerX - buttonWidthH, y + 10, BUTTON_WIDTH, 20, text, - (b) -> FlowScreen.open(Flows::loginWithProfile, profile))); + button -> FlowScreen.open(Flows::loginWithProfile, profile))); } else { ButtonWidget profileButton = new ButtonWidget(this.centerX - buttonWidthH, y + 10, BUTTON_WIDTH, 20, - Text.translatable("reauth.gui.noProfile"), (b) -> { + Text.translatable("reauth.gui.noProfile"), button -> { }); profileButton.active = false; this.addDrawableChild(profileButton); } this.addDrawableChild(new ButtonWidget(this.centerX - buttonWidthH, y + 45, buttonWidthH - 1, 20, - Text.translatable("reauth.gui.button.authcode"), (b) -> FlowScreen.open(Flows::loginWithAuthCode, - saveButton.isChecked()))); + Text.translatable("reauth.gui.button.authcode"), button -> FlowScreen.open(Flows::loginWithAuthCode, + saveButton.isChecked()))); this.addDrawableChild(new ButtonWidget(this.centerX + 1, y + 45, buttonWidthH - 1, 20, Text.translatable( - "reauth.gui.button.devicecode"), (b) -> FlowScreen.open(Flows::loginWithDeviceCode, - saveButton.isChecked()))); + "reauth.gui.button.devicecode"), button -> FlowScreen.open(Flows::loginWithDeviceCode, + saveButton.isChecked()))); this.addDrawableChild(new ButtonWidget(this.centerX - buttonWidthH, y + 105, BUTTON_WIDTH, 20, - Text.translatable("reauth.gui.button.offline"), - (b) -> this.transitionScreen(new OfflineLoginScreen()))); + Text.translatable("reauth.gui.button.offline"), + button -> this.transitionScreen(new OfflineLoginScreen()))); VersionChecker.Status result = ReAuth.versionCheck.getStatus(); @@ -69,19 +69,19 @@ public void init() { } @Override - public void render(MatrixStack matrixStack, int mouseX, int mouseY, float partialTicks) { - super.render(matrixStack, mouseX, mouseY, partialTicks); + public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) { + super.render(matrices, mouseX, mouseY, delta); int x = this.centerX - BUTTON_WIDTH / 2; - this.textRenderer.drawWithShadow(matrixStack, I18n.translate("reauth.gui.text.profile"), - x, this.centerY - 55, 0xA0A0A0); - this.textRenderer.drawWithShadow(matrixStack, I18n.translate("reauth.gui.text.microsoft"), - x, this.centerY - 20, 0xA0A0A0); - this.textRenderer.drawWithShadow(matrixStack, I18n.translate("reauth.gui.text.offline"), - x, this.centerY + 40, 0xA0A0A0); + this.textRenderer.drawWithShadow(matrices, I18n.translate("reauth.gui.text.profile"), + x, this.centerY - 55, 0xA0A0A0); + this.textRenderer.drawWithShadow(matrices, I18n.translate("reauth.gui.text.microsoft"), + x, this.centerY - 20, 0xA0A0A0); + this.textRenderer.drawWithShadow(matrices, I18n.translate("reauth.gui.text.offline"), + x, this.centerY + 40, 0xA0A0A0); if (this.message != null) { - this.textRenderer.drawWithShadow(matrixStack, this.message, x, this.baseY + 20, 0xFFFFFFFF); + this.textRenderer.drawWithShadow(matrices, this.message, x, this.baseY + 20, 0xFFFFFFFF); } } diff --git a/src/main/java/technicianlp/reauth/gui/OfflineLoginScreen.java b/src/main/java/technicianlp/reauth/gui/OfflineLoginScreen.java index 8acea50..87b5f67 100644 --- a/src/main/java/technicianlp/reauth/gui/OfflineLoginScreen.java +++ b/src/main/java/technicianlp/reauth/gui/OfflineLoginScreen.java @@ -25,24 +25,24 @@ public void init() { super.init(); this.username = new TextFieldWidget(this.textRenderer, this.centerX - BUTTON_WIDTH / 2, this.centerY - 5, - BUTTON_WIDTH, 20, Text.translatable("reauth.gui.auth.username")); + BUTTON_WIDTH, 20, Text.translatable("reauth.gui.auth.username")); this.username.setMaxLength(16); this.username.setTextFieldFocused(true); this.username.setText(Objects.requireNonNull(this.client).getSession().getUsername()); this.addSelectableChild(this.username); this.setInitialFocus(this.username); - this.confirm = new ButtonWidget(this.centerX - BUTTON_WIDTH / 2, this.baseY + this.screenHeight - 42, - BUTTON_WIDTH, 20, Text.translatable("reauth.gui.button.username"), (b) -> this.performUsernameChange()); + this.confirm = new ButtonWidget(this.centerX - BUTTON_WIDTH / 2, this.baseY + screenHeight - 42, + BUTTON_WIDTH, 20, Text.translatable("reauth.gui.button.username"), button -> this.performUsernameChange()); this.addDrawableChild(this.confirm); } @Override - public void render(MatrixStack matrixStack, int mouseX, int mouseY, float partialTicks) { - super.render(matrixStack, mouseX, mouseY, partialTicks); - this.username.render(matrixStack, mouseX, mouseY, partialTicks); - this.textRenderer.drawWithShadow(matrixStack, I18n.translate("reauth.gui.auth.username"), - this.centerX - (BUTTON_WIDTH / 2f), this.centerY - 15, 0xFFFFFFFF); + public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) { + super.render(matrices, mouseX, mouseY, delta); + this.username.render(matrices, mouseX, mouseY, delta); + this.textRenderer.drawWithShadow(matrices, I18n.translate("reauth.gui.auth.username"), + this.centerX - (BUTTON_WIDTH / 2f), this.centerY - 15, 0xFFFFFFFF); } @Override @@ -52,7 +52,7 @@ public void tick() { } @Override - public boolean keyPressed(int keyCode, int p_keyPressed_2_, int p_keyPressed_3_) { + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { if (keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_KP_ENTER) { Element focus = this.getFocused(); if (focus == this.username) { @@ -60,12 +60,11 @@ public boolean keyPressed(int keyCode, int p_keyPressed_2_, int p_keyPressed_3_) return true; } } - return super.keyPressed(keyCode, p_keyPressed_2_, p_keyPressed_3_); + return super.keyPressed(keyCode, scanCode, modifiers); } /** - * Calls the to do the Login and handles Errors - * Closes the Screen if successful + * Calls the to do the Login and handles Errors Closes the Screen if successful */ private void performUsernameChange() { if (SessionHelper.isValidOfflineUsername(this.username.getText())) { diff --git a/src/main/java/technicianlp/reauth/gui/SaveButton.java b/src/main/java/technicianlp/reauth/gui/SaveButton.java index 20d20ce..bab2001 100644 --- a/src/main/java/technicianlp/reauth/gui/SaveButton.java +++ b/src/main/java/technicianlp/reauth/gui/SaveButton.java @@ -8,16 +8,16 @@ public final class SaveButton extends CheckboxWidget { private final ITooltip tooltip; - public SaveButton(int x, int y, Text title, ITooltip tooltip) { - super(x, y, 20, 20, title, false); + public SaveButton(int x, int y, Text message, ITooltip tooltip) { + super(x, y, 20, 20, message, false); this.tooltip = tooltip; } @Override - public void renderButton(MatrixStack matrixStack, int mouseX, int mouseY, float partialTicks) { - super.renderButton(matrixStack, mouseX, mouseY, partialTicks); + public void renderButton(MatrixStack matrices, int mouseX, int mouseY, float delta) { + super.renderButton(matrices, mouseX, mouseY, delta); if (this.isHovered()) { - this.tooltip.onTooltip(this, matrixStack, mouseX, mouseY); + this.tooltip.onTooltip(this, matrices, mouseX, mouseY); } } diff --git a/src/main/java/technicianlp/reauth/session/SessionHelper.java b/src/main/java/technicianlp/reauth/session/SessionHelper.java index 7103afd..8fb1d9c 100644 --- a/src/main/java/technicianlp/reauth/session/SessionHelper.java +++ b/src/main/java/technicianlp/reauth/session/SessionHelper.java @@ -19,16 +19,17 @@ import java.util.UUID; import java.util.regex.Pattern; -public final class SessionHelper { +public enum SessionHelper { + ; private static final Field sessionField = ReflectionUtils.findObfuscatedField(MinecraftClient.class, "f_90998_", - "session"); + "session"); private static final Field userApiServiceField = ReflectionUtils.findObfuscatedField(MinecraftClient.class, - "f_193584_", "userApiService"); + "f_193584_", "userApiService"); private static final Field socialManagerField = ReflectionUtils.findObfuscatedField(MinecraftClient.class, - "f_91006_", "socialInteractionsManager"); + "f_91006_", "socialInteractionsManager"); private static final Field splashesSessionField = - ReflectionUtils.findObfuscatedField(SplashTextResourceSupplier.class, "f_118863_", "session"); + ReflectionUtils.findObfuscatedField(SplashTextResourceSupplier.class, "f_118863_", "session"); private static final Pattern usernamePattern = Pattern.compile("\\w{2,16}"); @@ -49,19 +50,20 @@ public static void setSession(SessionData data) { /** * Set the Session and update dependant fields - *

    + *

      *
    • Clear ProfileProperties and repopulate them *
    • Recreate {@link UserApiService}, using logic from * {@link MinecraftClient#createUserApiService(YggdrasilAuthenticationService, RunArgs)} *
    • Recreate {@link SocialInteractionsManager} with the new SocialInteractionsService *
    • Update {@link SplashTextResourceSupplier#session} + *
    */ private static void setSession(SessionData data, boolean online) { try { MinecraftClient minecraft = MinecraftClient.getInstance(); - Session session = new Session(data.username, data.uuid, data.accessToken, Optional.empty(), - Optional.empty(), Session.AccountType.byName(data.type)); + Session session = new Session(data.username(), data.uuid(), data.accessToken(), Optional.empty(), + Optional.empty(), Session.AccountType.byName(data.type())); ReflectionUtils.setField(sessionField, minecraft, session); SessionChecker.invalidate(); @@ -78,12 +80,12 @@ private static void setSession(SessionData data, boolean online) { UserApiService userApiService = null; if (online) { YggdrasilMinecraftSessionService sessionService = - (YggdrasilMinecraftSessionService) minecraft.getSessionService(); + (YggdrasilMinecraftSessionService) minecraft.getSessionService(); YggdrasilAuthenticationService authService = sessionService.getAuthenticationService(); try { userApiService = authService.createUserApiService(session.getAccessToken()); - } catch (AuthenticationException authenticationexception) { - ReAuth.log.error("Failed to create UserApiService", authenticationexception); + } catch (AuthenticationException authException) { + ReAuth.log.error("Failed to create UserApiService", authException); } } if (userApiService == null) { diff --git a/src/main/resources/assets/reauth/lang/en_us.json b/src/main/resources/assets/reauth/lang/en_us.json index 9ee815e..52bfdf5 100644 --- a/src/main/resources/assets/reauth/lang/en_us.json +++ b/src/main/resources/assets/reauth/lang/en_us.json @@ -44,12 +44,12 @@ "reauth.msauth.code.fail.server": "Authentication with Microsoft is temporarily unavailable", "reauth.msauth.code.fail.unknown": "Authentication with Microsoft failed", "reauth.msauth.code.retry": "Please click here to try again", - "reauth.msauth.code.error.http200": "Error: This is not an error!?", - "reauth.msauth.code.error.http400": "Error: Authentication failed", - "reauth.msauth.code.error.http404": "Error: This resource does not exist", - "reauth.msauth.code.error.http405": "Error: Incorrect request method", - "reauth.msauth.code.error.http415": "Error: Authentication data has invalid format", - "reauth.msauth.code.error.http501": "Error: Unknown request method", + "reauth.msauth.code.error.http.200": "Error: This is not an error!?", + "reauth.msauth.code.error.http.400": "Error: Authentication failed", + "reauth.msauth.code.error.http.404": "Error: This resource does not exist", + "reauth.msauth.code.error.http.405": "Error: Incorrect request method", + "reauth.msauth.code.error.http.415": "Error: Authentication data has invalid format", + "reauth.msauth.code.error.http.501": "Error: Unknown request method", "reauth.msauth.step.initial": "Starting authentication Process", "reauth.msauth.step.finished": "Login successful", "reauth.msauth.step.profile": "Saving Profile", From aaf653675e4ed8de0a8194a8ca166f7b75d35923 Mon Sep 17 00:00:00 2001 From: NgoKimPhu Date: Sat, 1 Oct 2022 03:12:59 +0700 Subject: [PATCH 5/8] use mixins instead of reflection --- .../reauth/util/ReflectionUtils.java | 16 ------------- .../technicianlp/reauth/EventHandler.java | 11 +++------ .../technicianlp/reauth/ReconnectHelper.java | 17 ++++--------- .../technicianlp/reauth/gui/FlowScreen.java | 3 +-- .../reauth/mixin/ConnectScreenMixin.java | 17 +++++++++++++ .../reauth/mixin/DisconnectedScreenMixin.java | 17 +++++++++++++ .../reauth/mixin/MinecraftClientMixin.java | 24 +++++++++++++++++++ .../SplashTextResourceSupplierMixin.java | 14 +++++++++++ .../reauth/session/SessionHelper.java | 21 +++++----------- src/main/resources/reauth.mixins.json | 8 +++++-- 10 files changed, 92 insertions(+), 56 deletions(-) create mode 100644 src/main/java/technicianlp/reauth/mixin/ConnectScreenMixin.java create mode 100644 src/main/java/technicianlp/reauth/mixin/DisconnectedScreenMixin.java create mode 100644 src/main/java/technicianlp/reauth/mixin/MinecraftClientMixin.java create mode 100644 src/main/java/technicianlp/reauth/mixin/SplashTextResourceSupplierMixin.java diff --git a/src/common/java/technicianlp/reauth/util/ReflectionUtils.java b/src/common/java/technicianlp/reauth/util/ReflectionUtils.java index 71b4c47..5a920d8 100644 --- a/src/common/java/technicianlp/reauth/util/ReflectionUtils.java +++ b/src/common/java/technicianlp/reauth/util/ReflectionUtils.java @@ -20,21 +20,6 @@ public static Field findField(Class clz, String name) { } } - // TODO: use Mutator mixins? - - public static Field findObfuscatedField(Class clz, String obfName, String name) { - try { - return findFieldInternal(clz, obfName); - } catch (NoSuchFieldException suppressed) { - try { - return findFieldInternal(clz, name); - } catch (NoSuchFieldException exception) { - exception.addSuppressed(suppressed); - throw new UncheckedReflectiveOperationException("Unable to find Obfuscated Field: " + name, exception); - } - } - } - public static void unlockFinalField(Field field) { try { Field fieldModifiers = findField(Field.class, "modifiers"); @@ -62,7 +47,6 @@ public static T getField(Field field, Object target) { } public static class UncheckedReflectiveOperationException extends RuntimeException { - public UncheckedReflectiveOperationException(String message, ReflectiveOperationException cause) { super(message, cause); } diff --git a/src/main/java/technicianlp/reauth/EventHandler.java b/src/main/java/technicianlp/reauth/EventHandler.java index 6eb3927..c207355 100644 --- a/src/main/java/technicianlp/reauth/EventHandler.java +++ b/src/main/java/technicianlp/reauth/EventHandler.java @@ -17,19 +17,15 @@ import technicianlp.reauth.configuration.Profile; import technicianlp.reauth.configuration.ProfileConstants; import technicianlp.reauth.gui.MainScreen; +import technicianlp.reauth.mixin.DisconnectedScreenMixin; import technicianlp.reauth.session.SessionChecker; import technicianlp.reauth.session.SessionStatus; -import technicianlp.reauth.util.ReflectionUtils; -import java.lang.reflect.Field; import java.util.List; public enum EventHandler { ; - private static final Field disconnectMessage = ReflectionUtils.findObfuscatedField(DisconnectedScreen.class, - "f_95988_", "reason"); - public static void register() { ScreenEvents.AFTER_INIT.register(EventHandler::afterInit); ScreenEvents.BEFORE_INIT.register(EventHandler::beforeInit); @@ -56,7 +52,7 @@ public static void afterInit(MinecraftClient client, Screen screen, int scaledWi private static void handleDisconnectScreen(Screen screen) { if ("connect.failed".equals(ReconnectHelper.getTranslationKey(screen.getTitle(), false))) { - if (ReconnectHelper.getTranslationKey(ReflectionUtils.getField(disconnectMessage, screen), true) + if (ReconnectHelper.getTranslationKey(((DisconnectedScreenMixin) screen).getReason(), true) .startsWith("disconnect.loginFailed")) { List buttons = Screens.getButtons(screen); ClickableWidget menu = buttons.get(0); @@ -70,8 +66,7 @@ private static void handleDisconnectScreen(Screen screen) { retryText = Text.translatable("reauth.retry.disabled"); } ButtonWidget retryButton = new ButtonWidget(menu.x, menu.y + 25, 200, 20, retryText, - button -> ReconnectHelper - .retryLogin(profile)); + button -> ReconnectHelper.retryLogin(profile)); if (profile == null || !ReconnectHelper.hasConnectionInfo()) { retryButton.active = false; } diff --git a/src/main/java/technicianlp/reauth/ReconnectHelper.java b/src/main/java/technicianlp/reauth/ReconnectHelper.java index 91c2ba2..3f61f3d 100644 --- a/src/main/java/technicianlp/reauth/ReconnectHelper.java +++ b/src/main/java/technicianlp/reauth/ReconnectHelper.java @@ -3,7 +3,6 @@ import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.screen.ConnectScreen; import net.minecraft.client.network.ServerAddress; -import net.minecraft.network.ClientConnection; import net.minecraft.text.MutableText; import net.minecraft.text.Text; import net.minecraft.text.TextContent; @@ -12,19 +11,13 @@ import technicianlp.reauth.authentication.flows.Flows; import technicianlp.reauth.configuration.Profile; import technicianlp.reauth.gui.FlowScreen; -import technicianlp.reauth.util.ReflectionUtils; +import technicianlp.reauth.mixin.ConnectScreenMixin; -import java.lang.reflect.Field; import java.net.InetSocketAddress; -import java.net.SocketAddress; public enum ReconnectHelper { ; - private static final Field managerField = ReflectionUtils.findObfuscatedField(ConnectScreen.class, "f_95684_", - "connection"); - private static final Field previousField = ReflectionUtils.findObfuscatedField(ConnectScreen.class, "f_95686_", - "parent"); private static ConnectScreen connectScreen; /** @@ -63,13 +56,11 @@ public static void retryLogin(Profile profile) { } private static void connect() { - if (connectScreen != null) { - SocketAddress add = ReflectionUtils.getField(managerField, connectScreen).getAddress(); - if (add instanceof InetSocketAddress address) { + if (connectScreen instanceof ConnectScreenMixin screenMixin) { + if (screenMixin.getConnection().getAddress() instanceof InetSocketAddress address) { MinecraftClient minecraft = MinecraftClient.getInstance(); ServerAddress server = new ServerAddress(address.getHostString(), address.getPort()); - ConnectScreen.connect(ReflectionUtils.getField(previousField, connectScreen), minecraft, server, - minecraft.getCurrentServerEntry()); + ConnectScreen.connect(screenMixin.getParent(), minecraft, server, minecraft.getCurrentServerEntry()); } } } diff --git a/src/main/java/technicianlp/reauth/gui/FlowScreen.java b/src/main/java/technicianlp/reauth/gui/FlowScreen.java index c8ce57a..fa95b1f 100644 --- a/src/main/java/technicianlp/reauth/gui/FlowScreen.java +++ b/src/main/java/technicianlp/reauth/gui/FlowScreen.java @@ -100,8 +100,7 @@ public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) { y += 18; matrices.pop(); } else { - this.textRenderer.drawWithShadow(matrices, line, - this.centerX - this.textRenderer.getWidth(line) / 2f, y, 0xFFFFFFFF); + drawCenteredText(matrices, this.textRenderer, line, this.centerX, y, 0xFFFFFFFF); y += 9; } } diff --git a/src/main/java/technicianlp/reauth/mixin/ConnectScreenMixin.java b/src/main/java/technicianlp/reauth/mixin/ConnectScreenMixin.java new file mode 100644 index 0000000..f69e73d --- /dev/null +++ b/src/main/java/technicianlp/reauth/mixin/ConnectScreenMixin.java @@ -0,0 +1,17 @@ +package technicianlp.reauth.mixin; + +import net.minecraft.client.gui.screen.ConnectScreen; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.network.ClientConnection; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(ConnectScreen.class) +public interface ConnectScreenMixin { + + @Accessor + ClientConnection getConnection(); + + @Accessor + Screen getParent(); +} diff --git a/src/main/java/technicianlp/reauth/mixin/DisconnectedScreenMixin.java b/src/main/java/technicianlp/reauth/mixin/DisconnectedScreenMixin.java new file mode 100644 index 0000000..05012c3 --- /dev/null +++ b/src/main/java/technicianlp/reauth/mixin/DisconnectedScreenMixin.java @@ -0,0 +1,17 @@ +package technicianlp.reauth.mixin; + +import net.minecraft.client.gui.screen.DisconnectedScreen; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.text.Text; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(DisconnectedScreen.class) +public abstract class DisconnectedScreenMixin extends Screen { + protected DisconnectedScreenMixin(Text title) { + super(title); + } + + @Accessor + public abstract Text getReason(); +} diff --git a/src/main/java/technicianlp/reauth/mixin/MinecraftClientMixin.java b/src/main/java/technicianlp/reauth/mixin/MinecraftClientMixin.java new file mode 100644 index 0000000..5ffbf93 --- /dev/null +++ b/src/main/java/technicianlp/reauth/mixin/MinecraftClientMixin.java @@ -0,0 +1,24 @@ +package technicianlp.reauth.mixin; + +import com.mojang.authlib.minecraft.UserApiService; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.SocialInteractionsManager; +import net.minecraft.client.util.Session; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(MinecraftClient.class) +public interface MinecraftClientMixin { + @Accessor + @Mutable + void setSession(Session session); + + @Accessor + @Mutable + void setUserApiService(UserApiService userApiService); + + @Accessor + @Mutable + void setSocialInteractionsManager(SocialInteractionsManager manager); +} diff --git a/src/main/java/technicianlp/reauth/mixin/SplashTextResourceSupplierMixin.java b/src/main/java/technicianlp/reauth/mixin/SplashTextResourceSupplierMixin.java new file mode 100644 index 0000000..97bac4b --- /dev/null +++ b/src/main/java/technicianlp/reauth/mixin/SplashTextResourceSupplierMixin.java @@ -0,0 +1,14 @@ +package technicianlp.reauth.mixin; + +import net.minecraft.client.resource.SplashTextResourceSupplier; +import net.minecraft.client.util.Session; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(SplashTextResourceSupplier.class) +public interface SplashTextResourceSupplierMixin { + @Accessor + @Mutable + void setSession(Session session); +} diff --git a/src/main/java/technicianlp/reauth/session/SessionHelper.java b/src/main/java/technicianlp/reauth/session/SessionHelper.java index 8fb1d9c..60e3d5e 100644 --- a/src/main/java/technicianlp/reauth/session/SessionHelper.java +++ b/src/main/java/technicianlp/reauth/session/SessionHelper.java @@ -11,9 +11,9 @@ import net.minecraft.client.util.Session; import technicianlp.reauth.ReAuth; import technicianlp.reauth.authentication.SessionData; -import technicianlp.reauth.util.ReflectionUtils; +import technicianlp.reauth.mixin.MinecraftClientMixin; +import technicianlp.reauth.mixin.SplashTextResourceSupplierMixin; -import java.lang.reflect.Field; import java.nio.charset.StandardCharsets; import java.util.Optional; import java.util.UUID; @@ -22,15 +22,6 @@ public enum SessionHelper { ; - private static final Field sessionField = ReflectionUtils.findObfuscatedField(MinecraftClient.class, "f_90998_", - "session"); - private static final Field userApiServiceField = ReflectionUtils.findObfuscatedField(MinecraftClient.class, - "f_193584_", "userApiService"); - private static final Field socialManagerField = ReflectionUtils.findObfuscatedField(MinecraftClient.class, - "f_91006_", "socialInteractionsManager"); - private static final Field splashesSessionField = - ReflectionUtils.findObfuscatedField(SplashTextResourceSupplier.class, "f_118863_", "session"); - private static final Pattern usernamePattern = Pattern.compile("\\w{2,16}"); /** @@ -65,7 +56,7 @@ private static void setSession(SessionData data, boolean online) { Session session = new Session(data.username(), data.uuid(), data.accessToken(), Optional.empty(), Optional.empty(), Session.AccountType.byName(data.type())); - ReflectionUtils.setField(sessionField, minecraft, session); + ((MinecraftClientMixin) minecraft).setSession(session); SessionChecker.invalidate(); // Update things depending on the Session. @@ -91,14 +82,14 @@ private static void setSession(SessionData data, boolean online) { if (userApiService == null) { userApiService = UserApiService.OFFLINE; } - ReflectionUtils.setField(userApiServiceField, minecraft, userApiService); + ((MinecraftClientMixin) minecraft).setUserApiService(userApiService); // Recreate FilterManager SocialInteractionsManager socialManager = new SocialInteractionsManager(minecraft, userApiService); - ReflectionUtils.setField(socialManagerField, minecraft, socialManager); + ((MinecraftClientMixin) minecraft).setSocialInteractionsManager(socialManager); // Update Splashes session - ReflectionUtils.setField(splashesSessionField, minecraft.getSplashTextLoader(), session); + ((SplashTextResourceSupplierMixin) minecraft.getSplashTextLoader()).setSession(session); } catch (Exception e) { ReAuth.log.error("Failed to update Session", e); } diff --git a/src/main/resources/reauth.mixins.json b/src/main/resources/reauth.mixins.json index 6ea7549..d56cdcb 100644 --- a/src/main/resources/reauth.mixins.json +++ b/src/main/resources/reauth.mixins.json @@ -3,8 +3,12 @@ "minVersion": "0.8", "package": "technicianlp.reauth.mixin", "compatibilityLevel": "JAVA_17", - "mixins": [], - "client": [], + "client": [ + "ConnectScreenMixin", + "DisconnectedScreenMixin", + "MinecraftClientMixin", + "SplashTextResourceSupplierMixin" + ], "injectors": { "defaultRequire": 1 } From c00edd6dd6e27b3ff389a4a5c254669fb574ce07 Mon Sep 17 00:00:00 2001 From: NgoKimPhu Date: Sat, 1 Oct 2022 03:48:49 +0700 Subject: [PATCH 6/8] recheck session state on clicking state text --- .../technicianlp/reauth/EventHandler.java | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/main/java/technicianlp/reauth/EventHandler.java b/src/main/java/technicianlp/reauth/EventHandler.java index c207355..f3fbf03 100644 --- a/src/main/java/technicianlp/reauth/EventHandler.java +++ b/src/main/java/technicianlp/reauth/EventHandler.java @@ -1,8 +1,10 @@ package technicianlp.reauth; import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; +import net.fabricmc.fabric.api.client.screen.v1.ScreenMouseEvents; import net.fabricmc.fabric.api.client.screen.v1.Screens; import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; import net.minecraft.client.gui.screen.ConnectScreen; import net.minecraft.client.gui.screen.DisconnectedScreen; import net.minecraft.client.gui.screen.Screen; @@ -26,6 +28,9 @@ public enum EventHandler { ; + public static final int STATE_X = 110; + public static final int STATE_Y = 10; + public static void register() { ScreenEvents.AFTER_INIT.register(EventHandler::afterInit); ScreenEvents.BEFORE_INIT.register(EventHandler::beforeInit); @@ -37,6 +42,7 @@ public static void afterInit(MinecraftClient client, Screen screen, int scaledWi Screens.getButtons(screen).add(new ButtonWidget(5, 5, 100, 20, Text.translatable("reauth.gui.button"), button -> openAuthenticationScreen(screen))); ScreenEvents.afterRender(screen).register(EventHandler::afterRender); + ScreenMouseEvents.afterMouseClick(screen).register(EventHandler::afterMouseClick); } else if (screen instanceof TitleScreen) { // Support for Custom Main Menu (add button outside of viewport) Screens.getButtons(screen).add(new ButtonWidget(-50, -50, 20, 20, @@ -84,13 +90,26 @@ public static void afterRender(Screen screen, MatrixStack matrices, int mouseX, MinecraftClient client = Screens.getClient(screen); Session session = client.getSession(); SessionStatus state = SessionChecker.getSessionStatus(session.getAccessToken(), session.getUuid()); - Screens.getTextRenderer(screen).drawWithShadow(matrices, I18n.translate(state.getTranslationKey()), - 110, 10, - 0xFFFFFFFF); + String stateText = I18n.translate(state.getTranslationKey()); + + TextRenderer textRenderer = Screens.getTextRenderer(screen); + textRenderer.drawWithShadow(matrices, stateText, STATE_X, STATE_Y, 0xFFFFFFFF); + statusTextWidth = textRenderer.getWidth(stateText); + } + } + + private static int statusTextWidth; + + private static boolean withinTextBox(double x, double y) { + return x >= STATE_X && x < (STATE_X + statusTextWidth) && y >= STATE_Y && y < STATE_Y + 9; + } + + private static void afterMouseClick(Screen screen, double mouseX, double mouseY, int button) { + if (withinTextBox(mouseX, mouseY)) { + SessionChecker.invalidate(); } } - // TODO: add refresh on clicking status public static void beforeInit(MinecraftClient client, Screen screen, int scaledWidth, int scaledHeight) { if (screen instanceof MultiplayerScreen && MinecraftClient.getInstance().currentScreen instanceof MultiplayerScreen && Screen.hasShiftDown()) { From a85c4e86c48b16d5a096184924ee4f8a19e01da6 Mon Sep 17 00:00:00 2001 From: NgoKimPhu Date: Sun, 23 Oct 2022 17:19:36 +0800 Subject: [PATCH 7/8] add support for multiple profiles --- .../http/server/PageWriter.java | 4 +- .../http/server/ResourcesHandler.java | 4 +- .../reauth/mojangfix/CertWorkaround.java | 2 +- .../reauth/certs/amazonrootca1.pem | 0 .../reauth/certs/digicertglobalrootg2.pem | 0 ...crosoftrsarootcertificateauthority2017.pem | 0 .../resources/{resources => }/reauth/icon.png | Bin .../resources/{resources => }/reauth/logo.png | Bin .../{resources => }/reauth/reauth.html | 0 .../technicianlp/reauth/EventHandler.java | 15 ++--- .../reauth/configuration/Profile.java | 10 +++ .../reauth/configuration/ProfileList.java | 39 ++++++----- .../technicianlp/reauth/gui/MainScreen.java | 21 +++--- .../reauth/gui/MultiOptionButton.java | 61 ++++++++++++++++++ .../reauth/mixin/DisconnectedScreenMixin.java | 9 +-- src/main/resources/fabric.mod.json | 1 + 16 files changed, 120 insertions(+), 46 deletions(-) rename src/common/resources/{resources => }/reauth/certs/amazonrootca1.pem (100%) rename src/common/resources/{resources => }/reauth/certs/digicertglobalrootg2.pem (100%) rename src/common/resources/{resources => }/reauth/certs/microsoftrsarootcertificateauthority2017.pem (100%) rename src/common/resources/{resources => }/reauth/icon.png (100%) rename src/common/resources/{resources => }/reauth/logo.png (100%) rename src/common/resources/{resources => }/reauth/reauth.html (100%) create mode 100644 src/main/java/technicianlp/reauth/gui/MultiOptionButton.java diff --git a/src/common/java/technicianlp/reauth/authentication/http/server/PageWriter.java b/src/common/java/technicianlp/reauth/authentication/http/server/PageWriter.java index aaee8c7..2d76e3c 100644 --- a/src/common/java/technicianlp/reauth/authentication/http/server/PageWriter.java +++ b/src/common/java/technicianlp/reauth/authentication/http/server/PageWriter.java @@ -40,7 +40,7 @@ ByteBuffer createHttpErrorResponsePage(HttpStatus error) throws IOException { } private static ByteBuffer createPage(String text1, String text2) throws IOException { - try (InputStream is = AuthenticationCodeServer.class.getResourceAsStream("/resources/reauth/reauth.html")) { + try (InputStream is = AuthenticationCodeServer.class.getResourceAsStream("/reauth/reauth.html")) { if (is != null) { String page = IOUtils.toString(is, StandardCharsets.UTF_8); page = page.replace("$text1", text1).replace("$text2", text2); @@ -48,7 +48,7 @@ private static ByteBuffer createPage(String text1, String text2) throws IOExcept buffer.put(page.getBytes(StandardCharsets.UTF_8)); return buffer; } else { - throw new FileNotFoundException("Resource /resources/reauth/reauth.html is unavailable"); + throw new FileNotFoundException("Resource /reauth/reauth.html is unavailable"); } } } diff --git a/src/common/java/technicianlp/reauth/authentication/http/server/ResourcesHandler.java b/src/common/java/technicianlp/reauth/authentication/http/server/ResourcesHandler.java index d452c82..5d671fa 100644 --- a/src/common/java/technicianlp/reauth/authentication/http/server/ResourcesHandler.java +++ b/src/common/java/technicianlp/reauth/authentication/http/server/ResourcesHandler.java @@ -42,10 +42,10 @@ private static Response handleResourceGet(String path) throws IOException { String resource = null; if ("/res/icon.png".equals(path)) { contentType = "image/png"; - resource = "/resources/reauth/icon.png"; + resource = "/reauth/icon.png"; } else if ("/res/logo.png".equals(path)) { contentType = "image/png"; - resource = "/resources/reauth/logo.png"; + resource = "/reauth/logo.png"; } if (resource != null) { diff --git a/src/common/java/technicianlp/reauth/mojangfix/CertWorkaround.java b/src/common/java/technicianlp/reauth/mojangfix/CertWorkaround.java index 51a9f3f..19d4919 100644 --- a/src/common/java/technicianlp/reauth/mojangfix/CertWorkaround.java +++ b/src/common/java/technicianlp/reauth/mojangfix/CertWorkaround.java @@ -82,7 +82,7 @@ static void checkCertificates() { private static X509Certificate loadCertificate(CertificateFactory certFactory, String name) throws CertificateException, IOException { - try (InputStream is = CertWorkaround.class.getResourceAsStream("/resources/reauth/certs/" + name + ".pem")) { + try (InputStream is = CertWorkaround.class.getResourceAsStream("/reauth/certs/" + name + ".pem")) { if (is != null) { return (X509Certificate) certFactory.generateCertificate(is); } else { diff --git a/src/common/resources/resources/reauth/certs/amazonrootca1.pem b/src/common/resources/reauth/certs/amazonrootca1.pem similarity index 100% rename from src/common/resources/resources/reauth/certs/amazonrootca1.pem rename to src/common/resources/reauth/certs/amazonrootca1.pem diff --git a/src/common/resources/resources/reauth/certs/digicertglobalrootg2.pem b/src/common/resources/reauth/certs/digicertglobalrootg2.pem similarity index 100% rename from src/common/resources/resources/reauth/certs/digicertglobalrootg2.pem rename to src/common/resources/reauth/certs/digicertglobalrootg2.pem diff --git a/src/common/resources/resources/reauth/certs/microsoftrsarootcertificateauthority2017.pem b/src/common/resources/reauth/certs/microsoftrsarootcertificateauthority2017.pem similarity index 100% rename from src/common/resources/resources/reauth/certs/microsoftrsarootcertificateauthority2017.pem rename to src/common/resources/reauth/certs/microsoftrsarootcertificateauthority2017.pem diff --git a/src/common/resources/resources/reauth/icon.png b/src/common/resources/reauth/icon.png similarity index 100% rename from src/common/resources/resources/reauth/icon.png rename to src/common/resources/reauth/icon.png diff --git a/src/common/resources/resources/reauth/logo.png b/src/common/resources/reauth/logo.png similarity index 100% rename from src/common/resources/resources/reauth/logo.png rename to src/common/resources/reauth/logo.png diff --git a/src/common/resources/resources/reauth/reauth.html b/src/common/resources/reauth/reauth.html similarity index 100% rename from src/common/resources/resources/reauth/reauth.html rename to src/common/resources/reauth/reauth.html diff --git a/src/main/java/technicianlp/reauth/EventHandler.java b/src/main/java/technicianlp/reauth/EventHandler.java index f3fbf03..14d5ef0 100644 --- a/src/main/java/technicianlp/reauth/EventHandler.java +++ b/src/main/java/technicianlp/reauth/EventHandler.java @@ -39,14 +39,14 @@ public static void register() { public static void afterInit(MinecraftClient client, Screen screen, int scaledWidth, int scaledHeight) { if (screen instanceof MultiplayerScreen) { // Add Button to MultiplayerScreen - Screens.getButtons(screen).add(new ButtonWidget(5, 5, 100, 20, - Text.translatable("reauth.gui.button"), button -> openAuthenticationScreen(screen))); + Screens.getButtons(screen).add(new ButtonWidget(5, 5, 100, 20, Text.translatable("reauth.gui.button"), + button -> openAuthenticationScreen(screen))); ScreenEvents.afterRender(screen).register(EventHandler::afterRender); ScreenMouseEvents.afterMouseClick(screen).register(EventHandler::afterMouseClick); } else if (screen instanceof TitleScreen) { // Support for Custom Main Menu (add button outside of viewport) - Screens.getButtons(screen).add(new ButtonWidget(-50, -50, 20, 20, - Text.translatable("reauth.gui.button"), button -> openAuthenticationScreen(screen))); + Screens.getButtons(screen).add(new ButtonWidget(-50, -50, 20, 20, Text.translatable("reauth.gui.button"), + button -> openAuthenticationScreen(screen))); } else if (screen instanceof DisconnectedScreen) { // Add Buttons to DisconnectedScreen if its reason is an invalid session handleDisconnectScreen(screen); @@ -66,8 +66,7 @@ private static void handleDisconnectScreen(Screen screen) { Profile profile = ReAuth.profiles.getProfile(); Text retryText; if (profile != null) { - retryText = Text.translatable("reauth.retry", profile.getValue(ProfileConstants.NAME, - "Steve")); + retryText = Text.translatable("reauth.retry", profile.getValue(ProfileConstants.NAME, "Steve")); } else { retryText = Text.translatable("reauth.retry.disabled"); } @@ -111,8 +110,8 @@ private static void afterMouseClick(Screen screen, double mouseX, double mouseY, } public static void beforeInit(MinecraftClient client, Screen screen, int scaledWidth, int scaledHeight) { - if (screen instanceof MultiplayerScreen && MinecraftClient.getInstance().currentScreen instanceof - MultiplayerScreen && Screen.hasShiftDown()) { + if (screen instanceof MultiplayerScreen && + MinecraftClient.getInstance().currentScreen instanceof MultiplayerScreen && Screen.hasShiftDown()) { SessionChecker.invalidate(); } } diff --git a/src/main/java/technicianlp/reauth/configuration/Profile.java b/src/main/java/technicianlp/reauth/configuration/Profile.java index 9ba600e..a237047 100644 --- a/src/main/java/technicianlp/reauth/configuration/Profile.java +++ b/src/main/java/technicianlp/reauth/configuration/Profile.java @@ -27,4 +27,14 @@ public CompletableFuture get(String key) { CommentedConfig getConfig() { return this.config; } + + @Override + public boolean equals(Object obj) { + return this == obj || obj instanceof Profile && this.config.equals(((Profile) obj).config); + } + + @Override + public int hashCode() { + return this.config == null ? 0 : this.config.hashCode(); + } } diff --git a/src/main/java/technicianlp/reauth/configuration/ProfileList.java b/src/main/java/technicianlp/reauth/configuration/ProfileList.java index 0611486..aba7981 100644 --- a/src/main/java/technicianlp/reauth/configuration/ProfileList.java +++ b/src/main/java/technicianlp/reauth/configuration/ProfileList.java @@ -4,6 +4,8 @@ import java.util.*; import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; public final class ProfileList { public static final String PROFILES_PATH = "profiles"; @@ -17,7 +19,11 @@ public final class ProfileList { this.configuration = configuration; this.configSupplier = config::createSubConfig; this.profilesProperty = config.getOrElse(PROFILES_PATH, - () -> config.set(PROFILES_PATH, new ArrayList<>())); + () -> { + List newList = new ArrayList<>(); + config.set(PROFILES_PATH, newList); + return newList; + }); config.setComment(PROFILES_PATH, "Saved Profiles. Check Documentation for Info & Syntax"); correctProfiles(this.profilesProperty); this.saveProfiles(); @@ -25,26 +31,25 @@ public final class ProfileList { public void storeProfile(Profile profile) { List list = this.profilesProperty; - if (list.isEmpty()) { - list.add(profile.getConfig()); - } else { // TODO check this logic - list.set(0, profile.getConfig()); - } + CommentedConfig profileConfig = profile.getConfig(); + list.removeIf(profileConfig::equals); + list.add(0, profileConfig); this.saveProfiles(); } public Profile getProfile() { - List list = this.profilesProperty; - if (list.isEmpty()) { - return null; - } - CommentedConfig config = list.get(0); - String profileType = config.getOrElse(ProfileConstants.PROFILE_TYPE, ProfileConstants.PROFILE_TYPE_NONE); - if (ProfileConstants.PROFILE_TYPE_NONE.equals(profileType)) { - return null; - } else { - return new Profile(config); - } + return this.getProfileStream().findFirst().orElse(null); + } + + public List getProfiles() { + return this.getProfileStream().collect(Collectors.toList()); + } + + private Stream getProfileStream() { + return this.profilesProperty.stream() + .filter(profile -> !ProfileConstants.PROFILE_TYPE_NONE.equals( + profile.getOrElse(ProfileConstants.PROFILE_TYPE, ProfileConstants.PROFILE_TYPE_NONE))) + .map(Profile::new); } Profile createProfile(Map data) { diff --git a/src/main/java/technicianlp/reauth/gui/MainScreen.java b/src/main/java/technicianlp/reauth/gui/MainScreen.java index cfedc84..8ba4a13 100644 --- a/src/main/java/technicianlp/reauth/gui/MainScreen.java +++ b/src/main/java/technicianlp/reauth/gui/MainScreen.java @@ -11,8 +11,10 @@ import technicianlp.reauth.configuration.Profile; import technicianlp.reauth.configuration.ProfileConstants; -public final class MainScreen extends AbstractScreen { +import java.util.List; +import java.util.stream.Collectors; +public final class MainScreen extends AbstractScreen { private String message; public MainScreen(Screen parent) { @@ -32,19 +34,20 @@ public void init() { mouseX, mouseY); SaveButton saveButton = new SaveButton(this.centerX - buttonWidthH, y + 70, Text.translatable("reauth.gui.button.save"), saveButtonTooltip); - this.addDrawableChild(saveButton); - Profile profile = ReAuth.profiles.getProfile(); - if (profile != null) { - Text text = Text.translatable("reauth.gui.profile", profile.getValue(ProfileConstants.NAME, "Steve")); - this.addDrawableChild(new ButtonWidget(this.centerX - buttonWidthH, y + 10, BUTTON_WIDTH, 20, text, - button -> FlowScreen.open(Flows::loginWithProfile, profile))); - } else { + List profiles = ReAuth.profiles.getProfiles(); + if (profiles.isEmpty()) { ButtonWidget profileButton = new ButtonWidget(this.centerX - buttonWidthH, y + 10, BUTTON_WIDTH, 20, Text.translatable("reauth.gui.noProfile"), button -> { }); profileButton.active = false; this.addDrawableChild(profileButton); + } else { + List texts = profiles.stream().map(profile -> Text.translatable("reauth.gui.profile", + profile.getValue(ProfileConstants.NAME, "Steve"))).collect(Collectors.toList()); + this.addDrawableChild( + new MultiOptionButton(this, this.centerX - buttonWidthH, y + 10, BUTTON_WIDTH, 20, + texts, idx -> FlowScreen.open(Flows::loginWithProfile, profiles.get(idx)))); } this.addDrawableChild(new ButtonWidget(this.centerX - buttonWidthH, y + 45, buttonWidthH - 1, 20, @@ -53,11 +56,11 @@ public void init() { this.addDrawableChild(new ButtonWidget(this.centerX + 1, y + 45, buttonWidthH - 1, 20, Text.translatable( "reauth.gui.button.devicecode"), button -> FlowScreen.open(Flows::loginWithDeviceCode, saveButton.isChecked()))); + this.addDrawableChild(saveButton); this.addDrawableChild(new ButtonWidget(this.centerX - buttonWidthH, y + 105, BUTTON_WIDTH, 20, Text.translatable("reauth.gui.button.offline"), button -> this.transitionScreen(new OfflineLoginScreen()))); - VersionChecker.Status result = ReAuth.versionCheck.getStatus(); if (result == VersionChecker.Status.OUTDATED) { // Cannot be null but is marked as such :( diff --git a/src/main/java/technicianlp/reauth/gui/MultiOptionButton.java b/src/main/java/technicianlp/reauth/gui/MultiOptionButton.java new file mode 100644 index 0000000..17c8647 --- /dev/null +++ b/src/main/java/technicianlp/reauth/gui/MultiOptionButton.java @@ -0,0 +1,61 @@ +package technicianlp.reauth.gui; + +import com.mojang.blaze3d.systems.RenderSystem; +import net.minecraft.client.gui.DrawableHelper; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.render.GameRenderer; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.screen.ScreenTexts; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.IntConsumer; + +public class MultiOptionButton extends ButtonWidget { + static final Identifier SERVER_SELECTION_TEXTURE = new Identifier("textures/gui/server_selection.png"); + + private final Screen screen; + + private final IntConsumer onSelect; + private final List options; + private int currentOptionIndex; + + public MultiOptionButton(Screen screen, int x, int y, int width, int height, List options, + IntConsumer onSelect) { + super(x, y, width, height, options.stream().findFirst().orElse(ScreenTexts.EMPTY), button -> {}); + this.screen = screen; + this.onSelect = onSelect; + this.options = new ArrayList<>(options); + } + + @Override + public void renderButton(MatrixStack matrices, int mouseX, int mouseY, float delta) { + super.renderButton(matrices, mouseX, mouseY, delta); + if (this.isHovered()) { + RenderSystem.setShaderTexture(0, SERVER_SELECTION_TEXTURE); + RenderSystem.setShader(GameRenderer::getPositionTexShader); + RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f); + boolean isDropdownHovered = this.hovered && mouseX > this.x + this.width - 16; + DrawableHelper.drawTexture(matrices, this.x + this.width - 17, this.y + 6, 64.0f, + isDropdownHovered ? 52f : 20f, 16, 12, 256, 256); + if (isDropdownHovered) { + this.screen.renderTooltip(matrices, + Text.of((this.currentOptionIndex + 1) + "/" + this.options.size()), + mouseX, mouseY); + } + } + } + + @Override + public void onClick(double mouseX, double mouseY) { + if (mouseX > this.x + this.width - 16) { + this.currentOptionIndex = (this.currentOptionIndex + 1) % this.options.size(); + this.setMessage(this.options.get(this.currentOptionIndex)); + } else { + this.onSelect.accept(this.currentOptionIndex); + } + } +} diff --git a/src/main/java/technicianlp/reauth/mixin/DisconnectedScreenMixin.java b/src/main/java/technicianlp/reauth/mixin/DisconnectedScreenMixin.java index 05012c3..b23c04b 100644 --- a/src/main/java/technicianlp/reauth/mixin/DisconnectedScreenMixin.java +++ b/src/main/java/technicianlp/reauth/mixin/DisconnectedScreenMixin.java @@ -1,17 +1,12 @@ package technicianlp.reauth.mixin; import net.minecraft.client.gui.screen.DisconnectedScreen; -import net.minecraft.client.gui.screen.Screen; import net.minecraft.text.Text; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; @Mixin(DisconnectedScreen.class) -public abstract class DisconnectedScreenMixin extends Screen { - protected DisconnectedScreenMixin(Text title) { - super(title); - } - +public interface DisconnectedScreenMixin { @Accessor - public abstract Text getReason(); + Text getReason(); } diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 92e50cc..abecc47 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -14,6 +14,7 @@ "issues": "https://github.com/NgoKimPhu/ReAuth/issues" }, "license": "All rights reserved", + "icon": "reauth/icon.png", "environment": "client", "entrypoints": { "client": [ From 666032335e2ca9cb41da2c5a507f516ae9ec8a84 Mon Sep 17 00:00:00 2001 From: NgoKimPhu Date: Sun, 23 Oct 2022 17:58:32 +0800 Subject: [PATCH 8/8] build fix (includes night-config dependencies) --- build.gradle | 4 +++- gradle.properties | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c021d46..65be9d7 100644 --- a/build.gradle +++ b/build.gradle @@ -38,7 +38,9 @@ dependencies { modApi("me.shedaniel.cloth:cloth-config-fabric:${project.cloth_config_version}") { exclude(group: "net.fabricmc.fabric-api") } - implementation 'com.electronwill.night-config:toml:3.6.6' + implementation "com.electronwill.night-config:toml:${project.night_config_version}" + include "com.electronwill.night-config:core:${project.night_config_version}" + include "com.electronwill.night-config:toml:${project.night_config_version}" } sourceSets { diff --git a/gradle.properties b/gradle.properties index 81ff0cf..ede995d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,3 +17,4 @@ archives_base_name=ReAuth-1.19.2-Fabric fabric_version=0.61.0+1.19.2 cloth_config_version=8.2.88 modmenu_version=4.0.6 +night_config_version=3.6.6