commit 666d07e2a8c26b35a68c7c711f83d2193b27c749 Author: Andrew Steinborn Date: Tue Jul 24 14:08:55 2018 -0400 Initial commit. Very broken and only does Server List Ping! diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..1966a63dd --- /dev/null +++ b/.gitignore @@ -0,0 +1,119 @@ + +# Created by https://www.gitignore.io/api/java,gradle,intellij + +### Intellij ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +### Intellij Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +.idea/sonarlint + +### Java ### +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# 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* + +### Gradle ### +.gradle +/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties + + +# End of https://www.gitignore.io/api/java,gradle,intellij diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 000000000..bc8d0a3a6 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 000000000..94a25f7f4 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 000000000..531541017 --- /dev/null +++ b/build.gradle @@ -0,0 +1,21 @@ +plugins { + id 'java' +} + +group 'io.minimum.minecraft' +version '1.0-SNAPSHOT' + +sourceCompatibility = 1.8 + +repositories { + mavenCentral() + maven { url 'https://jitpack.io' } +} + +dependencies { + compile 'io.netty:netty-all:4.1.27.Final' + compile 'com.google.guava:guava:25.1-jre' + compile 'com.google.code.gson:gson:2.8.5' + compile 'com.github.KyoriPowered:text:v1.12-1.5.0' + testCompile group: 'junit', name: 'junit', version: '4.12' +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..01b8bf6b1 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..e89beaead --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Jul 23 20:52:34 EDT 2018 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 000000000..cccdd3d51 --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 000000000..f9553162f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 000000000..716a2c248 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'velocity' + diff --git a/src/main/java/io/minimum/minecraft/velocity/Velocity.java b/src/main/java/io/minimum/minecraft/velocity/Velocity.java new file mode 100644 index 000000000..78741b0ec --- /dev/null +++ b/src/main/java/io/minimum/minecraft/velocity/Velocity.java @@ -0,0 +1,32 @@ +package io.minimum.minecraft.velocity; + +import io.minimum.minecraft.velocity.protocol.ProtocolConstants; +import io.minimum.minecraft.velocity.protocol.netty.*; +import io.minimum.minecraft.velocity.proxy.MinecraftClientSessionHandler; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; + +public class Velocity { + public static void main(String... args) throws InterruptedException { + new ServerBootstrap() + .channel(NioServerSocketChannel.class) + .group(new NioEventLoopGroup()) + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) throws Exception { + ch.pipeline().addLast("legacy-ping-decode", new LegacyPingDecoder()); + ch.pipeline().addLast("frame-decoder", new MinecraftVarintFrameDecoder()); + ch.pipeline().addLast("legacy-ping-encode", new LegacyPingEncoder()); + ch.pipeline().addLast("frame-encoder", new MinecraftVarintLengthEncoder()); + ch.pipeline().addLast("minecraft-decoder", new MinecraftDecoder(ProtocolConstants.Direction.TO_SERVER)); + ch.pipeline().addLast("minecraft-encoder", new MinecraftEncoder(ProtocolConstants.Direction.TO_CLIENT)); + ch.pipeline().addLast("handler", new MinecraftClientSessionHandler()); + } + }) + .bind(26671) + .await(); + } +} diff --git a/src/main/java/io/minimum/minecraft/velocity/data/ServerPing.java b/src/main/java/io/minimum/minecraft/velocity/data/ServerPing.java new file mode 100644 index 000000000..6c6314dd1 --- /dev/null +++ b/src/main/java/io/minimum/minecraft/velocity/data/ServerPing.java @@ -0,0 +1,95 @@ +package io.minimum.minecraft.velocity.data; + +import net.kyori.text.Component; + +public class ServerPing { + private final Version version; + private final Players players; + private final Component description; + private final String favicon; + + public ServerPing(Version version, Players players, Component description, String favicon) { + this.version = version; + this.players = players; + this.description = description; + this.favicon = favicon; + } + + public Version getVersion() { + return version; + } + + public Players getPlayers() { + return players; + } + + public Component getDescription() { + return description; + } + + public String getFavicon() { + return favicon; + } + + @Override + public String toString() { + return "ServerPing{" + + "version=" + version + + ", players=" + players + + ", description=" + description + + ", favicon='" + favicon + '\'' + + '}'; + } + + public static class Version { + private final int protocol; + private final String version; + + public Version(int protocol, String version) { + this.protocol = protocol; + this.version = version; + } + + public int getProtocol() { + return protocol; + } + + public String getVersion() { + return version; + } + + @Override + public String toString() { + return "Version{" + + "protocol=" + protocol + + ", version='" + version + '\'' + + '}'; + } + } + + public static class Players { + private final int online; + private final int max; + + public Players(int online, int max) { + this.online = online; + this.max = max; + } + + public int getOnline() { + return online; + } + + public int getMax() { + return max; + } + + @Override + public String toString() { + return "Players{" + + "online=" + online + + ", max=" + max + + '}'; + } + } +} diff --git a/src/main/java/io/minimum/minecraft/velocity/protocol/MinecraftPacket.java b/src/main/java/io/minimum/minecraft/velocity/protocol/MinecraftPacket.java new file mode 100644 index 000000000..6b6b9c8b4 --- /dev/null +++ b/src/main/java/io/minimum/minecraft/velocity/protocol/MinecraftPacket.java @@ -0,0 +1,9 @@ +package io.minimum.minecraft.velocity.protocol; + +import io.netty.buffer.ByteBuf; + +public interface MinecraftPacket { + void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion); + + void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion); +} diff --git a/src/main/java/io/minimum/minecraft/velocity/protocol/PacketWrapper.java b/src/main/java/io/minimum/minecraft/velocity/protocol/PacketWrapper.java new file mode 100644 index 000000000..bf392d4f1 --- /dev/null +++ b/src/main/java/io/minimum/minecraft/velocity/protocol/PacketWrapper.java @@ -0,0 +1,30 @@ +package io.minimum.minecraft.velocity.protocol; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; + +public class PacketWrapper { + private final MinecraftPacket packet; + private final ByteBuf buffer; + + public PacketWrapper(MinecraftPacket packet, ByteBuf buffer) { + this.packet = packet; + this.buffer = buffer; + } + + public MinecraftPacket getPacket() { + return packet; + } + + public ByteBuf getBuffer() { + return buffer; + } + + @Override + public String toString() { + return "PacketWrapper{" + + "packet=" + packet + + ", buffer=" + ByteBufUtil.hexDump(buffer) + + '}'; + } +} diff --git a/src/main/java/io/minimum/minecraft/velocity/protocol/ProtocolConstants.java b/src/main/java/io/minimum/minecraft/velocity/protocol/ProtocolConstants.java new file mode 100644 index 000000000..989975e01 --- /dev/null +++ b/src/main/java/io/minimum/minecraft/velocity/protocol/ProtocolConstants.java @@ -0,0 +1,10 @@ +package io.minimum.minecraft.velocity.protocol; + +public enum ProtocolConstants { ; + public static final int MINECRAFT_1_12 = 340; + + public enum Direction { + TO_SERVER, + TO_CLIENT + } +} diff --git a/src/main/java/io/minimum/minecraft/velocity/protocol/ProtocolUtils.java b/src/main/java/io/minimum/minecraft/velocity/protocol/ProtocolUtils.java new file mode 100644 index 000000000..2ce7c5297 --- /dev/null +++ b/src/main/java/io/minimum/minecraft/velocity/protocol/ProtocolUtils.java @@ -0,0 +1,58 @@ +package io.minimum.minecraft.velocity.protocol; + +import com.google.common.base.Preconditions; +import io.netty.buffer.ByteBuf; + +import java.nio.charset.StandardCharsets; + +public enum ProtocolUtils { ; + private static final int DEFAULT_MAX_STRING_SIZE = 1024 * 1024; // 1MB + + public static int readVarInt(ByteBuf buf) { + int numRead = 0; + int result = 0; + byte read; + do { + read = buf.readByte(); + int value = (read & 0b01111111); + result |= (value << (7 * numRead)); + + numRead++; + if (numRead > 5) { + throw new RuntimeException("VarInt is too big"); + } + } while ((read & 0b10000000) != 0); + + return result; + } + + public static void writeVarInt(ByteBuf buf, int value) { + do { + byte temp = (byte)(value & 0b01111111); + // Note: >>> means that the sign bit is shifted with the rest of the number rather than being left alone + value >>>= 7; + if (value != 0) { + temp |= 0b10000000; + } + buf.writeByte(temp); + } while (value != 0); + } + + public static String readString(ByteBuf buf) { + return readString(buf, DEFAULT_MAX_STRING_SIZE); + } + + public static String readString(ByteBuf buf, int cap) { + int length = readVarInt(buf); + Preconditions.checkArgument(length < cap, "Bad string size (got %s, maximum is %s)", length, cap); + byte[] str = new byte[length]; + buf.readBytes(str); + return new String(str, StandardCharsets.UTF_8); + } + + public static void writeString(ByteBuf buf, String str) { + byte[] asUtf8 = str.getBytes(StandardCharsets.UTF_8); + writeVarInt(buf, asUtf8.length); + buf.writeBytes(asUtf8); + } +} diff --git a/src/main/java/io/minimum/minecraft/velocity/protocol/StateRegistry.java b/src/main/java/io/minimum/minecraft/velocity/protocol/StateRegistry.java new file mode 100644 index 000000000..efad44037 --- /dev/null +++ b/src/main/java/io/minimum/minecraft/velocity/protocol/StateRegistry.java @@ -0,0 +1,63 @@ +package io.minimum.minecraft.velocity.protocol; + +import io.minimum.minecraft.velocity.protocol.packets.Handshake; +import io.minimum.minecraft.velocity.protocol.packets.Ping; +import io.minimum.minecraft.velocity.protocol.packets.StatusRequest; +import io.minimum.minecraft.velocity.protocol.packets.StatusResponse; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + +public enum StateRegistry { + HANDSHAKE { + { + TO_SERVER.register(0x00, Handshake.class, Handshake::new); + } + }, + STATUS { + { + TO_SERVER.register(0x00, StatusRequest.class, StatusRequest::new); + TO_SERVER.register(0x01, Ping.class, Ping::new); + + TO_CLIENT.register(0x00, StatusResponse.class, StatusResponse::new); + TO_CLIENT.register(0x01, Ping.class, Ping::new); + } + }; + + public final ProtocolMappings TO_CLIENT = new ProtocolMappings(ProtocolConstants.Direction.TO_CLIENT, this); + public final ProtocolMappings TO_SERVER = new ProtocolMappings(ProtocolConstants.Direction.TO_SERVER, this); + + public static class ProtocolMappings { + private final ProtocolConstants.Direction direction; + private final StateRegistry state; + private final Map> idsToSuppliers = new HashMap<>(); + private final Map, Integer> packetClassesToIds = new HashMap<>(); + + public ProtocolMappings(ProtocolConstants.Direction direction, StateRegistry state) { + this.direction = direction; + this.state = state; + } + + public void register(int id, Class clazz, Supplier packetSupplier) { + idsToSuppliers.put(id, packetSupplier); + packetClassesToIds.put(clazz, id); + } + + public MinecraftPacket createPacket(int id) { + Supplier supplier = idsToSuppliers.get(id); + if (supplier == null) { + return null; + } + return supplier.get(); + } + + public int getId(MinecraftPacket packet) { + Integer id = packetClassesToIds.get(packet.getClass()); + if (id == null) { + throw new IllegalArgumentException("Supplied packet " + packet.getClass().getName() + " doesn't have a mapping. Direction " + direction + " State " + state); + } + return id; + } + } +} diff --git a/src/main/java/io/minimum/minecraft/velocity/protocol/netty/LegacyPingDecoder.java b/src/main/java/io/minimum/minecraft/velocity/protocol/netty/LegacyPingDecoder.java new file mode 100644 index 000000000..20b9fc852 --- /dev/null +++ b/src/main/java/io/minimum/minecraft/velocity/protocol/netty/LegacyPingDecoder.java @@ -0,0 +1,26 @@ +package io.minimum.minecraft.velocity.protocol.netty; + +import io.minimum.minecraft.velocity.protocol.packets.LegacyPing; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; + +import java.util.List; + +public class LegacyPingDecoder extends ByteToMessageDecoder { + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { + if (in.readableBytes() < 2) { + return; + } + + short first = in.getUnsignedByte(in.readerIndex()); + short second = in.getUnsignedByte(in.readerIndex() + 1); + if (first == 0xfe && second == 0x01) { + in.skipBytes(in.readableBytes()); + out.add(new LegacyPing()); + } + + ctx.pipeline().remove(this); + } +} diff --git a/src/main/java/io/minimum/minecraft/velocity/protocol/netty/LegacyPingEncoder.java b/src/main/java/io/minimum/minecraft/velocity/protocol/netty/LegacyPingEncoder.java new file mode 100644 index 000000000..e3cd16f83 --- /dev/null +++ b/src/main/java/io/minimum/minecraft/velocity/protocol/netty/LegacyPingEncoder.java @@ -0,0 +1,36 @@ +package io.minimum.minecraft.velocity.protocol.netty; + +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import io.minimum.minecraft.velocity.protocol.packets.LegacyPingResponse; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +public class LegacyPingEncoder extends MessageToByteEncoder { + @Override + protected void encode(ChannelHandlerContext ctx, LegacyPingResponse msg, ByteBuf out) throws Exception { + out.writeByte(0xff); + String serializedResponse = serialize(msg); + byte[] serializedBytes = serializedResponse.getBytes(StandardCharsets.UTF_16BE); + out.writeShort(serializedBytes.length); + out.writeBytes(serializedBytes); + System.out.println(ByteBufUtil.prettyHexDump(out)); + } + + private String serialize(LegacyPingResponse response) { + List parts = ImmutableList.of( + "ยง1", + Integer.toString(response.getProtocolVersion()), + response.getServerVersion(), + response.getMotd(), + Integer.toString(response.getPlayersOnline()), + Integer.toString(response.getPlayersMax()) + ); + return Joiner.on('\0').join(parts); + } +} diff --git a/src/main/java/io/minimum/minecraft/velocity/protocol/netty/MinecraftDecoder.java b/src/main/java/io/minimum/minecraft/velocity/protocol/netty/MinecraftDecoder.java new file mode 100644 index 000000000..a7dad5d89 --- /dev/null +++ b/src/main/java/io/minimum/minecraft/velocity/protocol/netty/MinecraftDecoder.java @@ -0,0 +1,64 @@ +package io.minimum.minecraft.velocity.protocol.netty; + +import com.google.common.base.Preconditions; +import io.minimum.minecraft.velocity.protocol.*; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageDecoder; + +import java.util.List; + +public class MinecraftDecoder extends MessageToMessageDecoder { + private StateRegistry state; + private final ProtocolConstants.Direction direction; + private int protocolVersion; + + public MinecraftDecoder(ProtocolConstants.Direction direction) { + this.state = StateRegistry.HANDSHAKE; + this.direction = Preconditions.checkNotNull(direction, "direction"); + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List out) throws Exception { + if (msg.isReadable()) { + return; + } + + ByteBuf slice = msg.slice().retain(); + + int packetId = ProtocolUtils.readVarInt(msg); + StateRegistry.ProtocolMappings mappings = direction == ProtocolConstants.Direction.TO_CLIENT ? state.TO_CLIENT : state.TO_SERVER; + MinecraftPacket packet = mappings.createPacket(packetId); + System.out.println("Decode!"); + System.out.println("packet ID: " + packetId); + System.out.println("packet hexdump: " + ByteBufUtil.hexDump(slice)); + if (packet == null) { + msg.skipBytes(msg.readableBytes()); + out.add(new PacketWrapper(null, slice)); + } else { + packet.decode(msg, direction, protocolVersion); + out.add(new PacketWrapper(packet, slice)); + } + } + + public int getProtocolVersion() { + return protocolVersion; + } + + public void setProtocolVersion(int protocolVersion) { + this.protocolVersion = protocolVersion; + } + + public StateRegistry getState() { + return state; + } + + public void setState(StateRegistry state) { + this.state = state; + } + + public ProtocolConstants.Direction getDirection() { + return direction; + } +} diff --git a/src/main/java/io/minimum/minecraft/velocity/protocol/netty/MinecraftEncoder.java b/src/main/java/io/minimum/minecraft/velocity/protocol/netty/MinecraftEncoder.java new file mode 100644 index 000000000..9d665b58a --- /dev/null +++ b/src/main/java/io/minimum/minecraft/velocity/protocol/netty/MinecraftEncoder.java @@ -0,0 +1,46 @@ +package io.minimum.minecraft.velocity.protocol.netty; + +import com.google.common.base.Preconditions; +import io.minimum.minecraft.velocity.protocol.*; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; + +public class MinecraftEncoder extends MessageToByteEncoder { + private StateRegistry state; + private final ProtocolConstants.Direction direction; + private int protocolVersion; + + public MinecraftEncoder(ProtocolConstants.Direction direction) { + this.state = StateRegistry.HANDSHAKE; + this.direction = Preconditions.checkNotNull(direction, "direction"); + } + + @Override + protected void encode(ChannelHandlerContext ctx, MinecraftPacket msg, ByteBuf out) throws Exception { + StateRegistry.ProtocolMappings mappings = direction == ProtocolConstants.Direction.TO_CLIENT ? state.TO_CLIENT : state.TO_SERVER; + int packetId = mappings.getId(msg); + ProtocolUtils.writeVarInt(out, packetId); + msg.encode(out, direction, protocolVersion); + } + + public int getProtocolVersion() { + return protocolVersion; + } + + public void setProtocolVersion(int protocolVersion) { + this.protocolVersion = protocolVersion; + } + + public StateRegistry getState() { + return state; + } + + public void setState(StateRegistry state) { + this.state = state; + } + + public ProtocolConstants.Direction getDirection() { + return direction; + } +} diff --git a/src/main/java/io/minimum/minecraft/velocity/protocol/netty/MinecraftVarintFrameDecoder.java b/src/main/java/io/minimum/minecraft/velocity/protocol/netty/MinecraftVarintFrameDecoder.java new file mode 100644 index 000000000..964a94c3a --- /dev/null +++ b/src/main/java/io/minimum/minecraft/velocity/protocol/netty/MinecraftVarintFrameDecoder.java @@ -0,0 +1,28 @@ +package io.minimum.minecraft.velocity.protocol.netty; + +import io.minimum.minecraft.velocity.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; + +import java.util.List; + +public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder { + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { + if (in.readableBytes() < 1) { + return; + } + + in.markReaderIndex(); + int packetLength = ProtocolUtils.readVarInt(in); + if (in.readableBytes() < packetLength) { + in.resetReaderIndex(); + return; + } + + System.out.println("Got a varint-prefixed packet length " + packetLength); + out.add(in.slice(in.readerIndex(), packetLength).retain()); + in.skipBytes(packetLength); + } +} diff --git a/src/main/java/io/minimum/minecraft/velocity/protocol/netty/MinecraftVarintLengthEncoder.java b/src/main/java/io/minimum/minecraft/velocity/protocol/netty/MinecraftVarintLengthEncoder.java new file mode 100644 index 000000000..4d9e8a4c2 --- /dev/null +++ b/src/main/java/io/minimum/minecraft/velocity/protocol/netty/MinecraftVarintLengthEncoder.java @@ -0,0 +1,14 @@ +package io.minimum.minecraft.velocity.protocol.netty; + +import io.minimum.minecraft.velocity.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; + +public class MinecraftVarintLengthEncoder extends MessageToByteEncoder { + @Override + protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throws Exception { + ProtocolUtils.writeVarInt(out, msg.readableBytes()); + out.writeBytes(msg); + } +} diff --git a/src/main/java/io/minimum/minecraft/velocity/protocol/packets/Handshake.java b/src/main/java/io/minimum/minecraft/velocity/protocol/packets/Handshake.java new file mode 100644 index 000000000..563e5909e --- /dev/null +++ b/src/main/java/io/minimum/minecraft/velocity/protocol/packets/Handshake.java @@ -0,0 +1,71 @@ +package io.minimum.minecraft.velocity.protocol.packets; + +import io.minimum.minecraft.velocity.protocol.MinecraftPacket; +import io.minimum.minecraft.velocity.protocol.ProtocolConstants; +import io.minimum.minecraft.velocity.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; + +public class Handshake implements MinecraftPacket { + private int protocolVersion; + private String serverAddress; + private int port; + private int nextStatus; + + public int getProtocolVersion() { + return protocolVersion; + } + + public void setProtocolVersion(int protocolVersion) { + this.protocolVersion = protocolVersion; + } + + public String getServerAddress() { + return serverAddress; + } + + public void setServerAddress(String serverAddress) { + this.serverAddress = serverAddress; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public int getNextStatus() { + return nextStatus; + } + + public void setNextStatus(int nextStatus) { + this.nextStatus = nextStatus; + } + + @Override + public String toString() { + return "Handshake{" + + "protocolVersion=" + protocolVersion + + ", serverAddress='" + serverAddress + '\'' + + ", port=" + port + + ", nextStatus=" + nextStatus + + '}'; + } + + @Override + public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) { + protocolVersion = ProtocolUtils.readVarInt(buf); + serverAddress = ProtocolUtils.readString(buf, 255); + port = buf.readUnsignedShort(); + nextStatus = ProtocolUtils.readVarInt(buf); + } + + @Override + public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) { + ProtocolUtils.writeVarInt(buf, protocolVersion); + ProtocolUtils.writeString(buf, serverAddress); + buf.writeShort(port); + ProtocolUtils.writeVarInt(buf, nextStatus); + } +} diff --git a/src/main/java/io/minimum/minecraft/velocity/protocol/packets/LegacyPing.java b/src/main/java/io/minimum/minecraft/velocity/protocol/packets/LegacyPing.java new file mode 100644 index 000000000..5b15db7e7 --- /dev/null +++ b/src/main/java/io/minimum/minecraft/velocity/protocol/packets/LegacyPing.java @@ -0,0 +1,4 @@ +package io.minimum.minecraft.velocity.protocol.packets; + +public class LegacyPing { +} diff --git a/src/main/java/io/minimum/minecraft/velocity/protocol/packets/LegacyPingResponse.java b/src/main/java/io/minimum/minecraft/velocity/protocol/packets/LegacyPingResponse.java new file mode 100644 index 000000000..29347dd35 --- /dev/null +++ b/src/main/java/io/minimum/minecraft/velocity/protocol/packets/LegacyPingResponse.java @@ -0,0 +1,82 @@ +package io.minimum.minecraft.velocity.protocol.packets; + +import io.minimum.minecraft.velocity.data.ServerPing; +import net.kyori.text.serializer.ComponentSerializers; + +public class LegacyPingResponse { + private int protocolVersion; + private String serverVersion; + private String motd; + private int playersOnline; + private int playersMax; + + public LegacyPingResponse() { + } + + public LegacyPingResponse(int protocolVersion, String serverVersion, String motd, int playersOnline, int playersMax) { + this.protocolVersion = protocolVersion; + this.serverVersion = serverVersion; + this.motd = motd; + this.playersOnline = playersOnline; + this.playersMax = playersMax; + } + + public int getProtocolVersion() { + return protocolVersion; + } + + public void setProtocolVersion(int protocolVersion) { + this.protocolVersion = protocolVersion; + } + + public String getServerVersion() { + return serverVersion; + } + + public void setServerVersion(String serverVersion) { + this.serverVersion = serverVersion; + } + + public String getMotd() { + return motd; + } + + public void setMotd(String motd) { + this.motd = motd; + } + + public int getPlayersOnline() { + return playersOnline; + } + + public void setPlayersOnline(int playersOnline) { + this.playersOnline = playersOnline; + } + + public int getPlayersMax() { + return playersMax; + } + + public void setPlayersMax(int playersMax) { + this.playersMax = playersMax; + } + + @Override + public String toString() { + return "LegacyPingResponse{" + + "protocolVersion=" + protocolVersion + + ", serverVersion='" + serverVersion + '\'' + + ", motd='" + motd + '\'' + + ", playersOnline=" + playersOnline + + ", playersMax=" + playersMax + + '}'; + } + + public static LegacyPingResponse from(ServerPing ping) { + return new LegacyPingResponse(ping.getVersion().getProtocol(), + ping.getVersion().getVersion(), + ComponentSerializers.LEGACY.serialize(ping.getDescription()), + ping.getPlayers().getOnline(), + ping.getPlayers().getMax()); + } +} diff --git a/src/main/java/io/minimum/minecraft/velocity/protocol/packets/Ping.java b/src/main/java/io/minimum/minecraft/velocity/protocol/packets/Ping.java new file mode 100644 index 000000000..9b13c96b6 --- /dev/null +++ b/src/main/java/io/minimum/minecraft/velocity/protocol/packets/Ping.java @@ -0,0 +1,34 @@ +package io.minimum.minecraft.velocity.protocol.packets; + +import io.minimum.minecraft.velocity.protocol.MinecraftPacket; +import io.minimum.minecraft.velocity.protocol.ProtocolConstants; +import io.netty.buffer.ByteBuf; + +public class Ping implements MinecraftPacket { + private long randomId; + + public long getRandomId() { + return randomId; + } + + public void setRandomId(long randomId) { + this.randomId = randomId; + } + + @Override + public String toString() { + return "Ping{" + + "randomId=" + randomId + + '}'; + } + + @Override + public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) { + randomId = buf.readLong(); + } + + @Override + public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) { + buf.writeLong(randomId); + } +} diff --git a/src/main/java/io/minimum/minecraft/velocity/protocol/packets/StatusRequest.java b/src/main/java/io/minimum/minecraft/velocity/protocol/packets/StatusRequest.java new file mode 100644 index 000000000..cb7d01263 --- /dev/null +++ b/src/main/java/io/minimum/minecraft/velocity/protocol/packets/StatusRequest.java @@ -0,0 +1,17 @@ +package io.minimum.minecraft.velocity.protocol.packets; + +import io.minimum.minecraft.velocity.protocol.MinecraftPacket; +import io.minimum.minecraft.velocity.protocol.ProtocolConstants; +import io.netty.buffer.ByteBuf; + +public class StatusRequest implements MinecraftPacket { + @Override + public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) { + + } + + @Override + public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) { + + } +} diff --git a/src/main/java/io/minimum/minecraft/velocity/protocol/packets/StatusResponse.java b/src/main/java/io/minimum/minecraft/velocity/protocol/packets/StatusResponse.java new file mode 100644 index 000000000..20b364705 --- /dev/null +++ b/src/main/java/io/minimum/minecraft/velocity/protocol/packets/StatusResponse.java @@ -0,0 +1,35 @@ +package io.minimum.minecraft.velocity.protocol.packets; + +import io.minimum.minecraft.velocity.protocol.MinecraftPacket; +import io.minimum.minecraft.velocity.protocol.ProtocolConstants; +import io.minimum.minecraft.velocity.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; + +public class StatusResponse implements MinecraftPacket { + private String status; + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + @Override + public String toString() { + return "StatusResponse{" + + "status='" + status + '\'' + + '}'; + } + + @Override + public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) { + status = ProtocolUtils.readString(buf, Short.MAX_VALUE); + } + + @Override + public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) { + ProtocolUtils.writeString(buf, status); + } +} diff --git a/src/main/java/io/minimum/minecraft/velocity/proxy/MinecraftClientSessionHandler.java b/src/main/java/io/minimum/minecraft/velocity/proxy/MinecraftClientSessionHandler.java new file mode 100644 index 000000000..afa9fa0cb --- /dev/null +++ b/src/main/java/io/minimum/minecraft/velocity/proxy/MinecraftClientSessionHandler.java @@ -0,0 +1,93 @@ +package io.minimum.minecraft.velocity.proxy; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import io.minimum.minecraft.velocity.data.ServerPing; +import io.minimum.minecraft.velocity.protocol.MinecraftPacket; +import io.minimum.minecraft.velocity.protocol.PacketWrapper; +import io.minimum.minecraft.velocity.protocol.StateRegistry; +import io.minimum.minecraft.velocity.protocol.netty.MinecraftDecoder; +import io.minimum.minecraft.velocity.protocol.netty.MinecraftEncoder; +import io.minimum.minecraft.velocity.protocol.packets.*; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.SimpleChannelInboundHandler; +import net.kyori.text.Component; +import net.kyori.text.TextComponent; +import net.kyori.text.serializer.GsonComponentSerializer; + +public class MinecraftClientSessionHandler extends ChannelInboundHandlerAdapter { + private static final Gson GSON = new GsonBuilder() + .registerTypeHierarchyAdapter(Component.class, new GsonComponentSerializer()) + .create(); + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof PacketWrapper) { + try { + handle(ctx, (PacketWrapper) msg); + } finally { + ((PacketWrapper) msg).getBuffer().release(); + } + } + + if (msg instanceof LegacyPing) { + System.out.println("Got LEGACY status request!"); + ServerPing ping = new ServerPing( + new ServerPing.Version(340, "1.12"), + new ServerPing.Players(0, 0), + TextComponent.of("this is a test"), + null + ); + LegacyPingResponse response = LegacyPingResponse.from(ping); + ctx.writeAndFlush(response); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + cause.printStackTrace(); + ctx.close(); + } + + private void handle(ChannelHandlerContext ctx, PacketWrapper msg) { + MinecraftPacket packet = msg.getPacket(); + if (packet == null) { + System.out.println("no packet!"); + return; + } + + if (packet instanceof Handshake) { + System.out.println("Handshake: " + packet); + switch (((Handshake) packet).getNextStatus()) { + case 1: + // status + ctx.pipeline().get(MinecraftDecoder.class).setState(StateRegistry.STATUS); + ctx.pipeline().get(MinecraftEncoder.class).setState(StateRegistry.STATUS); + break; + case 2: + // login + throw new UnsupportedOperationException("Login not supported yet"); + } + } + + if (packet instanceof StatusRequest) { + System.out.println("Got status request!"); + ServerPing ping = new ServerPing( + new ServerPing.Version(340, "1.12.2"), + new ServerPing.Players(0, 0), + TextComponent.of("test"), + null + ); + StatusResponse response = new StatusResponse(); + response.setStatus(GSON.toJson(ping)); + ctx.writeAndFlush(response, ctx.voidPromise()); + } + + if (packet instanceof Ping) { + System.out.println("Ping: " + packet); + ctx.writeAndFlush(packet).addListener(ChannelFutureListener.CLOSE); + } + } +}