goauth

Fabric plugin for enhanced whitelists
git clone git://git.bain.cz/goauth.git
Log | Files | Refs | LICENSE

commit 6445d708894d4256bccc750a1065c555ea711ef7
Author: bain <bain@bain.cz>
Date:   Sat, 27 Nov 2021 15:35:40 +0100

Initial Commit

Diffstat:
A.gitignore | 118+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ALICENSE | 21+++++++++++++++++++++
Abuild.gradle | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agradle.properties | 14++++++++++++++
Agradle/wrapper/gradle-wrapper.jar | 0
Agradle/wrapper/gradle-wrapper.properties | 5+++++
Agradlew | 185+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agradlew.bat | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asettings.gradle | 9+++++++++
Asrc/main/java/cz/bain/plugins/goauth/DummyServerPlayNetworkHandler.java | 162+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/cz/bain/plugins/goauth/Goauth.java | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/cz/bain/plugins/goauth/GoauthConfig.java | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/cz/bain/plugins/goauth/OAuthUpdate.java | 16++++++++++++++++
Asrc/main/java/cz/bain/plugins/goauth/StringRepository.java | 8++++++++
Asrc/main/java/cz/bain/plugins/goauth/WhitelistUpdateRunner.java | 129+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/cz/bain/plugins/goauth/events/callbacks/OnPlayerConnectCallback.java | 24++++++++++++++++++++++++
Asrc/main/java/cz/bain/plugins/goauth/events/callbacks/OnServerTickCallback.java | 23+++++++++++++++++++++++
Asrc/main/java/cz/bain/plugins/goauth/events/callbacks/OnWhitelistAddCallback.java | 26++++++++++++++++++++++++++
Asrc/main/java/cz/bain/plugins/goauth/events/listeners/OnPlayerConnectListener.java | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/cz/bain/plugins/goauth/events/listeners/OnServerTickListener.java | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/cz/bain/plugins/goauth/events/listeners/OnWhitelistAddListener.java | 34++++++++++++++++++++++++++++++++++
Asrc/main/java/cz/bain/plugins/goauth/mixin/DimensionTypeAccessor.java | 14++++++++++++++
Asrc/main/java/cz/bain/plugins/goauth/mixin/MinecraftServerMixin.java | 32++++++++++++++++++++++++++++++++
Asrc/main/java/cz/bain/plugins/goauth/mixin/OnPlayerConnectMixin.java | 32++++++++++++++++++++++++++++++++
Asrc/main/java/cz/bain/plugins/goauth/mixin/PlayerManagerAccessor.java | 17+++++++++++++++++
Asrc/main/java/cz/bain/plugins/goauth/mixin/UserCacheMixin.java | 37+++++++++++++++++++++++++++++++++++++
Asrc/main/java/cz/bain/plugins/goauth/mixin/WhitelistCommandMixin.java | 23+++++++++++++++++++++++
Asrc/main/resources/fabric.mod.json | 29+++++++++++++++++++++++++++++
Asrc/main/resources/goauth.mixins.json | 19+++++++++++++++++++
29 files changed, 1427 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,118 @@ +# User-specific stuff +.idea/ + +*.iml +*.ipr +*.iws + +# IntelliJ +out/ +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +.gradle +build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Cache of project +.gradletasknamecache + +**/build/ + +# Common working directory +run/ + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar diff --git a/LICENSE b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2021 bain + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/build.gradle b/build.gradle @@ -0,0 +1,90 @@ +plugins { + id 'fabric-loom' version '0.10-SNAPSHOT' + id 'maven-publish' +} + +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. +} + +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}" + + // Fabric API. This is technically optional, but you probably want it anyway. + modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_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. +} + +processResources { + inputs.property "version", project.version + filteringCharset "UTF-8" + + filesMatching("fabric.mod.json") { + expand "version": project.version + } +} + +def targetJavaVersion = 16 +tasks.withType(JavaCompile).configureEach { + // 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 + // If Javadoc is generated, this must be specified in that task too. + it.options.encoding = "UTF-8" + if (targetJavaVersion >= 10 || JavaVersion.current().isJava10Compatible()) { + it.options.release = targetJavaVersion + } +} + +java { + def javaVersion = JavaVersion.toVersion(targetJavaVersion) + if (JavaVersion.current() < javaVersion) { + toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion) + } + archivesBaseName = project.archives_base_name + // Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task + // if it is present. + // If you remove this line, sources will not be generated. + withSourcesJar() +} + +jar { + from("LICENSE") { + rename { "${it}_${project.archivesBaseName}" } + } +} + +// configure the maven publication +publishing { + publications { + mavenJava(MavenPublication) { + // add all the jars that should be included when publishing to maven + artifact(remapJar) { + builtBy remapJar + } + artifact(sourcesJar) { + builtBy remapSourcesJar + } + } + } + + // See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing. + repositories { + // Add repositories to publish to here. + // Notice: This block does NOT have the same function as the block in the top level. + // The repositories here will be used for publishing your artifact, not for + // retrieving dependencies. + } +} diff --git a/gradle.properties b/gradle.properties @@ -0,0 +1,14 @@ +# Done to increase the memory available to gradle. +org.gradle.jvmargs=-Xmx1G +# Fabric Properties +# check these on https://modmuss50.me/fabric.html +minecraft_version=1.17.1 +yarn_mappings=1.17.1+build.64 +loader_version=0.12.5 +# Mod Properties +mod_version=1.0 +maven_group=cz.bain.plugins +archives_base_name=goauth +# Dependencies +# check this on https://modmuss50.me/fabric.html +fabric_version=0.42.1+1.17 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar Binary files differ. diff --git a/gradle/wrapper/gradle-wrapper.properties 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.1.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or 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 UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + 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" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat 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/settings.gradle b/settings.gradle @@ -0,0 +1,9 @@ +pluginManagement { + repositories { + maven { + name = 'Fabric' + url = 'https://maven.fabricmc.net/' + } + gradlePluginPortal() + } +} diff --git a/src/main/java/cz/bain/plugins/goauth/DummyServerPlayNetworkHandler.java b/src/main/java/cz/bain/plugins/goauth/DummyServerPlayNetworkHandler.java @@ -0,0 +1,161 @@ +package cz.bain.plugins.goauth; + +import net.minecraft.network.ClientConnection; +import net.minecraft.network.MessageType; +import net.minecraft.network.packet.c2s.play.*; +import net.minecraft.network.packet.s2c.play.GameMessageS2CPacket; +import net.minecraft.network.packet.s2c.play.KeepAliveS2CPacket; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayNetworkHandler; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; +import net.minecraft.util.Util; + +import java.util.UUID; + +public class DummyServerPlayNetworkHandler extends ServerPlayNetworkHandler { + + private int ticks = 0; + private final MinecraftServer server; + private long lastKeepAliveTime; + private boolean waitingForKeepAlive; + private long keepAliveId; + private final Goauth plugin; + + public DummyServerPlayNetworkHandler(MinecraftServer server, ClientConnection connection, ServerPlayerEntity player, Goauth plugin) { + super(server, connection, player); + this.server = server; + this.plugin = plugin; + } + + + @Override + public void tick() { + ++this.ticks; + if (this.ticks > 18000) { + this.disconnect(Text.of(StringRepository.limboExpiredDisconnect)); + } + + if (this.ticks > 160 && this.ticks % 40 == 0) + this.sendPacket(new GameMessageS2CPacket(Text.of("Please authenticate in chat"), MessageType.GAME_INFO, new UUID(0, 0))); + + server.getProfiler().push("keepAlive"); + long l = Util.getMeasuringTimeMs(); + if (l - this.lastKeepAliveTime >= 15000L) { + if (this.waitingForKeepAlive) { + this.disconnect(new TranslatableText("disconnect.timeout")); + } else { + this.waitingForKeepAlive = true; + this.lastKeepAliveTime = l; + this.keepAliveId = l; + this.sendPacket(new KeepAliveS2CPacket(this.keepAliveId)); + } + } + + this.server.getProfiler().pop(); + } + + @Override + public void onKeepAlive(KeepAliveC2SPacket packet) { + if (this.waitingForKeepAlive && packet.getId() == this.keepAliveId) { + int i = (int) (Util.getMeasuringTimeMs() - this.lastKeepAliveTime); + this.player.pingMilliseconds = (this.player.pingMilliseconds * 3 + i) / 4; + this.waitingForKeepAlive = false; + } else { + this.disconnect(new TranslatableText("disconnect.timeout")); + } + + } + + @Override + public void onDisconnected(Text reason) { + Goauth.LOGGER.info("Player {} disconnected from limbo", this.player.getName().getString()); + this.player.onDisconnect(); + this.player.getTextStream().onDisconnect(); + this.plugin.playersLimbo.remove(this.getPlayer().getGameProfile().getName()); + } + + /* Override most of the methods to do nothing, the player can't do it anyways */ + + public void onTeleportConfirm(TeleportConfirmC2SPacket packet) { + } + + public void onPlayerMove(PlayerMoveC2SPacket packet) { + } + + public void onRecipeBookData(RecipeBookDataC2SPacket packet) { + } + + public void onRecipeCategoryOptions(RecipeCategoryOptionsC2SPacket packet) { + } + + public void onAdvancementTab(AdvancementTabC2SPacket packet) { + } + + public void onRequestCommandCompletions(RequestCommandCompletionsC2SPacket packet) { + } + + public void onUpdateCommandBlock(UpdateCommandBlockC2SPacket packet) { + } + + public void onUpdateCommandBlockMinecart(UpdateCommandBlockMinecartC2SPacket packet) { + } + + public void onPickFromInventory(PickFromInventoryC2SPacket packet) { + } + + public void onRenameItem(RenameItemC2SPacket packet) { + } + + public void onUpdateBeacon(UpdateBeaconC2SPacket packet) { + } + + public void onStructureBlockUpdate(UpdateStructureBlockC2SPacket packet) { + } + + public void onJigsawUpdate(UpdateJigsawC2SPacket packet) { + } + + public void onJigsawGenerating(JigsawGeneratingC2SPacket packet) { + } + + public void onMerchantTradeSelect(SelectMerchantTradeC2SPacket packet) { + } + + public void onBookUpdate(BookUpdateC2SPacket packet) { + } + + public void onQueryEntityNbt(QueryEntityNbtC2SPacket packet) { + } + + public void onQueryBlockNbt(QueryBlockNbtC2SPacket packet) { + } + + public void onPlayerAction(PlayerActionC2SPacket packet) { + } + + public void onPlayerInteractBlock(PlayerInteractBlockC2SPacket packet) { + } + + public void onPlayerInteractItem(PlayerInteractItemC2SPacket packet) { + } + + public void onSpectatorTeleport(SpectatorTeleportC2SPacket packet) { + } + + public void onResourcePackStatus(ResourcePackStatusC2SPacket packet) { + } + + public void onBoatPaddleState(BoatPaddleStateC2SPacket packet) { + } + + public void onPlayerInput(PlayerInputC2SPacket packet) { + } + + public void onVehicleMove(VehicleMoveC2SPacket packet) { + } + + public void onGameMessage(ChatMessageC2SPacket packet) { + } +} +\ No newline at end of file diff --git a/src/main/java/cz/bain/plugins/goauth/Goauth.java b/src/main/java/cz/bain/plugins/goauth/Goauth.java @@ -0,0 +1,72 @@ +package cz.bain.plugins.goauth; + +import cz.bain.plugins.goauth.events.callbacks.OnPlayerConnectCallback; +import cz.bain.plugins.goauth.events.callbacks.OnServerTickCallback; +import cz.bain.plugins.goauth.events.callbacks.OnWhitelistAddCallback; +import cz.bain.plugins.goauth.events.listeners.OnPlayerConnectListener; +import cz.bain.plugins.goauth.events.listeners.OnServerTickListener; +import cz.bain.plugins.goauth.events.listeners.OnWhitelistAddListener; +import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.UserCache; +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.binary.Hex; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Objects; + + +public class Goauth implements ModInitializer { + + public static final Logger LOGGER = LogManager.getLogger("goauth"); + public final List<OAuthUpdate> completed = new ArrayList<>(); + public final HashMap<String, ServerPlayerEntity> playersLimbo = new HashMap<>(); + public static int port = 8000; + public String authLink; + public boolean running = true; + + @Override + public void onInitialize() { + OnPlayerConnectCallback.EVENT.register(new OnPlayerConnectListener(this)); + OnServerTickCallback.EVENT.register(new OnServerTickListener(this)); + OnWhitelistAddCallback.EVENT.register(new OnWhitelistAddListener(this)); + + ServerLifecycleEvents.SERVER_STARTED.register(server -> { + UserCache.setUseRemote(server.isOnlineMode()); + }); + ServerLifecycleEvents.SERVER_STOPPED.register(server -> running = false); + + GoauthConfig config; + try { + config = new GoauthConfig(new File("goauth.properties")); + } catch (IOException e) { + LOGGER.error(e.getMessage()); + return; + } + port = Integer.parseInt(config.getRequired("port")); + byte[] key; + try { + key = Hex.decodeHex(config.getRequired("key").toCharArray()); + } catch (DecoderException e) { + LOGGER.error("Failed to parse communication key! Needs to be in hex."); + return; + } + if (key.length != 32) { + LOGGER.error("Communication key is not 64 bytes long!"); + return; + } + + StringRepository.authLinkText = config.getRequired("auth-link-text"); + authLink = Objects.requireNonNullElse(config.get("auth-link"), ""); + + new Thread(new WhitelistUpdateRunner(key, this)).start(); + LOGGER.info("Initialized!"); + } +} diff --git a/src/main/java/cz/bain/plugins/goauth/GoauthConfig.java b/src/main/java/cz/bain/plugins/goauth/GoauthConfig.java @@ -0,0 +1,57 @@ +package cz.bain.plugins.goauth; + +import org.jetbrains.annotations.Nullable; +import org.lwjgl.system.CallbackI; + +import java.io.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; + +public class GoauthConfig { + + private final HashMap<String, String> options = new HashMap<>(); + + public GoauthConfig(File file) throws IOException { + + if (!file.exists()) { + try (FileWriter writer = new FileWriter(file)) { + writer.write("port=8001\nkey=<32-byte-key>\nauth-link-text=Authenticate\n\n# optional\n# auth-link=https://bain.cz/\n"); + } + throw new IOException("goauth.properties didn't exist, please review its contents and restart the server"); + } + + BufferedReader reader = new BufferedReader(new FileReader(file)); + String line; + int line_no = 0; + List<String> required = new ArrayList<>(); + required.add("port"); + required.add("key"); + required.add("auth-link-text"); + while ((line = reader.readLine()) != null) { + line_no++; + if (line.startsWith("#") || line.length() == 0) continue; + String[] parsed = line.split("="); + if (parsed.length != 2) { + reader.close(); + throw new IOException("Could not parse config! Error on line " + line_no); + } + required.remove(parsed[0]); + options.put(parsed[0], parsed[1]); + } + reader.close(); + if (!required.isEmpty()) { + throw new IOException("Missing configuration options: " + Arrays.toString(required.toArray())); + } + } + + @Nullable + public String get(String key) { + return options.getOrDefault(key, null); + } + + public String getRequired(String key) { + return options.get(key); + } +} diff --git a/src/main/java/cz/bain/plugins/goauth/OAuthUpdate.java b/src/main/java/cz/bain/plugins/goauth/OAuthUpdate.java @@ -0,0 +1,16 @@ +package cz.bain.plugins.goauth; + +public record OAuthUpdate(cz.bain.plugins.goauth.OAuthUpdate.UpdateType type, String username) { + public enum UpdateType { + ADD, + REMOVE; + + public static UpdateType fromString(String type) { + return switch (type) { + case "add" -> UpdateType.ADD; + case "remove" -> UpdateType.REMOVE; + default -> null; + }; + } + } +} diff --git a/src/main/java/cz/bain/plugins/goauth/StringRepository.java b/src/main/java/cz/bain/plugins/goauth/StringRepository.java @@ -0,0 +1,8 @@ +package cz.bain.plugins.goauth; + +public class StringRepository { + public static String authLinkText; + public static String limboRemoveDisconnect = "You have been added to the whitelist, please reconnect"; + public static String limboExpiredDisconnect = "Authentication period expired"; + public static String playerWhitelistKick = "You have been removed from the whitelist"; +} diff --git a/src/main/java/cz/bain/plugins/goauth/WhitelistUpdateRunner.java b/src/main/java/cz/bain/plugins/goauth/WhitelistUpdateRunner.java @@ -0,0 +1,129 @@ +package cz.bain.plugins.goauth; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; + +public class WhitelistUpdateRunner implements Runnable { + + private final SecretKeySpec key; + private final Goauth plugin; + + public WhitelistUpdateRunner(byte[] key, Goauth plugin) { + this.key = new SecretKeySpec(key, "ChaCha20"); + this.plugin = plugin; + } + + @Override + public void run() { + ServerSocket serverSocket; + try { + serverSocket = new ServerSocket(); + serverSocket.bind(new InetSocketAddress(Goauth.port), 2); + serverSocket.setSoTimeout(5); + + } catch (IOException e) { + Goauth.LOGGER.error("Failed to create socket for updates!"); + return; + } + while (this.plugin.running) { + try ( + Socket socket = serverSocket.accept(); + InputStream inputStream = socket.getInputStream(); + OutputStream outputStream = socket.getOutputStream() + ) { + // protecting ourselves from being bombarded with large amounts of data + byte[] sizeBytes = inputStream.readNBytes(4); + if (sizeBytes.length != 4) continue; + int size = fromByteArray(sizeBytes); + if (size > 2048) { + outputStream.write(0x01); + outputStream.flush(); + Goauth.LOGGER.warn("Whitelist update rejected: payload size too big ({})", size); + continue; + } else { + outputStream.write(0x00); + outputStream.flush(); + } + byte[] iv = inputStream.readNBytes(12); + if (iv.length != 12) continue; + byte[] cipherText = inputStream.readNBytes(size); + if (cipherText.length != size) continue; + byte[] plainText; + try { + plainText = decryptMessage(cipherText, iv); + } catch (GeneralSecurityException e) { + Goauth.LOGGER.warn("Whitelist update rejected: failed to decrypt payload"); + outputStream.write(0x01); + outputStream.flush(); + continue; + } + JsonParser parser = new JsonParser(); + JsonArray el = parser.parse(new String(plainText, StandardCharsets.UTF_8)).getAsJsonArray(); + Goauth.LOGGER.info("Got " + el.size() + " new whitelist updates"); + synchronized (this.plugin.completed) { + for (JsonElement user : el) { + JsonObject userObject = user.getAsJsonObject(); + String username = userObject.get("username").getAsString(); + OAuthUpdate.UpdateType type = OAuthUpdate.UpdateType.fromString(userObject.get("type").getAsString()); + if (type == null) { + Goauth.LOGGER.warn("Whitelist update rejected: bad update data (type={})", userObject.get("type").getAsString()); + outputStream.write(0x01); + outputStream.flush(); + break; + } + this.plugin.completed.add(new OAuthUpdate(type, username)); + outputStream.write(0x00); + } + } + } catch (SocketTimeoutException ignored) { + } catch (IOException | IllegalStateException e) { + Goauth.LOGGER.warn("Error while processing OAuth updates, dropping connection"); + e.printStackTrace(); + } + } + } + + /** + * Decrypts a message with the ChaCha20-Poly1305 cipher. + * + * @param cipherText cipher text with appended digest to the end + * @param iv unique nonce + * @return plain text + * @throws GeneralSecurityException when the decryption goes wrong + */ + private byte[] decryptMessage(byte[] cipherText, byte[] iv) throws GeneralSecurityException { + Cipher cipher = Cipher.getInstance("ChaCha20-Poly1305"); + IvParameterSpec ivSpec = new IvParameterSpec(iv); + cipher.init(Cipher.DECRYPT_MODE, this.key, ivSpec); + + return cipher.doFinal(cipherText); + } + + /** + * Little endian int from byte array + * + * @param bytes byte array + * @return int + */ + private int fromByteArray(byte[] bytes) { + return ((bytes[0] & 0xFF) << 24) | + ((bytes[1] & 0xFF) << 16) | + ((bytes[2] & 0xFF) << 8) | + (bytes[3] & 0xFF); + } +} diff --git a/src/main/java/cz/bain/plugins/goauth/events/callbacks/OnPlayerConnectCallback.java b/src/main/java/cz/bain/plugins/goauth/events/callbacks/OnPlayerConnectCallback.java @@ -0,0 +1,24 @@ +package cz.bain.plugins.goauth.events.callbacks; + +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; +import net.minecraft.network.ClientConnection; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.ActionResult; + +public interface OnPlayerConnectCallback { + Event<OnPlayerConnectCallback> EVENT = EventFactory.createArrayBacked(OnPlayerConnectCallback.class, + (listeners) -> (player, connection) -> { + for (OnPlayerConnectCallback listener : listeners) { + ActionResult result = listener.interact(player, connection); + + if (result != ActionResult.PASS) { + return result; + } + } + + return ActionResult.PASS; + }); + + ActionResult interact(ServerPlayerEntity player, ClientConnection connection); +} diff --git a/src/main/java/cz/bain/plugins/goauth/events/callbacks/OnServerTickCallback.java b/src/main/java/cz/bain/plugins/goauth/events/callbacks/OnServerTickCallback.java @@ -0,0 +1,23 @@ +package cz.bain.plugins.goauth.events.callbacks; + +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; +import net.minecraft.server.PlayerManager; +import net.minecraft.util.ActionResult; + +public interface OnServerTickCallback { + Event<OnServerTickCallback> EVENT = EventFactory.createArrayBacked(OnServerTickCallback.class, + (listeners) -> (playerManager, ticks) -> { + for (OnServerTickCallback listener : listeners) { + ActionResult result = listener.interact(playerManager, ticks); + + if (result != ActionResult.PASS) { + return result; + } + } + + return ActionResult.PASS; + }); + + ActionResult interact(PlayerManager playerManager, long ticks); +} diff --git a/src/main/java/cz/bain/plugins/goauth/events/callbacks/OnWhitelistAddCallback.java b/src/main/java/cz/bain/plugins/goauth/events/callbacks/OnWhitelistAddCallback.java @@ -0,0 +1,26 @@ +package cz.bain.plugins.goauth.events.callbacks; + +import com.mojang.authlib.GameProfile; +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.util.ActionResult; + +import java.util.Collection; + +public interface OnWhitelistAddCallback { + Event<OnWhitelistAddCallback> EVENT = EventFactory.createArrayBacked(OnWhitelistAddCallback.class, + (listeners) -> (source, targets) -> { + for (OnWhitelistAddCallback listener : listeners) { + ActionResult result = listener.interact(source, targets); + + if (result != ActionResult.PASS) { + return result; + } + } + + return ActionResult.PASS; + }); + + ActionResult interact(ServerCommandSource source, Collection<GameProfile> targets); +} diff --git a/src/main/java/cz/bain/plugins/goauth/events/listeners/OnPlayerConnectListener.java b/src/main/java/cz/bain/plugins/goauth/events/listeners/OnPlayerConnectListener.java @@ -0,0 +1,86 @@ +package cz.bain.plugins.goauth.events.listeners; + +import cz.bain.plugins.goauth.DummyServerPlayNetworkHandler; +import cz.bain.plugins.goauth.Goauth; +import cz.bain.plugins.goauth.StringRepository; +import cz.bain.plugins.goauth.events.callbacks.OnPlayerConnectCallback; +import cz.bain.plugins.goauth.mixin.DimensionTypeAccessor; +import cz.bain.plugins.goauth.mixin.PlayerManagerAccessor; +import io.netty.buffer.Unpooled; +import net.minecraft.network.ClientConnection; +import net.minecraft.network.MessageType; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.network.packet.s2c.play.*; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayNetworkHandler; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.*; +import net.minecraft.util.ActionResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.Difficulty; +import net.minecraft.world.GameMode; +import net.minecraft.world.World; +import net.minecraft.world.border.WorldBorder; + +import java.util.Collections; +import java.util.Objects; +import java.util.UUID; + +public class OnPlayerConnectListener implements OnPlayerConnectCallback { + + private final Goauth plugin; + + public OnPlayerConnectListener(Goauth plugin) { + this.plugin = plugin; + } + + /** + * Here we fake everything if the player is not on the whitelist. Just send him nothing and a link to authenticate. + */ + @Override + public ActionResult interact(ServerPlayerEntity player, ClientConnection connection) { + MinecraftServer server = Objects.requireNonNull(player.getServer()); + + // the player is in the whitelist, let the server do its things + if (server.getPlayerManager().getWhitelist().isAllowed(player.getGameProfile())) return ActionResult.PASS; + + Goauth.LOGGER.info("Player " + player.getGameProfile().getName() + " is not on the whitelist, putting him in limbo"); + + // fake initial connection info, use DummyServerPlayNetworkHandler to override all events and ignore them + ServerPlayNetworkHandler serverPlayNetworkHandler = new DummyServerPlayNetworkHandler(server, connection, player, plugin); + serverPlayNetworkHandler.sendPacket(new GameJoinS2CPacket( + player.getId(), GameMode.SPECTATOR, null, + 0, false, server.getWorldRegistryKeys(), + ((PlayerManagerAccessor) server.getPlayerManager()).getRegistryManager(), + DimensionTypeAccessor.getTHE_END(), World.END, 1, + 3, true, false, false, true + )); + serverPlayNetworkHandler.sendPacket(new CustomPayloadS2CPacket(CustomPayloadS2CPacket.BRAND, (new PacketByteBuf(Unpooled.buffer())).writeString("limbo"))); + serverPlayNetworkHandler.sendPacket(new DifficultyS2CPacket(Difficulty.PEACEFUL, true)); + serverPlayNetworkHandler.sendPacket(new PlayerAbilitiesS2CPacket(player.getAbilities())); + serverPlayNetworkHandler.sendPacket(new UpdateSelectedSlotS2CPacket(player.getInventory().selectedSlot)); + serverPlayNetworkHandler.sendPacket(new PlayerPositionLookS2CPacket(0, 0, 0, 0, 0, Collections.emptySet(), 0, true)); + serverPlayNetworkHandler.sendPacket(new GameStateChangeS2CPacket(GameStateChangeS2CPacket.GAME_MODE_CHANGED, GameMode.SPECTATOR.getId())); + serverPlayNetworkHandler.sendPacket(new WorldBorderInitializeS2CPacket(new WorldBorder())); + serverPlayNetworkHandler.sendPacket(new WorldTimeUpdateS2CPacket(0, 0, false)); + serverPlayNetworkHandler.sendPacket(new PlayerSpawnPositionS2CPacket(new BlockPos(0, 0, 0), 0.0f)); + + LiteralText welcomeMsg = new LiteralText("Welcome! Please authenticate: "); + welcomeMsg.setStyle(Style.EMPTY.withBold(true).withColor(TextColor.parse("yellow"))); + LiteralText text = new LiteralText(StringRepository.authLinkText); + Style urlStyle = Style.EMPTY + .withBold(false) + .withColor(TextColor.parse("white")) + .withUnderline(!this.plugin.authLink.isEmpty()) + .withClickEvent(!this.plugin.authLink.isEmpty() ? new ClickEvent(ClickEvent.Action.OPEN_URL, this.plugin.authLink + "?username=" + player.getGameProfile().getName()) : null) + .withHoverEvent(!this.plugin.authLink.isEmpty() ? new HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.of("Open link")) : null); + text.setStyle(urlStyle); + welcomeMsg.append(text); + player.sendMessage(welcomeMsg, MessageType.SYSTEM, new UUID(0, 0)); + + this.plugin.playersLimbo.put(player.getGameProfile().getName(), player); + + // return completely + return ActionResult.FAIL; + } +} diff --git a/src/main/java/cz/bain/plugins/goauth/events/listeners/OnServerTickListener.java b/src/main/java/cz/bain/plugins/goauth/events/listeners/OnServerTickListener.java @@ -0,0 +1,56 @@ +package cz.bain.plugins.goauth.events.listeners; + +import com.mojang.authlib.GameProfile; +import cz.bain.plugins.goauth.Goauth; +import cz.bain.plugins.goauth.OAuthUpdate; +import cz.bain.plugins.goauth.StringRepository; +import cz.bain.plugins.goauth.events.callbacks.OnServerTickCallback; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.server.PlayerManager; +import net.minecraft.server.Whitelist; +import net.minecraft.server.WhitelistEntry; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.util.ActionResult; + +public class OnServerTickListener implements OnServerTickCallback { + + private final Goauth plugin; + + public OnServerTickListener(Goauth plugin) { + this.plugin = plugin; + } + + @Override + public ActionResult interact(PlayerManager playerManager, long ticks) { + if (ticks % 20 == 0) { + Whitelist wl = playerManager.getWhitelist(); + synchronized (this.plugin.completed) { + for (OAuthUpdate update : this.plugin.completed) { + switch (update.type()) { + case ADD -> { + GameProfile profile = new GameProfile(PlayerEntity.getOfflinePlayerUuid(update.username()), update.username()); + if (!wl.isAllowed(profile)) wl.add(new WhitelistEntry(profile)); + Goauth.LOGGER.info("Added " + update.username() + " to the whitelist"); + ServerPlayerEntity player = this.plugin.playersLimbo.get(update.username()); + if (player != null) { + player.networkHandler.disconnect( + Text.of(StringRepository.limboRemoveDisconnect) + ); + } + } + case REMOVE -> { + GameProfile profile = new GameProfile(PlayerEntity.getOfflinePlayerUuid(update.username()), update.username()); + if (wl.isAllowed(profile)) wl.remove(profile); + Goauth.LOGGER.info("Removed " + update.username() + " from the whitelist"); + ServerPlayerEntity player = playerManager.getPlayer(update.username()); + if (player != null) player.networkHandler.disconnect(Text.of(StringRepository.playerWhitelistKick)); + } + } + } + this.plugin.completed.clear(); + } + } + return ActionResult.PASS; + } +} diff --git a/src/main/java/cz/bain/plugins/goauth/events/listeners/OnWhitelistAddListener.java b/src/main/java/cz/bain/plugins/goauth/events/listeners/OnWhitelistAddListener.java @@ -0,0 +1,34 @@ +package cz.bain.plugins.goauth.events.listeners; + +import com.mojang.authlib.GameProfile; +import cz.bain.plugins.goauth.Goauth; +import cz.bain.plugins.goauth.StringRepository; +import cz.bain.plugins.goauth.events.callbacks.OnWhitelistAddCallback; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.util.ActionResult; + +import java.util.Collection; + +public class OnWhitelistAddListener implements OnWhitelistAddCallback { + + private final Goauth plugin; + + public OnWhitelistAddListener(Goauth plugin) { + this.plugin = plugin; + } + + @Override + public ActionResult interact(ServerCommandSource source, Collection<GameProfile> targets) { + + for (GameProfile target : targets) { + ServerPlayerEntity player = this.plugin.playersLimbo.get(target.getName()); + if (player != null) { + player.networkHandler.disconnect(Text.of(StringRepository.limboRemoveDisconnect)); + } + } + + return ActionResult.PASS; + } +} diff --git a/src/main/java/cz/bain/plugins/goauth/mixin/DimensionTypeAccessor.java b/src/main/java/cz/bain/plugins/goauth/mixin/DimensionTypeAccessor.java @@ -0,0 +1,14 @@ +package cz.bain.plugins.goauth.mixin; + +import net.minecraft.world.dimension.DimensionType; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(DimensionType.class) +public interface DimensionTypeAccessor { + + @Accessor + static DimensionType getTHE_END() { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/cz/bain/plugins/goauth/mixin/MinecraftServerMixin.java b/src/main/java/cz/bain/plugins/goauth/mixin/MinecraftServerMixin.java @@ -0,0 +1,32 @@ +package cz.bain.plugins.goauth.mixin; + +import cz.bain.plugins.goauth.events.callbacks.OnServerTickCallback; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.PlayerManager; +import net.minecraft.util.ActionResult; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.function.BooleanSupplier; + +@Mixin(MinecraftServer.class) +public abstract class MinecraftServerMixin { + + @Shadow + public abstract int getTicks(); + + @Shadow + public abstract PlayerManager getPlayerManager(); + + @Inject(at = @At(value = "TAIL"), method = "tick", cancellable = true) + public void tickInjection(BooleanSupplier shouldKeepTicking, CallbackInfo ci) { + ActionResult result = OnServerTickCallback.EVENT.invoker().interact(getPlayerManager(), getTicks()); + + if (result == ActionResult.FAIL) { + ci.cancel(); + } + } +} diff --git a/src/main/java/cz/bain/plugins/goauth/mixin/OnPlayerConnectMixin.java b/src/main/java/cz/bain/plugins/goauth/mixin/OnPlayerConnectMixin.java @@ -0,0 +1,32 @@ +package cz.bain.plugins.goauth.mixin; + +import cz.bain.plugins.goauth.events.callbacks.OnPlayerConnectCallback; +import net.minecraft.network.ClientConnection; +import net.minecraft.server.PlayerManager; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.ActionResult; +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; + +@Mixin(PlayerManager.class) +public class OnPlayerConnectMixin { + @Inject(at = @At(value = "HEAD"), method = "onPlayerConnect", cancellable = true) + private void onPlayerConnectInjectionHEAD(ClientConnection connection, ServerPlayerEntity player, CallbackInfo info) { + ActionResult result = OnPlayerConnectCallback.EVENT.invoker().interact(player, connection); + + if (result == ActionResult.FAIL) { + info.cancel(); + } + } + + @Inject(at = @At(value = "TAIL"), method = "onPlayerConnect", cancellable = true) + private void onPlayerConnectInjectionTAIL(ClientConnection connection, ServerPlayerEntity player, CallbackInfo info) { + ActionResult result = OnPlayerConnectCallback.EVENT.invoker().interact(player, connection); + + if (result == ActionResult.FAIL) { + info.cancel(); + } + } +} diff --git a/src/main/java/cz/bain/plugins/goauth/mixin/PlayerManagerAccessor.java b/src/main/java/cz/bain/plugins/goauth/mixin/PlayerManagerAccessor.java @@ -0,0 +1,17 @@ +package cz.bain.plugins.goauth.mixin; + +import net.minecraft.server.PlayerManager; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.registry.DynamicRegistryManager; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(PlayerManager.class) +public interface PlayerManagerAccessor { + @Invoker + void callSavePlayerData(ServerPlayerEntity player); + + @Accessor + DynamicRegistryManager.Impl getRegistryManager(); +} diff --git a/src/main/java/cz/bain/plugins/goauth/mixin/UserCacheMixin.java b/src/main/java/cz/bain/plugins/goauth/mixin/UserCacheMixin.java @@ -0,0 +1,37 @@ +package cz.bain.plugins.goauth.mixin; + +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.GameProfileRepository; +import cz.bain.plugins.goauth.Goauth; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.util.UserCache; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.Optional; + +@Mixin(UserCache.class) +public abstract class UserCacheMixin { + + @Shadow + private static boolean shouldUseRemote() { + return false; + } + + /** + * Override UUIDs when server is in offline mode + */ + @Inject(at = @At(value = "RETURN"), method = "findProfileByName", cancellable = true) + private static void findProfileByNameInject(GameProfileRepository repository, String name, CallbackInfoReturnable<Optional<GameProfile>> cir) { + if (!shouldUseRemote() && cir.getReturnValue().isPresent()) { + GameProfile profile = cir.getReturnValue().get(); + Goauth.LOGGER.info("changin uuid"); + cir.setReturnValue(Optional.of( + new GameProfile(PlayerEntity.getUuidFromProfile(new GameProfile(null, profile.getName())), profile.getName()) + )); + } + } +} diff --git a/src/main/java/cz/bain/plugins/goauth/mixin/WhitelistCommandMixin.java b/src/main/java/cz/bain/plugins/goauth/mixin/WhitelistCommandMixin.java @@ -0,0 +1,23 @@ +package cz.bain.plugins.goauth.mixin; + +import com.mojang.authlib.GameProfile; +import cz.bain.plugins.goauth.events.callbacks.OnWhitelistAddCallback; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.dedicated.command.WhitelistCommand; +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.CallbackInfoReturnable; + +import java.util.Collection; + +@Mixin(WhitelistCommand.class) +public class WhitelistCommandMixin { + + @Inject(at = @At(value = "RETURN"), method = "executeAdd") + private static void executeAdd(ServerCommandSource source, Collection<GameProfile> targets, CallbackInfoReturnable<Integer> cir) { + if (cir.getReturnValue() == 1) { + OnWhitelistAddCallback.EVENT.invoker().interact(source, targets); + } + } +} diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json @@ -0,0 +1,29 @@ +{ + "schemaVersion": 1, + "id": "goauth", + "version": "${version}", + "name": "GoogleOAuth", + "description": "Implements a Google OAuth whitelist", + "authors": [ + "bain" + ], + "contact": { + "website": "https://bain.cz/" + }, + "license": "MIT", + "icon": "assets/goauth/icon.png", + "environment": "*", + "entrypoints": { + "main": [ + "cz.bain.plugins.goauth.Goauth" + ] + }, + "mixins": [ + "goauth.mixins.json" + ], + "depends": { + "fabricloader": ">=0.12.5", + "fabric": "*", + "minecraft": "1.17.1" + } +} diff --git a/src/main/resources/goauth.mixins.json b/src/main/resources/goauth.mixins.json @@ -0,0 +1,19 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "cz.bain.plugins.goauth.mixin", + "compatibilityLevel": "JAVA_16", + "mixins": [ + "DimensionTypeAccessor", + "MinecraftServerMixin", + "OnPlayerConnectMixin", + "PlayerManagerAccessor", + "UserCacheMixin", + "WhitelistCommandMixin" + ], + "client": [ + ], + "injectors": { + "defaultRequire": 1 + } +}