From fb29519b22e4f22705652a8ef7cf1cca1881ca93 Mon Sep 17 00:00:00 2001 From: creeper123123321 <7974274+creeper123123321@users.noreply.github.com> Date: Wed, 10 Feb 2021 10:27:06 -0300 Subject: [PATCH] separate class files --- build.gradle.kts | 5 + .../creeper123123321/viaaas/CloudCodec.kt | 231 -------- .../creeper123123321/viaaas/CloudHandler.kt | 529 ------------------ .../creeper123123321/viaaas/CloudPackets.kt | 336 ----------- .../creeper123123321/viaaas/CloudPlatform.kt | 198 ------- .../github/creeper123123321/viaaas/Util.kt | 135 +++++ .../github/creeper123123321/viaaas/VIAaaS.kt | 244 +------- .../creeper123123321/viaaas/VIAaaSAddress.kt | 71 +++ .../github/creeper123123321/viaaas/ViaWeb.kt | 253 --------- .../viaaas/codec/CompressionCodec.kt | 75 +++ .../viaaas/codec/CryptoCodec.kt | 22 + .../viaaas/codec/FrameCodec.kt | 43 ++ .../viaaas/codec/MinecraftCodec.kt | 36 ++ .../viaaas/command/CloudCommands.kt | 5 + .../viaaas/command/VIAaaSConsole.kt | 116 ++++ .../viaaas/handler/BackEndInit.kt | 24 + .../viaaas/handler/CloudMinecraftHandler.kt | 51 ++ .../viaaas/handler/CloudViaCodec.kt | 40 ++ .../viaaas/handler/ConnectionData.kt | 19 + .../viaaas/handler/FrontEndInit.kt | 26 + .../creeper123123321/viaaas/handler/Util.kt | 9 + .../viaaas/handler/state/HandshakeState.kt | 118 ++++ .../viaaas/handler/state/LoginState.kt | 210 +++++++ .../handler/state/MinecraftConnectionState.kt | 23 + .../viaaas/handler/state/PlayState.kt | 23 + .../viaaas/handler/state/StatusState.kt | 30 + .../creeper123123321/viaaas/packet/Packet.kt | 11 + .../viaaas/packet/PacketRegistry.kt | 90 +++ .../viaaas/packet/UnknownPacket.kt | 15 + .../viaaas/packet/handshake/Handshake.kt | 28 + .../viaaas/packet/login/CryptoRequest.kt | 42 ++ .../viaaas/packet/login/CryptoResponse.kt | 33 ++ .../viaaas/packet/login/LoginDisconnect.kt | 16 + .../viaaas/packet/login/LoginStart.kt | 17 + .../viaaas/packet/login/LoginSuccess.kt | 39 ++ .../viaaas/packet/login/PluginRequest.kt | 23 + .../viaaas/packet/login/PluginResponse.kt | 27 + .../viaaas/packet/login/SetCompression.kt | 17 + .../viaaas/packet/status/StatusPing.kt | 16 + .../viaaas/packet/status/StatusPong.kt | 17 + .../viaaas/packet/status/StatusRequest.kt | 12 + .../viaaas/packet/status/StatusResponse.kt | 16 + .../viaaas/platform/CloudBackwards.kt | 15 + .../viaaas/platform/CloudBossBar.kt | 8 + .../viaaas/platform/CloudInjector.kt | 19 + .../viaaas/platform/CloudLoader.kt | 18 + .../viaaas/platform/CloudPlatform.kt | 69 +++ .../viaaas/platform/CloudRewind.kt | 11 + .../viaaas/platform/CloudTask.kt | 8 + .../viaaas/platform/CloudViaAPI.kt | 22 + .../viaaas/provider/CloudVersionProvider.kt | 13 + .../creeper123123321/viaaas/web/ViaWebApp.kt | 50 ++ .../creeper123123321/viaaas/web/WebClient.kt | 11 + .../viaaas/web/WebDashboardServer.kt | 95 ++++ .../creeper123123321/viaaas/web/WebLogin.kt | 97 ++++ .../creeper123123321/viaaas/web/WebState.kt | 8 + 56 files changed, 1960 insertions(+), 1775 deletions(-) delete mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/CloudCodec.kt delete mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/CloudHandler.kt delete mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/CloudPackets.kt delete mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/CloudPlatform.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/Util.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/VIAaaSAddress.kt delete mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/ViaWeb.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/codec/CompressionCodec.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/codec/CryptoCodec.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/codec/FrameCodec.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/codec/MinecraftCodec.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/command/CloudCommands.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/command/VIAaaSConsole.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/handler/BackEndInit.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/handler/CloudMinecraftHandler.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/handler/CloudViaCodec.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/handler/ConnectionData.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/handler/FrontEndInit.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/handler/Util.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/handler/state/HandshakeState.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/handler/state/LoginState.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/handler/state/MinecraftConnectionState.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/handler/state/PlayState.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/handler/state/StatusState.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/packet/Packet.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/packet/PacketRegistry.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/packet/UnknownPacket.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/packet/handshake/Handshake.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/packet/login/CryptoRequest.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/packet/login/CryptoResponse.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/packet/login/LoginDisconnect.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/packet/login/LoginStart.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/packet/login/LoginSuccess.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/packet/login/PluginRequest.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/packet/login/PluginResponse.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/packet/login/SetCompression.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/packet/status/StatusPing.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/packet/status/StatusPong.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/packet/status/StatusRequest.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/packet/status/StatusResponse.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/platform/CloudBackwards.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/platform/CloudBossBar.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/platform/CloudInjector.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/platform/CloudLoader.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/platform/CloudPlatform.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/platform/CloudRewind.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/platform/CloudTask.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/platform/CloudViaAPI.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/provider/CloudVersionProvider.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/web/ViaWebApp.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/web/WebClient.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/web/WebDashboardServer.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/web/WebLogin.kt create mode 100644 src/main/kotlin/com/github/creeper123123321/viaaas/web/WebState.kt diff --git a/build.gradle.kts b/build.gradle.kts index 26625d9..44e5bea 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -53,8 +53,13 @@ dependencies { implementation(kotlin("stdlib-jdk8")) implementation("io.ktor:ktor-network-tls-certificates:$ktorVersion") implementation("io.ktor:ktor-server-netty:$ktorVersion") + implementation("io.ktor:ktor-client-core:$ktorVersion") + implementation("io.ktor:ktor-client-core-jvm:$ktorVersion") implementation("io.ktor:ktor-client-cio:$ktorVersion") + implementation("io.ktor:ktor-client-logging-jvm:$ktorVersion") + implementation("io.ktor:ktor-server-core:$ktorVersion") implementation("io.ktor:ktor-client-gson:$ktorVersion") + implementation("io.ktor:ktor-server-host-common:$ktorVersion") implementation("io.ktor:ktor-websockets:$ktorVersion") testImplementation("io.ktor:ktor-server-test-host:$ktorVersion") } diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/CloudCodec.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/CloudCodec.kt deleted file mode 100644 index 2183bd0..0000000 --- a/src/main/kotlin/com/github/creeper123123321/viaaas/CloudCodec.kt +++ /dev/null @@ -1,231 +0,0 @@ -package com.github.creeper123123321.viaaas - -import io.netty.buffer.ByteBuf -import io.netty.buffer.ByteBufAllocator -import io.netty.channel.Channel -import io.netty.channel.ChannelHandlerContext -import io.netty.channel.ChannelInitializer -import io.netty.handler.codec.ByteToMessageCodec -import io.netty.handler.codec.DecoderException -import io.netty.handler.codec.MessageToMessageCodec -import io.netty.handler.flow.FlowControlHandler -import io.netty.handler.timeout.ReadTimeoutHandler -import us.myles.ViaVersion.api.data.UserConnection -import us.myles.ViaVersion.api.protocol.ProtocolPipeline -import us.myles.ViaVersion.api.type.Type -import us.myles.ViaVersion.exception.CancelDecoderException -import us.myles.ViaVersion.exception.CancelEncoderException -import java.util.concurrent.TimeUnit -import java.util.zip.Deflater -import java.util.zip.Inflater -import javax.crypto.Cipher - -object FrontChannelInit : ChannelInitializer() { - override fun initChannel(ch: Channel) { - ch.pipeline() - .addLast("timeout", ReadTimeoutHandler(30, TimeUnit.SECONDS)) - // "crypto" - .addLast("frame", FrameCodec()) - // "compress" - .addLast("flow-handler", FlowControlHandler()) - .addLast("mc", CloudMinecraftCodec()) - .addLast( - "handler", CloudMinecraftHandler( - ConnectionData(frontChannel = ch), other = null, frontEnd = true - ) - ) - } -} - -class BackendInit(val connectionData: ConnectionData) : ChannelInitializer() { - override fun initChannel(ch: Channel) { - val user = UserConnection(ch, true) - ProtocolPipeline(user) - ch.pipeline().addLast("timeout", ReadTimeoutHandler(30, TimeUnit.SECONDS)) - // "crypto" - .addLast("frame", FrameCodec()) - // compress - .addLast("via-codec", CloudViaCodec(user)) - .addLast("mc", CloudMinecraftCodec()) - .addLast("handler", CloudMinecraftHandler(connectionData, connectionData.frontChannel, frontEnd = false)) - } -} - -class CloudMinecraftCodec : MessageToMessageCodec() { - override fun encode(ctx: ChannelHandlerContext, msg: Packet, out: MutableList) { - if (!ctx.channel().isActive) return - val buf = ByteBufAllocator.DEFAULT.buffer() - try { - val handler = ctx.pipeline().get(CloudMinecraftHandler::class.java) - PacketRegistry.encode(msg, buf, handler.data.frontVer!!) - out.add(buf.retain()) - } finally { - buf.release() - } - } - - override fun decode(ctx: ChannelHandlerContext, msg: ByteBuf, out: MutableList) { - if (!ctx.channel().isActive || !msg.isReadable) return - val handler = ctx.pipeline().get(CloudMinecraftHandler::class.java) - out.add( - PacketRegistry.decode( - msg, - handler.data.frontVer ?: 0, - handler.data.state.state, handler.frontEnd - ) - ) - if (msg.isReadable) throw IllegalStateException("Remaining bytes!!!") - } -} - -class CloudCrypto(val cipherDecode: Cipher, var cipherEncode: Cipher) : MessageToMessageCodec() { - override fun decode(ctx: ChannelHandlerContext, msg: ByteBuf, out: MutableList) { - val i = msg.readerIndex() - val size = msg.readableBytes() - msg.writerIndex(i + cipherDecode.update(msg.nioBuffer(), msg.nioBuffer(i, cipherDecode.getOutputSize(size)))) - out.add(msg.retain()) - } - - override fun encode(ctx: ChannelHandlerContext, msg: ByteBuf, out: MutableList) { - val i = msg.readerIndex() - val size = msg.readableBytes() - msg.writerIndex(i + cipherEncode.update(msg.nioBuffer(), msg.nioBuffer(i, cipherEncode.getOutputSize(size)))) - out.add(msg.retain()) - } -} - -class CloudCompressionCodec(val threshold: Int) : MessageToMessageCodec() { - // https://github.com/Gerrygames/ClientViaVersion/blob/master/src/main/java/de/gerrygames/the5zig/clientviaversion/netty/CompressionEncoder.java - private val inflater: Inflater = - Inflater()// https://github.com/Gerrygames/ClientViaVersion/blob/master/src/main/java/de/gerrygames/the5zig/clientviaversion/netty/CompressionEncoder.java - private val deflater: Deflater = Deflater() - - @Throws(Exception::class) - override fun encode(ctx: ChannelHandlerContext, input: ByteBuf, out: MutableList) { - val frameLength = input.readableBytes() - val outBuf = ctx.alloc().buffer() - try { - if (frameLength < threshold) { - outBuf.writeByte(0) - outBuf.writeBytes(input) - out.add(outBuf.retain()) - return - } - Type.VAR_INT.writePrimitive(outBuf, frameLength) - deflater.setInput(input.nioBuffer()) - deflater.finish() - while (!deflater.finished()) { - outBuf.ensureWritable(8192) - val wIndex = outBuf.writerIndex() - outBuf.writerIndex(wIndex + deflater.deflate(outBuf.nioBuffer(wIndex, outBuf.writableBytes()))) - } - out.add(outBuf.retain()) - } finally { - outBuf.release() - deflater.reset() - } - } - - @Throws(Exception::class) - override fun decode(ctx: ChannelHandlerContext, input: ByteBuf, out: MutableList) { - if (input.isReadable) { - val outLength = Type.VAR_INT.readPrimitive(input) - if (outLength == 0) { - out.add(input.retain()) - return - } - - if (outLength < threshold) { - throw DecoderException("Badly compressed packet - size of $outLength is below server threshold of $threshold") - } - if (outLength > 2097152) { - throw DecoderException("Badly compressed packet - size of $outLength is larger than protocol maximum of 2097152") - } - - inflater.setInput(input.nioBuffer()) - val output = ctx.alloc().buffer(outLength, outLength) - try { - output.writerIndex( - output.writerIndex() + inflater.inflate( - output.nioBuffer(output.writerIndex(), output.writableBytes()) - ) - ) - out.add(output.retain()) - } finally { - inflater.reset() - output.release() - } - } - } - -} - -val badLength = DecoderException("Invalid length!") - -class FrameCodec : ByteToMessageCodec() { - override fun decode(ctx: ChannelHandlerContext, input: ByteBuf, out: MutableList) { - if (!ctx.channel().isActive) { - input.clear() // Ignore, should prevent DoS https://github.com/SpigotMC/BungeeCord/pull/2908 - return - } - - val index = input.readerIndex() - var nByte = 0 - val result = input.forEachByte { - nByte++ - val hasNext = it.toInt().and(0x10000000) != 0 - if (nByte > 3) throw badLength - hasNext - } - input.readerIndex(index) - if (result == -1) return // not readable - - val length = Type.VAR_INT.readPrimitive(input) - - if (length >= 2097152 || length < 0) throw badLength - if (!input.isReadable(length)) { - input.readerIndex(index) - return - } - - out.add(input.readRetainedSlice(length)) - } - - override fun encode(ctx: ChannelHandlerContext, msg: ByteBuf, out: ByteBuf) { - if (msg.readableBytes() >= 2097152) throw badLength - Type.VAR_INT.writePrimitive(out, msg.readableBytes()) - out.writeBytes(msg) - } -} - -class CloudViaCodec(val info: UserConnection) : MessageToMessageCodec() { - override fun decode(ctx: ChannelHandlerContext, bytebuf: ByteBuf, out: MutableList) { - if (!info.checkIncomingPacket()) throw CancelDecoderException.generate(null) - if (!info.shouldTransformPacket()) { - out.add(bytebuf.retain()) - return - } - val transformedBuf: ByteBuf = ctx.alloc().buffer().writeBytes(bytebuf) - try { - info.transformIncoming(transformedBuf, CancelDecoderException::generate) - out.add(transformedBuf.retain()) - } finally { - transformedBuf.release() - } - } - - override fun encode(ctx: ChannelHandlerContext, bytebuf: ByteBuf, out: MutableList) { - if (!info.checkOutgoingPacket()) throw CancelEncoderException.generate(null) - if (!info.shouldTransformPacket()) { - out.add(bytebuf.retain()) - return - } - val transformedBuf: ByteBuf = ctx.alloc().buffer().writeBytes(bytebuf) - try { - info.transformOutgoing(transformedBuf, CancelEncoderException::generate) - out.add(transformedBuf.retain()) - } finally { - transformedBuf.release() - } - } -} diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/CloudHandler.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/CloudHandler.kt deleted file mode 100644 index e244ba8..0000000 --- a/src/main/kotlin/com/github/creeper123123321/viaaas/CloudHandler.kt +++ /dev/null @@ -1,529 +0,0 @@ -package com.github.creeper123123321.viaaas - -import com.google.common.net.UrlEscapers -import com.google.common.primitives.Ints -import com.google.gson.Gson -import com.google.gson.JsonObject -import io.ktor.client.request.* -import io.netty.bootstrap.Bootstrap -import io.netty.channel.* -import io.netty.channel.socket.SocketChannel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import org.slf4j.LoggerFactory -import us.myles.ViaVersion.exception.CancelCodecException -import us.myles.ViaVersion.packets.State -import java.math.BigInteger -import java.net.InetAddress -import java.net.InetSocketAddress -import java.net.SocketAddress -import java.security.MessageDigest -import java.security.PrivateKey -import java.security.PublicKey -import java.util.* -import java.util.UUID -import java.util.concurrent.CompletableFuture -import javax.crypto.Cipher -import javax.crypto.spec.IvParameterSpec -import javax.crypto.spec.SecretKeySpec -import javax.naming.NameNotFoundException -import javax.naming.directory.InitialDirContext - - -val mcLogger = LoggerFactory.getLogger("VIAaaS MC") - -class ConnectionData( - val frontChannel: Channel, - var backChannel: Channel? = null, - var state: MinecraftConnectionState = HandshakeState(), - var frontOnline: Boolean? = null, // todo - var frontName: String? = null, - var backName: String? = null, - var frontVer: Int? = null, - var backVer: Int? = null, -) { - val frontHandler get() = frontChannel.pipeline().get(CloudMinecraftHandler::class.java) - val backHandler get() = backChannel?.pipeline()?.get(CloudMinecraftHandler::class.java) -} - -class CloudMinecraftHandler( - val data: ConnectionData, - var other: Channel?, - val frontEnd: Boolean -) : SimpleChannelInboundHandler() { - var remoteAddress: SocketAddress? = null - - override fun channelRead0(ctx: ChannelHandlerContext, packet: Packet) { - if (ctx.channel().isActive) { - data.state.handlePacket(this, ctx, packet) - } - } - - override fun channelActive(ctx: ChannelHandlerContext) { - remoteAddress = ctx.channel().remoteAddress() - } - - override fun channelInactive(ctx: ChannelHandlerContext) { - other?.close() - data.state.onInactivated(this) - } - - override fun channelReadComplete(ctx: ChannelHandlerContext?) { - other?.flush() - } - - override fun channelWritabilityChanged(ctx: ChannelHandlerContext) { - other?.setAutoRead(ctx.channel().isWritable) - } - - override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { - if (cause is CancelCodecException) return - mcLogger.debug("Exception: ", cause) - disconnect("Exception: $cause") - } - - fun disconnect(s: String) { - data.state.disconnect(this, s) - } -} - -interface MinecraftConnectionState { - val state: State - fun handlePacket( - handler: CloudMinecraftHandler, ctx: ChannelHandlerContext, - packet: Packet - ) - - fun disconnect(handler: CloudMinecraftHandler, msg: String) { - mcLogger.info("Disconnected ${handler.remoteAddress}: $msg") - } - - fun onInactivated(handler: CloudMinecraftHandler) { - mcLogger.info(handler.remoteAddress?.toString() + " inactivated") - } -} - -class HandshakeState : MinecraftConnectionState { - fun connectBack(handler: CloudMinecraftHandler, socketAddr: InetSocketAddress): ChannelFuture { - return Bootstrap() - .handler(BackendInit(handler.data)) - .channelFactory(channelSocketFactory()) - .group(handler.data.frontChannel.eventLoop()) - .option(ChannelOption.IP_TOS, 0x18) - .option(ChannelOption.TCP_NODELAY, true) - .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 15_000) // Half of mc timeout - .connect(socketAddr) - } - - override val state: State - get() = State.HANDSHAKE - - override fun handlePacket(handler: CloudMinecraftHandler, ctx: ChannelHandlerContext, packet: Packet) { - if (packet !is HandshakePacket) throw IllegalArgumentException("Invalid packet!") - - handler.data.frontVer = packet.protocolId - when (packet.nextState.ordinal) { - 1 -> handler.data.state = StatusState - 2 -> handler.data.state = LoginState() - else -> throw IllegalStateException("Invalid next state") - } - - val parsed = VIAaaSAddress().parse(packet.address.substringBefore(0.toChar()), VIAaaSConfig.hostName) - val backProto = parsed.protocol ?: 47 // todo autodetection - val hadHostname = parsed.viaSuffix != null - - packet.address = parsed.serverAddress!! - packet.port = parsed.port ?: if (VIAaaSConfig.defaultBackendPort == -1) { - packet.port - } else { - VIAaaSConfig.defaultBackendPort - } - - handler.data.backVer = backProto - handler.data.frontOnline = parsed.online - handler.data.backName = parsed.username - - val playerAddr = handler.data.frontHandler.remoteAddress - mcLogger.info("Connecting $playerAddr (${handler.data.frontVer}) -> ${packet.address}:${packet.port} ($backProto)") - - if (!hadHostname && VIAaaSConfig.requireHostName) { - throw UnsupportedOperationException("This VIAaaS instance requires you to use the hostname") - } - - handler.data.frontChannel.setAutoRead(false) - GlobalScope.launch(Dispatchers.IO) { - try { - val srvResolved = resolveSrv(packet.address, packet.port) - packet.address = srvResolved.first - packet.port = srvResolved.second - - val socketAddr = InetSocketAddress(InetAddress.getByName(packet.address), packet.port) - - if (checkLocalAddress(socketAddr.address) - || matchesAddress(socketAddr, VIAaaSConfig.blockedBackAddresses) - || !matchesAddress(socketAddr, VIAaaSConfig.allowedBackAddresses) - ) { - throw SecurityException("Not allowed") - } - - val future = connectBack(handler, socketAddr) - - future.addListener { - if (it.isSuccess) { - mcLogger.info("Connected ${handler.remoteAddress} -> $socketAddr") - - val backChan = future.channel() as SocketChannel - handler.data.backChannel = backChan - handler.other = backChan - - forward(handler, packet, true) - - handler.data.frontChannel.setAutoRead(true) - } else { - // We're in the event loop - handler.disconnect("Couldn't connect: " + it.cause().toString()) - } - } - } catch (e: Exception) { - handler.data.frontChannel.eventLoop().submit { - handler.disconnect("Couldn't connect: $e") - } - } - } - } - - override fun disconnect(handler: CloudMinecraftHandler, msg: String) { - handler.data.frontChannel.close() // Not worth logging - } - - override fun onInactivated(handler: CloudMinecraftHandler) { - // Not worth logging - } -} - -class LoginState : MinecraftConnectionState { - val callbackPlayerId = CompletableFuture() - lateinit var frontToken: ByteArray - lateinit var frontServerId: String - override val state: State - get() = State.LOGIN - - override fun handlePacket(handler: CloudMinecraftHandler, ctx: ChannelHandlerContext, packet: Packet) { - when (packet) { - is LoginStart -> handleLoginStart(handler, packet) - is CryptoResponse -> handleCryptoResponse(handler, packet) - is PluginResponse -> forward(handler, packet) - is LoginDisconnect -> forward(handler, packet) - is CryptoRequest -> handleCryptoRequest(handler, packet) - is LoginSuccess -> handleLoginSuccess(handler, packet) - is SetCompression -> handleCompression(handler, packet) - is PluginRequest -> forward(handler, packet) - else -> throw IllegalArgumentException("Invalid packet!") - } - } - - private fun handleLoginSuccess(handler: CloudMinecraftHandler, loginSuccess: LoginSuccess) { - handler.data.state = PlayState - forward(handler, loginSuccess) - } - - private fun handleCompression(handler: CloudMinecraftHandler, setCompression: SetCompression) { - val pipe = handler.data.frontChannel.pipeline() - val threshold = setCompression.threshold - - val backPipe = pipe.get(CloudMinecraftHandler::class.java).other!!.pipeline() - if (threshold != -1) { - backPipe.addAfter("frame", "compress", CloudCompressionCodec(threshold)) - } else if (backPipe.get("compress") != null) { - backPipe.remove("compress") - } - - forward(handler, setCompression) - - if (threshold != -1) { - pipe.addAfter("frame", "compress", CloudCompressionCodec(threshold)) - // todo viarewind backend compression - } else if (pipe.get("compress") != null) { - pipe.remove("compress") - } - } - - fun authenticateOnlineFront(frontHandler: CloudMinecraftHandler) { - val id = "VIAaaS" + ByteArray(10).let { - secureRandom.nextBytes(it) - Base64.getEncoder().withoutPadding().encodeToString(it) - // https://developer.mozilla.org/en-US/docs/Glossary/Base64 133% of original - } - // We'll use non-vanilla server id, public key size and token size - val token = ByteArray(16).let { - secureRandom.nextBytes(it) - it - } - frontToken = token - frontServerId = id - - val cryptoRequest = CryptoRequest() - cryptoRequest.serverId = id - cryptoRequest.publicKey = mcCryptoKey.public - cryptoRequest.token = token - - sendPacket(frontHandler.data.frontChannel, cryptoRequest, true) - } - - fun handleCryptoRequest(handler: CloudMinecraftHandler, cryptoRequest: CryptoRequest) { - val data = handler.data - val backServerId = cryptoRequest.serverId - val backPublicKey = cryptoRequest.publicKey - val backToken = cryptoRequest.token - - if (data.frontOnline == null) { - authenticateOnlineFront(handler) - } - - val backKey = ByteArray(16).let { - secureRandom.nextBytes(it) - it - } - val backHash = generateServerHash(backServerId, backKey, backPublicKey) - - callbackPlayerId.whenComplete { playerId, e -> - if (e != null) return@whenComplete - val frontHandler = handler.data.frontHandler - GlobalScope.launch(Dispatchers.IO) { - try { - val sessionJoin = viaWebServer.requestSessionJoin( - parseUndashedId(playerId), - handler.data.backName!!, - backHash, - frontHandler.remoteAddress!!, // Frontend handler - backPublicKey - ) - - val backChan = handler.data.backChannel!! - sessionJoin.whenCompleteAsync({ _, throwable -> - if (throwable != null) { - frontHandler.data.backHandler!!.disconnect("Online mode error: $throwable") - } else { - val cryptoResponse = CryptoResponse() - cryptoResponse.encryptedKey = encryptRsa(backPublicKey, backKey) - cryptoResponse.encryptedToken = encryptRsa(backPublicKey, backToken) - forward(frontHandler, cryptoResponse, true) - - val backAesEn = mcCfb8(backKey, Cipher.ENCRYPT_MODE) - val backAesDe = mcCfb8(backKey, Cipher.DECRYPT_MODE) - backChan.pipeline().addBefore("frame", "crypto", CloudCrypto(backAesDe, backAesEn)) - } - }, backChan.eventLoop()) - } catch (e: Exception) { - frontHandler.disconnect("Online mode error: $e") - } - } - } - } - - fun handleCryptoResponse(handler: CloudMinecraftHandler, cryptoResponse: CryptoResponse) { - val frontHash = let { - val frontKey = decryptRsa(mcCryptoKey.private, cryptoResponse.encryptedKey) - // RSA token - wat??? why is it encrypted with RSA if it was sent unencrypted? - val decryptedToken = decryptRsa(mcCryptoKey.private, cryptoResponse.encryptedToken) - - if (!decryptedToken.contentEquals(frontToken)) throw IllegalStateException("invalid token!") - - val aesEn = mcCfb8(frontKey, Cipher.ENCRYPT_MODE) - val aesDe = mcCfb8(frontKey, Cipher.DECRYPT_MODE) - - handler.data.frontChannel.pipeline().addBefore("frame", "crypto", CloudCrypto(aesDe, aesEn)) - - generateServerHash(frontServerId, frontKey, mcCryptoKey.public) - } - - handler.data.frontChannel.setAutoRead(false) - GlobalScope.launch(Dispatchers.IO) { - try { - val profile = httpClient.get( - "https://sessionserver.mojang.com/session/minecraft/hasJoined?username=" + - UrlEscapers.urlFormParameterEscaper().escape(handler.data.frontName!!) + - "&serverId=$frontHash" - ) ?: throw IllegalArgumentException("Couldn't authenticate with session servers") - - val id = profile.get("id")!!.asString - mcLogger.info("Validated front-end session: ${handler.data.frontName} $id") - callbackPlayerId.complete(id) - } catch (e: Exception) { - callbackPlayerId.completeExceptionally(e) - } - handler.data.frontChannel.setAutoRead(true) - } - } - - fun handleLoginStart(handler: CloudMinecraftHandler, loginStart: LoginStart) { - if (loginStart.username.length > 16) throw badLength - if (handler.data.frontName != null) throw IllegalStateException("Login already started") - - handler.data.frontName = loginStart.username - handler.data.backName = handler.data.backName ?: handler.data.frontName - - loginStart.username = handler.data.backName!! - - callbackPlayerId.whenComplete { _, e -> if (e != null) disconnect(handler, "Profile error: $e") } - - if (handler.data.frontOnline == false) { - callbackPlayerId.complete(generateOfflinePlayerUuid(handler.data.frontName!!).toString().replace("-", "")) - } - - if (handler.data.frontOnline == true) { // forced - authenticateOnlineFront(handler.data.backHandler!!) - callbackPlayerId.whenComplete { _, e -> - if (e == null) forward(handler, loginStart, true) - } - } else { - forward(handler, loginStart) - } - } - - override fun disconnect(handler: CloudMinecraftHandler, msg: String) { - super.disconnect(handler, msg) - - val packet = LoginDisconnect() - packet.msg = Gson().toJson("[VIAaaS] §c$msg") - sendFlushPacketClose(handler.data.frontChannel, packet) - } -} - -object StatusState : MinecraftConnectionState { - override val state: State - get() = State.STATUS - - override fun handlePacket(handler: CloudMinecraftHandler, ctx: ChannelHandlerContext, packet: Packet) { - if (packet is UnknownPacket) throw IllegalArgumentException("Invalid packet") - forward(handler, packet) - } - - override fun disconnect(handler: CloudMinecraftHandler, msg: String) { - super.disconnect(handler, msg) - - val packet = StatusResponse() - packet.json = """{"version": {"name": "VIAaaS", "protocol": -1}, "players": {"max": 0, "online": 0, - | "sample": []}, "description": {"text": ${Gson().toJson("§c$msg")}}}""".trimMargin() - sendFlushPacketClose(handler.data.frontChannel, packet) - } -} - -object PlayState : MinecraftConnectionState { - override val state: State - get() = State.PLAY - - override fun handlePacket(handler: CloudMinecraftHandler, ctx: ChannelHandlerContext, packet: Packet) { - if ((packet as UnknownPacket).id !in 0..127) throw IllegalArgumentException("Invalid packet id!") - forward(handler, packet) - } - - override fun disconnect(handler: CloudMinecraftHandler, msg: String) { - super.disconnect(handler, msg) - handler.data.frontChannel.close() - } -} - -fun decryptRsa(privateKey: PrivateKey, data: ByteArray) = Cipher.getInstance("RSA").let { - it.init(Cipher.DECRYPT_MODE, privateKey) - it.doFinal(data) -} - -fun encryptRsa(publicKey: PublicKey, data: ByteArray) = Cipher.getInstance("RSA").let { - it.init(Cipher.ENCRYPT_MODE, publicKey) - it.doFinal(data) -} - -fun mcCfb8(key: ByteArray, mode: Int): Cipher { - val spec = SecretKeySpec(key, "AES") - val iv = IvParameterSpec(key) - return Cipher.getInstance("AES/CFB8/NoPadding").let { - it.init(mode, spec, iv) - it - } -} - - -fun Channel.setAutoRead(b: Boolean) { - this.config().isAutoRead = b - if (b) this.read() -} - -fun twosComplementHexdigest(digest: ByteArray): String { - return BigInteger(digest).toString(16) -} - -// https://github.com/VelocityPowered/Velocity/blob/0dd6fe1ef2783fe1f9322af06c6fd218aa67cdb1/proxy/src/main/java/com/velocitypowered/proxy/util/EncryptionUtils.java -fun generateServerHash(serverId: String, sharedSecret: ByteArray?, key: PublicKey): String { - val digest = MessageDigest.getInstance("SHA-1") - digest.update(serverId.toByteArray(Charsets.ISO_8859_1)) - digest.update(sharedSecret) - digest.update(key.encoded) - return twosComplementHexdigest(digest.digest()) -} - -private fun sendFlushPacketClose(ch: Channel, packet: Packet) { - ch.writeAndFlush(packet).addListener { ch.close() } -} - -private fun forward(handler: CloudMinecraftHandler, packet: Packet, flush: Boolean = false) { - sendPacket(handler.other!!, packet, flush) -} - -private fun sendPacket(ch: Channel, packet: Packet, flush: Boolean = false) { - if (flush) { - ch.writeAndFlush(packet, ch.voidPromise()) - } else { - ch.write(packet, ch.voidPromise()) - } -} - -private fun resolveSrv(address: String, port: Int): Pair { - if (port == 25565) { - try { - // https://github.com/GeyserMC/Geyser/blob/99e72f35b308542cf0dbfb5b58816503c3d6a129/connector/src/main/java/org/geysermc/connector/GeyserConnector.java - val attr = InitialDirContext() - .getAttributes("dns:///_minecraft._tcp.$address", arrayOf("SRV"))["SRV"] - if (attr != null && attr.size() > 0) { - val record = (attr.get(0) as String).split(" ") - return record[3] to record[2].toInt() - } - } catch (ignored: NameNotFoundException) { - } - } - return address to port -} - -private fun checkLocalAddress(inetAddress: InetAddress): Boolean { - return VIAaaSConfig.blockLocalAddress && (inetAddress.isSiteLocalAddress - || inetAddress.isLoopbackAddress - || inetAddress.isLinkLocalAddress - || inetAddress.isAnyLocalAddress) -} - -private fun matchesAddress(addr: InetSocketAddress, list: List): Boolean { - return (matchAddress(addr.hostString, list) - || (addr.address != null && (matchAddress(addr.address.hostAddress, list) - || matchAddress(addr.address.hostName, list)))) -} - -private fun matchAddress(addr: String, list: List): Boolean { - if (list.contains("*")) return true - val parts = addr.split(".").filter(String::isNotEmpty) - val isNumericIp = parts.size == 4 && parts.all { Ints.tryParse(it) != null } - return (0..parts.size).any { i: Int -> - val query: String = if (isNumericIp) { - parts.filterIndexed { it, _ -> it <= i }.joinToString(".") + - if (i != 3) ".*" else "" - } else { - (if (i != 0) "*." else "") + - parts.filterIndexed { it, _ -> it >= i }.joinToString(".") - } - list.contains(query) - } -} - -// https://github.com/VelocityPowered/Velocity/blob/e3f17eeb245b8d570f16c1f2aff5e7eafb698d5e/api/src/main/java/com/velocitypowered/api/util/UuidUtils.java -fun generateOfflinePlayerUuid(username: String) = - UUID.nameUUIDFromBytes("OfflinePlayer:$username".toByteArray(Charsets.UTF_8)) \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/CloudPackets.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/CloudPackets.kt deleted file mode 100644 index 1564298..0000000 --- a/src/main/kotlin/com/github/creeper123123321/viaaas/CloudPackets.kt +++ /dev/null @@ -1,336 +0,0 @@ -package com.github.creeper123123321.viaaas - -import com.google.common.collect.Range -import io.netty.buffer.ByteBuf -import us.myles.ViaVersion.api.protocol.ProtocolVersion -import us.myles.ViaVersion.api.type.Type -import us.myles.ViaVersion.packets.State -import java.security.KeyFactory -import java.security.PublicKey -import java.security.spec.X509EncodedKeySpec -import java.util.* -import java.util.function.Supplier -import kotlin.properties.Delegates - -/** - * A mutable object which represents a Minecraft packet data - */ -interface Packet { - fun decode(byteBuf: ByteBuf, protocolVersion: Int) - fun encode(byteBuf: ByteBuf, protocolVersion: Int) -} - -object PacketRegistry { - val entries = mutableListOf() - - init { - register(Range.all(), State.HANDSHAKE, 0, true, ::HandshakePacket) - register(Range.all(), State.LOGIN, 0, true, ::LoginStart) - register(Range.all(), State.LOGIN, 1, true, ::CryptoResponse) - register(Range.atLeast(ProtocolVersion.v1_13.version), State.LOGIN, 2, true, ::PluginResponse) - register(Range.all(), State.LOGIN, 0, false, ::LoginDisconnect) - register(Range.all(), State.LOGIN, 1, false, ::CryptoRequest) - register(Range.all(), State.LOGIN, 2, false, ::LoginSuccess) - register(Range.all(), State.LOGIN, 3, false, ::SetCompression) - register(Range.all(), State.LOGIN, 4, false, ::PluginRequest) - register(Range.all(), State.STATUS, 0, true, ::StatusRequest) - register(Range.all(), State.STATUS, 1, true, ::StatusPing) - register(Range.all(), State.STATUS, 0, false, ::StatusResponse) - register(Range.all(), State.STATUS, 1, false, ::StatusPong) - } - - inline fun register( - protocol: Range, - state: State, - id: Int, - serverBound: Boolean, - constructor: Supplier

- ) { - entries.add(RegistryEntry(protocol, state, id, serverBound, constructor, P::class.java)) - } - - data class RegistryEntry( - val versionRange: Range, - val state: State, - val id: Int, - val serverBound: Boolean, - val constructor: Supplier, - val packetClass: Class - ) - - fun getPacketConstructor( - protocolVersion: Int, - state: State, - id: Int, - serverBound: Boolean - ): Supplier? { - return entries.firstOrNull { - it.serverBound == serverBound && it.state == state - && it.versionRange.contains(protocolVersion) && it.id == id - }?.constructor - } - - fun getPacketId(packetClass: Class, protocolVersion: Int): Int? { - return entries.firstOrNull { - it.versionRange.contains(protocolVersion) && it.packetClass == packetClass - }?.id - } - - fun decode(byteBuf: ByteBuf, protocolVersion: Int, state: State, serverBound: Boolean): Packet { - val packetId = Type.VAR_INT.readPrimitive(byteBuf) - val packet = - getPacketConstructor(protocolVersion, state, packetId, serverBound)?.get() ?: UnknownPacket(packetId) - packet.decode(byteBuf, protocolVersion) - if (byteBuf.isReadable) throw IllegalStateException("Remaining bytes!") - return packet - } - - fun encode(packet: Packet, byteBuf: ByteBuf, protocolVersion: Int) { - val id = if (packet is UnknownPacket) { - packet.id - } else { - getPacketId(packet.javaClass, protocolVersion)!! - } - Type.VAR_INT.writePrimitive(byteBuf, id) - packet.encode(byteBuf, protocolVersion) - } -} - -class UnknownPacket(val id: Int) : Packet { - lateinit var content: ByteArray - - override fun decode(byteBuf: ByteBuf, protocolVersion: Int) { - content = ByteArray(byteBuf.readableBytes()).also { byteBuf.readBytes(it) } - } - - override fun encode(byteBuf: ByteBuf, protocolVersion: Int) { - byteBuf.writeBytes(content) - } -} - -// Some code based on https://github.com/VelocityPowered/Velocity/tree/dev/1.1.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet - -class HandshakePacket : Packet { - var protocolId by Delegates.notNull() - lateinit var address: String - var port by Delegates.notNull() - lateinit var nextState: State - - override fun decode(byteBuf: ByteBuf, protocolVersion: Int) { - protocolId = Type.VAR_INT.readPrimitive(byteBuf) - address = Type.STRING.read(byteBuf) - port = byteBuf.readUnsignedShort() - nextState = State.values()[Type.VAR_INT.readPrimitive(byteBuf)] - } - - override fun encode(byteBuf: ByteBuf, protocolVersion: Int) { - Type.VAR_INT.writePrimitive(byteBuf, protocolId) - Type.STRING.write(byteBuf, address) - byteBuf.writeShort(port) - byteBuf.writeByte(nextState.ordinal) // var int is too small, fits in a byte - } -} - -class LoginStart : Packet { - lateinit var username: String - - override fun decode(byteBuf: ByteBuf, protocolVersion: Int) { - username = Type.STRING.read(byteBuf) - } - - override fun encode(byteBuf: ByteBuf, protocolVersion: Int) { - Type.STRING.write(byteBuf, username) - } -} - -class CryptoResponse : Packet { - lateinit var encryptedKey: ByteArray - lateinit var encryptedToken: ByteArray - - override fun decode(byteBuf: ByteBuf, protocolVersion: Int) { - if (protocolVersion >= ProtocolVersion.v1_8.version) { - encryptedKey = Type.BYTE_ARRAY_PRIMITIVE.read(byteBuf) - encryptedToken = Type.BYTE_ARRAY_PRIMITIVE.read(byteBuf) - } else { - encryptedKey = ByteArray(byteBuf.readUnsignedShort()).also { byteBuf.readBytes(it) } - encryptedToken = ByteArray(byteBuf.readUnsignedShort()).also { byteBuf.readBytes(it) } - } - } - - override fun encode(byteBuf: ByteBuf, protocolVersion: Int) { - if (protocolVersion >= ProtocolVersion.v1_8.version) { - Type.BYTE_ARRAY_PRIMITIVE.write(byteBuf, encryptedKey) - Type.BYTE_ARRAY_PRIMITIVE.write(byteBuf, encryptedToken) - } else { - byteBuf.writeShort(encryptedKey.size) - byteBuf.writeBytes(encryptedKey) - byteBuf.writeShort(encryptedToken.size) - byteBuf.writeBytes(encryptedToken) - } - } -} - -class PluginResponse : Packet { - var id by Delegates.notNull() - var success by Delegates.notNull() - lateinit var data: ByteArray - override fun decode(byteBuf: ByteBuf, protocolVersion: Int) { - id = Type.VAR_INT.readPrimitive(byteBuf) - success = byteBuf.readBoolean() - if (success) { - data = ByteArray(byteBuf.readableBytes()).also { byteBuf.readBytes(it) } - } - } - - override fun encode(byteBuf: ByteBuf, protocolVersion: Int) { - Type.VAR_INT.writePrimitive(byteBuf, id) - byteBuf.writeBoolean(success) - if (success) { - byteBuf.writeBytes(data) - } - } -} - -class LoginDisconnect : Packet { - lateinit var msg: String - override fun decode(byteBuf: ByteBuf, protocolVersion: Int) { - msg = Type.STRING.read(byteBuf) - } - - override fun encode(byteBuf: ByteBuf, protocolVersion: Int) { - Type.STRING.write(byteBuf, msg) - } -} - -class CryptoRequest : Packet { - lateinit var serverId: String - lateinit var publicKey: PublicKey - lateinit var token: ByteArray - - override fun decode(byteBuf: ByteBuf, protocolVersion: Int) { - serverId = Type.STRING.read(byteBuf) - if (protocolVersion >= ProtocolVersion.v1_8.version) { - publicKey = KeyFactory.getInstance("RSA") - .generatePublic(X509EncodedKeySpec(Type.BYTE_ARRAY_PRIMITIVE.read(byteBuf))) - token = Type.BYTE_ARRAY_PRIMITIVE.read(byteBuf) - } else { - publicKey = KeyFactory.getInstance("RSA") - .generatePublic(X509EncodedKeySpec(ByteArray(byteBuf.readUnsignedShort()).also { byteBuf.readBytes(it) })) - token = ByteArray(byteBuf.readUnsignedShort()).also { byteBuf.readBytes(it) } - } - } - - override fun encode(byteBuf: ByteBuf, protocolVersion: Int) { - Type.STRING.write(byteBuf, serverId) - if (protocolVersion >= ProtocolVersion.v1_8.version) { - Type.BYTE_ARRAY_PRIMITIVE.write(byteBuf, publicKey.encoded) - Type.BYTE_ARRAY_PRIMITIVE.write(byteBuf, token) - } else { - val encodedKey = publicKey.encoded - byteBuf.writeShort(encodedKey.size) - byteBuf.writeBytes(encodedKey) - byteBuf.writeShort(token.size) - byteBuf.writeBytes(token) - } - } -} - -class LoginSuccess : Packet { - lateinit var id: UUID - lateinit var username: String - - override fun decode(byteBuf: ByteBuf, protocolVersion: Int) { - id = when { - protocolVersion >= ProtocolVersion.v1_16.version -> { - Type.UUID_INT_ARRAY.read(byteBuf) - } - protocolVersion >= ProtocolVersion.v1_7_6.version -> { - UUID.fromString(Type.STRING.read(byteBuf)) - } - else -> parseUndashedId(Type.STRING.read(byteBuf)) - } - username = Type.STRING.read(byteBuf) - } - - override fun encode(byteBuf: ByteBuf, protocolVersion: Int) { - when { - protocolVersion >= ProtocolVersion.v1_16.version -> { - Type.UUID_INT_ARRAY.write(byteBuf, id) - } - protocolVersion >= ProtocolVersion.v1_7_6.version -> { - Type.STRING.write(byteBuf, id.toString()) - } - else -> Type.STRING.write(byteBuf, id.toString().replace("-", "")) - } - Type.STRING.write(byteBuf, username) - } -} - -class SetCompression : Packet { - var threshold by Delegates.notNull() - override fun decode(byteBuf: ByteBuf, protocolVersion: Int) { - threshold = Type.VAR_INT.readPrimitive(byteBuf) - } - - override fun encode(byteBuf: ByteBuf, protocolVersion: Int) { - Type.VAR_INT.writePrimitive(byteBuf, threshold) - } -} - -class PluginRequest : Packet { - var id by Delegates.notNull() - lateinit var channel: String - lateinit var data: ByteArray - override fun decode(byteBuf: ByteBuf, protocolVersion: Int) { - id = Type.VAR_INT.readPrimitive(byteBuf) - channel = Type.STRING.read(byteBuf) - data = ByteArray(byteBuf.readableBytes()).also { byteBuf.readBytes(it) } - } - - override fun encode(byteBuf: ByteBuf, protocolVersion: Int) { - Type.VAR_INT.writePrimitive(byteBuf, id) - Type.STRING.write(byteBuf, channel) - byteBuf.writeBytes(data) - } -} - -class StatusResponse : Packet { - lateinit var json: String - override fun decode(byteBuf: ByteBuf, protocolVersion: Int) { - json = Type.STRING.read(byteBuf) - } - - override fun encode(byteBuf: ByteBuf, protocolVersion: Int) { - Type.STRING.write(byteBuf, json) - } -} - -class StatusRequest: Packet { - override fun decode(byteBuf: ByteBuf, protocolVersion: Int) { - } - - override fun encode(byteBuf: ByteBuf, protocolVersion: Int) { - } -} - -class StatusPing: Packet { - var number by Delegates.notNull() - override fun decode(byteBuf: ByteBuf, protocolVersion: Int) { - number = byteBuf.readLong() - } - - override fun encode(byteBuf: ByteBuf, protocolVersion: Int) { - byteBuf.writeLong(number) - } -} - -class StatusPong: Packet { - var number by Delegates.notNull() - override fun decode(byteBuf: ByteBuf, protocolVersion: Int) { - number = byteBuf.readLong() - } - - override fun encode(byteBuf: ByteBuf, protocolVersion: Int) { - byteBuf.writeLong(number) - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/CloudPlatform.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/CloudPlatform.kt deleted file mode 100644 index dc7c494..0000000 --- a/src/main/kotlin/com/github/creeper123123321/viaaas/CloudPlatform.kt +++ /dev/null @@ -1,198 +0,0 @@ -package com.github.creeper123123321.viaaas - -import com.google.common.util.concurrent.ThreadFactoryBuilder -import de.gerrygames.viarewind.api.ViaRewindPlatform -import io.netty.buffer.ByteBuf -import io.netty.channel.DefaultEventLoop -import nl.matsv.viabackwards.api.ViaBackwardsPlatform -import org.slf4j.LoggerFactory -import us.myles.ViaVersion.AbstractViaConfig -import us.myles.ViaVersion.api.Via -import us.myles.ViaVersion.api.ViaAPI -import us.myles.ViaVersion.api.ViaVersionConfig -import us.myles.ViaVersion.api.boss.BossBar -import us.myles.ViaVersion.api.boss.BossColor -import us.myles.ViaVersion.api.boss.BossStyle -import us.myles.ViaVersion.api.command.ViaCommandSender -import us.myles.ViaVersion.api.configuration.ConfigurationProvider -import us.myles.ViaVersion.api.data.UserConnection -import us.myles.ViaVersion.api.platform.* -import us.myles.ViaVersion.api.protocol.ProtocolRegistry -import us.myles.ViaVersion.boss.CommonBoss -import us.myles.ViaVersion.bungee.providers.BungeeMovementTransmitter -import us.myles.ViaVersion.commands.ViaCommandHandler -import us.myles.ViaVersion.protocols.base.VersionProvider -import us.myles.ViaVersion.protocols.protocol1_9to1_8.providers.MovementTransmitterProvider -import us.myles.ViaVersion.sponge.VersionInfo -import us.myles.ViaVersion.sponge.util.LoggerWrapper -import us.myles.ViaVersion.util.GsonUtil -import us.myles.viaversion.libs.gson.JsonObject -import java.io.File -import java.net.URL -import java.util.* -import java.util.concurrent.CompletableFuture -import java.util.concurrent.Executors -import java.util.concurrent.Future -import java.util.concurrent.TimeUnit -import java.util.logging.Logger - -object CloudBackwards : ViaBackwardsPlatform { - val log = LoggerWrapper(LoggerFactory.getLogger("ViaBackwards")) - override fun getDataFolder() = File("config/viabackwards") - override fun getLogger(): Logger = log - override fun disable() { - } -} - -object CloudRewind : ViaRewindPlatform { - val log = LoggerWrapper(LoggerFactory.getLogger("ViaRewind")) - override fun getLogger(): Logger = log -} - -object CloudLoader : ViaPlatformLoader { - override fun unload() { - } - - override fun load() { - Via.getManager().providers.use(MovementTransmitterProvider::class.java, BungeeMovementTransmitter()) - Via.getManager().providers.use(VersionProvider::class.java, CloudVersionProvider) - } -} - -object CloudCommands : ViaCommandHandler() -object CloudInjector : ViaInjector { - override fun getEncoderName(): String = "via-codec" - override fun getDecoderName() = "via-codec" - override fun getDump(): JsonObject = JsonObject() - - override fun uninject() { - } - - override fun inject() { - } - - - override fun getServerProtocolVersion() = 47 // Dummy -} - -class CloudBossBar(title: String, health: Float, style: BossStyle, color: BossColor) : - CommonBoss(title, health, color, style) - -object CloudAPI : ViaAPI { - override fun isInjected(p0: UUID): Boolean = false - override fun createBossBar(p0: String, p1: BossColor, p2: BossStyle): BossBar<*> = CloudBossBar(p0, 0f, p2, p1) - override fun createBossBar(p0: String, p1: Float, p2: BossColor, p3: BossStyle): BossBar<*> = - CloudBossBar(p0, p1, p3, p2) - - override fun sendRawPacket(p0: Unit?, p1: ByteBuf?) { - TODO("Not yet implemented") - } - - override fun sendRawPacket(p0: UUID?, p1: ByteBuf?) { - TODO("Not yet implemented") - } - - override fun getPlayerVersion(p0: Unit?): Int { - TODO("Not yet implemented") - } - - override fun getPlayerVersion(p0: UUID?): Int { - TODO("Not yet implemented") - } - - override fun getVersion(): String = CloudPlatform.pluginVersion - override fun getSupportedVersions(): SortedSet = ProtocolRegistry.getSupportedVersions() -} - -object CloudPlatform : ViaPlatform { - val connMan = CloudConnectionManager() - val executor = Executors.newCachedThreadPool(ThreadFactoryBuilder().setNameFormat("Via-%d").setDaemon(true).build()) - val eventLoop = DefaultEventLoop(executor) - - init { - eventLoop.execute(initFuture::join) - } - - override fun sendMessage(p0: UUID, p1: String) { - // todo - } - - override fun kickPlayer(p0: UUID, p1: String): Boolean = false // todo - override fun getApi(): ViaAPI = CloudAPI - override fun getDataFolder(): File = File("viaversion") - override fun getConf(): ViaVersionConfig = CloudConfig - override fun onReload() { - } - - override fun getDump(): JsonObject = JsonObject() - override fun runSync(runnable: Runnable): TaskId = CloudTask(eventLoop.submit(runnable)) - override fun runSync(p0: Runnable, p1: Long): TaskId = - CloudTask(eventLoop.schedule(p0, p1 * 50L, TimeUnit.MILLISECONDS)) - - override fun runRepeatingSync(p0: Runnable, p1: Long): TaskId = - CloudTask(eventLoop.scheduleAtFixedRate(p0, 0, p1 * 50L, TimeUnit.MILLISECONDS)) - - override fun runAsync(p0: Runnable): TaskId = CloudTask(CompletableFuture.runAsync(p0, executor)) - override fun getLogger(): Logger = LoggerWrapper(LoggerFactory.getLogger("ViaVersion")) - override fun getConnectionManager(): ViaConnectionManager = connMan - override fun getOnlinePlayers(): Array = arrayOf() - override fun cancelTask(p0: TaskId?) { - (p0 as CloudTask).obj.cancel(false) - } - - override fun isPluginEnabled(): Boolean = true - override fun getConfigurationProvider(): ConfigurationProvider = CloudConfig - - override fun getPlatformName(): String = "VIAaaS" - override fun getPlatformVersion(): String = viaaasVer - override fun getPluginVersion(): String = VersionInfo.VERSION - override fun isOldClientsAllowed(): Boolean = true - override fun isProxy(): Boolean = true -} - -class CloudConnectionManager : ViaConnectionManager() { - override fun isFrontEnd(conn: UserConnection): Boolean = false -} - -object CloudConfig : AbstractViaConfig(File("config/viaversion.yml")) { - // https://github.com/ViaVersion/ViaFabric/blob/mc-1.16/src/main/java/com/github/creeper123123321/viafabric/platform/VRViaConfig.java - override fun getDefaultConfigURL(): URL = javaClass.classLoader.getResource("assets/viaversion/config.yml")!! - - override fun handleConfig(config: Map) { - // Nothing Currently - } - - override fun getUnsupportedOptions(): List = UNSUPPORTED - override fun isAntiXRay(): Boolean = false - override fun isItemCache(): Boolean = false - override fun isNMSPlayerTicking(): Boolean = false - override fun is1_12QuickMoveActionFix(): Boolean = false - override fun getBlockConnectionMethod(): String = "packet" - override fun is1_9HitboxFix(): Boolean = false - override fun is1_14HitboxFix(): Boolean = false - - // Based on Sponge ViaVersion - private val UNSUPPORTED = listOf( - "anti-xray-patch", "bungee-ping-interval", - "bungee-ping-save", "bungee-servers", "quick-move-action-fix", "nms-player-ticking", - "item-cache", "velocity-ping-interval", "velocity-ping-save", "velocity-servers", - "blockconnection-method", "change-1_9-hitbox", "change-1_14-hitbox" - ) - - init { - // Load config - reloadConfig() - } -} - -class CloudTask(val obj: Future<*>) : TaskId { - override fun getObject(): Any = obj -} - -object CloudVersionProvider : VersionProvider() { - override fun getServerProtocol(connection: UserConnection): Int { - val ver = connection.channel!!.pipeline().get(CloudMinecraftHandler::class.java).data.backVer - if (ver != null) return ver - return super.getServerProtocol(connection) - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/Util.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/Util.kt new file mode 100644 index 0000000..157a989 --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/Util.kt @@ -0,0 +1,135 @@ +package com.github.creeper123123321.viaaas + +import com.github.creeper123123321.viaaas.config.VIAaaSConfig +import com.google.common.base.Preconditions +import com.google.common.primitives.Ints +import io.netty.channel.Channel +import io.netty.handler.codec.DecoderException +import org.slf4j.LoggerFactory +import java.math.BigInteger +import java.net.InetAddress +import java.net.InetSocketAddress +import java.security.MessageDigest +import java.security.PrivateKey +import java.security.PublicKey +import java.security.SecureRandom +import java.util.* +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec +import javax.naming.NameNotFoundException +import javax.naming.directory.InitialDirContext + +val badLength = DecoderException("Invalid length!") +val mcLogger = LoggerFactory.getLogger("VIAaaS MC") +val webLogger = LoggerFactory.getLogger("VIAaaS Web") +val viaaasLogger = LoggerFactory.getLogger("VIAaaS") + +// https://github.com/VelocityPowered/Velocity/blob/6467335f74a7d1617512a55cc9acef5e109b51ac/api/src/main/java/com/velocitypowered/api/util/UuidUtils.java +fun parseUndashedId(string: String): UUID { + Preconditions.checkArgument(string.length == 32, "Length is incorrect") + return UUID( + string.substring(0, 16).toULong(16).toLong(), + string.substring(16).toULong(16).toLong() + ) +} + +// https://github.com/VelocityPowered/Velocity/blob/0dd6fe1ef2783fe1f9322af06c6fd218aa67cdb1/proxy/src/main/java/com/velocitypowered/proxy/util/EncryptionUtils.java +fun generateServerHash(serverId: String, sharedSecret: ByteArray?, key: PublicKey): String { + val digest = MessageDigest.getInstance("SHA-1") + digest.update(serverId.toByteArray(Charsets.ISO_8859_1)) + digest.update(sharedSecret) + digest.update(key.encoded) + return twosComplementHexdigest(digest.digest()) +} + +fun twosComplementHexdigest(digest: ByteArray): String { + return BigInteger(digest).toString(16) +} + +fun Channel.setAutoRead(b: Boolean) { + this.config().isAutoRead = b + if (b) this.read() +} + +fun resolveSrv(address: String, port: Int): Pair { + if (port == 25565) { + try { + // https://github.com/GeyserMC/Geyser/blob/99e72f35b308542cf0dbfb5b58816503c3d6a129/connector/src/main/java/org/geysermc/connector/GeyserConnector.java + val attr = InitialDirContext() + .getAttributes("dns:///_minecraft._tcp.$address", arrayOf("SRV"))["SRV"] + if (attr != null && attr.size() > 0) { + val record = (attr.get(0) as String).split(" ") + return record[3] to record[2].toInt() + } + } catch (ignored: NameNotFoundException) { + } + } + return address to port +} + +fun decryptRsa(privateKey: PrivateKey, data: ByteArray) = Cipher.getInstance("RSA").let { + it.init(Cipher.DECRYPT_MODE, privateKey) + it.doFinal(data) +} + +fun encryptRsa(publicKey: PublicKey, data: ByteArray) = Cipher.getInstance("RSA").let { + it.init(Cipher.ENCRYPT_MODE, publicKey) + it.doFinal(data) +} + +fun mcCfb8(key: ByteArray, mode: Int): Cipher { + val spec = SecretKeySpec(key, "AES") + val iv = IvParameterSpec(key) + return Cipher.getInstance("AES/CFB8/NoPadding").let { + it.init(mode, spec, iv) + it + } +} + +fun checkLocalAddress(inetAddress: InetAddress): Boolean { + return VIAaaSConfig.blockLocalAddress && (inetAddress.isSiteLocalAddress + || inetAddress.isLoopbackAddress + || inetAddress.isLinkLocalAddress + || inetAddress.isAnyLocalAddress) +} + +fun matchesAddress(addr: InetSocketAddress, list: List): Boolean { + return (matchAddress(addr.hostString, list) + || (addr.address != null && (matchAddress(addr.address.hostAddress, list) + || matchAddress(addr.address.hostName, list)))) +} + +private fun matchAddress(addr: String, list: List): Boolean { + if (list.contains("*")) return true + val parts = addr.split(".").filter(String::isNotEmpty) + val isNumericIp = parts.size == 4 && parts.all { Ints.tryParse(it) != null } + return (0..parts.size).any { i: Int -> + val query: String = if (isNumericIp) { + parts.filterIndexed { it, _ -> it <= i }.joinToString(".") + + if (i != 3) ".*" else "" + } else { + (if (i != 0) "*." else "") + + parts.filterIndexed { it, _ -> it >= i }.joinToString(".") + } + list.contains(query) + } +} + +// https://github.com/VelocityPowered/Velocity/blob/e3f17eeb245b8d570f16c1f2aff5e7eafb698d5e/api/src/main/java/com/velocitypowered/api/util/UuidUtils.java +fun generateOfflinePlayerUuid(username: String) = + UUID.nameUUIDFromBytes("OfflinePlayer:$username".toByteArray(Charsets.UTF_8)) + +fun send(ch: Channel, obj: Any, flush: Boolean = false) { + if (flush) { + ch.writeAndFlush(obj, ch.voidPromise()) + } else { + ch.write(obj, ch.voidPromise()) + } +} + +fun writeFlushClose(ch: Channel, obj: Any) { + ch.writeAndFlush(obj).addListener { ch.close() } +} + +val secureRandom = if (VIAaaSConfig.useStrongRandom) SecureRandom.getInstanceStrong() else SecureRandom() \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/VIAaaS.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/VIAaaS.kt index e54bbf9..6987eba 100644 --- a/src/main/kotlin/com/github/creeper123123321/viaaas/VIAaaS.kt +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/VIAaaS.kt @@ -1,5 +1,12 @@ package com.github.creeper123123321.viaaas +import com.github.creeper123123321.viaaas.command.CloudCommands +import com.github.creeper123123321.viaaas.command.VIAaaSConsole +import com.github.creeper123123321.viaaas.config.VIAaaSConfig +import com.github.creeper123123321.viaaas.handler.FrontEndInit +import com.github.creeper123123321.viaaas.platform.* +import com.github.creeper123123321.viaaas.web.ViaWebApp +import com.github.creeper123123321.viaaas.web.WebDashboardServer import de.gerrygames.viarewind.api.ViaRewindConfigImpl import io.ktor.application.* import io.ktor.client.* @@ -27,43 +34,30 @@ import io.netty.channel.socket.SocketChannel import io.netty.channel.socket.nio.NioServerSocketChannel import io.netty.channel.socket.nio.NioSocketChannel import io.netty.util.concurrent.Future -import net.minecrell.terminalconsole.SimpleTerminalConsole -import org.jline.reader.Candidate -import org.jline.reader.LineReader -import org.jline.reader.LineReaderBuilder -import org.slf4j.LoggerFactory import us.myles.ViaVersion.ViaManager import us.myles.ViaVersion.api.Via -import us.myles.ViaVersion.api.command.ViaCommandSender import us.myles.ViaVersion.api.data.MappingDataLoader -import us.myles.ViaVersion.api.protocol.ProtocolVersion -import us.myles.ViaVersion.util.Config import us.myles.ViaVersion.util.GsonUtil import us.myles.viaversion.libs.gson.JsonObject import java.io.File import java.net.InetAddress import java.security.KeyPairGenerator -import java.security.SecureRandom -import java.util.* import java.util.concurrent.CompletableFuture val viaaasVer = GsonUtil.getGson().fromJson( - CloudPlatform::class.java.classLoader.getResourceAsStream("viaaas_info.json")!! - .reader(Charsets.UTF_8).readText(), JsonObject::class.java + CloudPlatform::class.java.classLoader.getResourceAsStream("viaaas_info.json")!!.reader(Charsets.UTF_8).readText(), + JsonObject::class.java ).get("version").asString - +val viaWebServer = WebDashboardServer() var runningServer = true -val viaaasLogger = LoggerFactory.getLogger("VIAaaS") - val httpClient = HttpClient { - defaultRequest { - header("User-Agent", "VIAaaS/$viaaasVer") + install(UserAgent) { + agent = "VIAaaS/$viaaasVer" } install(JsonFeature) { serializer = GsonSerializer() } } - val initFuture = CompletableFuture() // Minecraft doesn't have forward secrecy @@ -72,8 +66,6 @@ val mcCryptoKey = KeyPairGenerator.getInstance("RSA").let { it.genKeyPair() } -val secureRandom = if (VIAaaSConfig.useStrongRandom) SecureRandom.getInstanceStrong() else SecureRandom() - fun eventLoopGroup(): EventLoopGroup { if (VIAaaSConfig.isNativeTransportMc) { if (Epoll.isAvailable()) return EpollEventLoopGroup() @@ -101,7 +93,7 @@ fun channelSocketFactory(): ChannelFactory { fun main(args: Array) { // Stolen from https://github.com/VelocityPowered/Velocity/blob/dev/1.1.0/proxy/src/main/java/com/velocitypowered/proxy/Velocity.java if (System.getProperty("io.netty.allocator.maxOrder") == null) { - System.setProperty("io.netty.allocator.maxOrder", "9"); + System.setProperty("io.netty.allocator.maxOrder", "9") } File("config/https.jks").apply { @@ -127,7 +119,7 @@ fun main(args: Array) { val future = ServerBootstrap() .group(parent, child) .channelFactory(channelServerSocketFactory()) - .childHandler(FrontChannelInit) + .childHandler(FrontEndInit) .childOption(ChannelOption.IP_TOS, 0x18) .childOption(ChannelOption.TCP_NODELAY, true) .bind(InetAddress.getByName(VIAaaSConfig.bindAddress), VIAaaSConfig.port) @@ -143,9 +135,7 @@ fun main(args: Array) { initFuture.complete(Unit) - if (runningServer) { - VIAaaSConsole().start() - } + VIAaaSConsole().start() ktorServer?.stop(1000, 1000) httpClient.close() @@ -155,208 +145,6 @@ fun main(args: Array) { Via.getManager().destroy() } -class VIAaaSConsole : SimpleTerminalConsole(), ViaCommandSender { - val commands = hashMapOf?, String, Array) -> Unit>() - override fun isRunning(): Boolean = runningServer - - init { - commands["stop"] = { suggestion, _, _ -> if (suggestion == null) this.shutdown() } - commands["end"] = commands["stop"]!! - commands["viaversion"] = { suggestion, _, args -> - if (suggestion == null) { - Via.getManager().commandHandler.onCommand(this, args) - } else { - suggestion.addAll(Via.getManager().commandHandler.onTabComplete(this, args)) - } - } - commands["viaver"] = commands["viaversion"]!! - commands["vvcloud"] = commands["viaversion"]!! - commands["help"] = { suggestion, _, _ -> - if (suggestion == null) sendMessage(commands.entries.groupBy { it.value }.entries.joinToString(", ") { - it.value.joinToString("/") { it.key } - }) - } - commands["?"] = commands["help"]!! - commands["ver"] = { suggestion, _, _ -> - if (suggestion == null) sendMessage(viaaasVer) - } - commands["list"] = { suggestion, _, _ -> - if (suggestion == null) { - sendMessage("List of player connections: ") - Via.getPlatform().connectionManager.connections.forEach { - val backAddr = it.channel?.remoteAddress() - val pVer = it.protocolInfo?.protocolVersion?.let { - ProtocolVersion.getProtocol(it) - } - val backName = it.protocolInfo?.username - val backVer = it.protocolInfo?.serverProtocolVersion?.let { - ProtocolVersion.getProtocol(it) - } - val pAddr = - it.channel?.pipeline()?.get(CloudMinecraftHandler::class.java)?.other?.remoteAddress() - val pName = it.channel?.pipeline()?.get(CloudMinecraftHandler::class.java)?.data?.frontName - sendMessage("$pAddr $pVer ($pName) -> $backVer ($backName) $backAddr") - } - } - } - } - - override fun buildReader(builder: LineReaderBuilder): LineReader { - // Stolen from Velocity - return super.buildReader(builder.appName("VIAaaS").completer { _, line, candidates -> - try { - val cmdArgs = line.line().substring(0, line.cursor()).split(" ") - val alias = cmdArgs[0] - val args = cmdArgs.filterIndexed { i, _ -> i > 0 } - if (cmdArgs.size == 1) { - candidates.addAll(commands.keys.filter { it.startsWith(alias, ignoreCase = true) } - .map { Candidate(it) }) - } else { - val cmd = commands[alias.toLowerCase()] - if (cmd != null) { - val suggestions = mutableListOf() - cmd(suggestions, alias, args.toTypedArray()) - candidates.addAll(suggestions.map(::Candidate)) - } - } - } catch (e: Exception) { - sendMessage("Error completing command: $e") - } - }) - } - - override fun runCommand(command: String) { - val cmd = command.split(" ") - try { - val alias = cmd[0].toLowerCase() - val args = cmd.subList(1, cmd.size).toTypedArray() - val runnable = commands[alias] - if (runnable == null) { - sendMessage("unknown command, try 'help'") - } else { - runnable(null, alias, args) - } - } catch (e: Exception) { - sendMessage("Error running command: $e") - } - } - - override fun shutdown() { - viaaasLogger.info("Shutting down...") - runningServer = false - } - - - override fun sendMessage(p0: String) { - LoggerFactory.getLogger(this.name).info(p0) - } - - override fun hasPermission(p0: String): Boolean = true - override fun getUUID(): UUID = UUID.fromString(name) - override fun getName(): String = "VIAaaS Console" -} - fun Application.mainWeb() { ViaWebApp().apply { main() } -} - -object VIAaaSConfig : Config(File("config/viaaas.yml")) { - init { - reloadConfig() - } - - override fun getUnsupportedOptions() = emptyList().toMutableList() - override fun getDefaultConfigURL() = VIAaaSConfig::class.java.classLoader.getResource("viaaas.yml")!! - override fun handleConfig(p0: MutableMap?) { - } - - val isNativeTransportMc: Boolean get() = this.getBoolean("native-transport-mc", true) - val port: Int get() = this.getInt("port", 25565) - val bindAddress: String get() = this.getString("bind-address", "localhost")!! - val hostName: String get() = this.getString("host-name", "viaaas.localhost")!! - val mcRsaSize: Int get() = this.getInt("mc-rsa-size", 4096) - val useStrongRandom: Boolean get() = this.getBoolean("use-strong-random", true) - val blockLocalAddress: Boolean get() = this.getBoolean("block-local-address", true) - val requireHostName: Boolean get() = this.getBoolean("require-host-name", true) - val defaultBackendPort: Int get() = this.getInt("default-backend-port", 25565) - val blockedBackAddresses: List - get() = this.get( - "blocked-back-addresses", - List::class.java, - emptyList() - )!!.map { it as String } - val allowedBackAddresses: List - get() = this.get( - "allowed-back-addresses", - List::class.java, - emptyList() - )!!.map { it as String } -} - -class VIAaaSAddress { - var serverAddress: String? = null - var viaSuffix: String? = null - var viaOptions: String? = null - var protocol: Int? = null - var port: Int? = null - var online: Boolean? = null - var username: String? = null - fun parse(rawAddress: String, viaHostName: String): VIAaaSAddress { - val address = rawAddress.removeSuffix(".") - val suffixRemoved = address.removeSuffix(".$viaHostName") - - if (suffixRemoved == address) { - serverAddress = address - return this - } - - var endOfOptions = false - val optionsList = arrayListOf() - serverAddress = suffixRemoved.split('.').asReversed().filter { - if (endOfOptions || !parseOption(it)) { - endOfOptions = true - true - } else { - optionsList.add(it) - false - } - }.asReversed().joinToString(".") - - viaOptions = optionsList.asReversed().joinToString(".") - - viaSuffix = viaHostName - - return this - } - - fun parseOption(part: String): Boolean { - if (part.startsWith("_")) { - val arg = part.substring(2) - when { - part.startsWith("_p", ignoreCase = true) -> port = arg.toInt() - part.startsWith("_o", ignoreCase = true) -> { - online = when { - arg.startsWith("t", ignoreCase = true) -> true - arg.startsWith("f", ignoreCase = true) -> false - else -> null - } - } - part.startsWith("_v", ignoreCase = true) -> { - try { - protocol = arg.toInt() - } catch (e: NumberFormatException) { - ProtocolVersion.getClosest(arg.replace("_", "."))?.also { - protocol = it.version - } - } - } - part.startsWith("_u", ignoreCase = true) -> { - if (arg.length > 16) throw IllegalArgumentException("Invalid username") - username = arg - } - } - return true - } - return false - } -} +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/VIAaaSAddress.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/VIAaaSAddress.kt new file mode 100644 index 0000000..41f01b0 --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/VIAaaSAddress.kt @@ -0,0 +1,71 @@ +package com.github.creeper123123321.viaaas + +import us.myles.ViaVersion.api.protocol.ProtocolVersion + +class VIAaaSAddress { + var serverAddress: String? = null + var viaSuffix: String? = null + var viaOptions: String? = null + var protocol: Int? = null + var port: Int? = null + var online: Boolean? = null + var username: String? = null + fun parse(rawAddress: String, viaHostName: String): VIAaaSAddress { + val address = rawAddress.removeSuffix(".") + val suffixRemoved = address.removeSuffix(".$viaHostName") + + if (suffixRemoved == address) { + serverAddress = address + return this + } + + var endOfOptions = false + val optionsList = arrayListOf() + serverAddress = suffixRemoved.split('.').asReversed().filter { + if (endOfOptions || !parseOption(it)) { + endOfOptions = true + true + } else { + optionsList.add(it) + false + } + }.asReversed().joinToString(".") + + viaOptions = optionsList.asReversed().joinToString(".") + + viaSuffix = viaHostName + + return this + } + + fun parseOption(part: String): Boolean { + if (part.startsWith("_")) { + val arg = part.substring(2) + when { + part.startsWith("_p", ignoreCase = true) -> port = arg.toInt() + part.startsWith("_o", ignoreCase = true) -> { + online = when { + arg.startsWith("t", ignoreCase = true) -> true + arg.startsWith("f", ignoreCase = true) -> false + else -> null + } + } + part.startsWith("_v", ignoreCase = true) -> { + try { + protocol = arg.toInt() + } catch (e: NumberFormatException) { + ProtocolVersion.getClosest(arg.replace("_", "."))?.also { + protocol = it.version + } + } + } + part.startsWith("_u", ignoreCase = true) -> { + if (arg.length > 16) throw IllegalArgumentException("Invalid username") + username = arg + } + } + return true + } + return false + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/ViaWeb.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/ViaWeb.kt deleted file mode 100644 index ee41eb8..0000000 --- a/src/main/kotlin/com/github/creeper123123321/viaaas/ViaWeb.kt +++ /dev/null @@ -1,253 +0,0 @@ -package com.github.creeper123123321.viaaas - -import com.google.common.base.Preconditions -import com.google.common.cache.CacheBuilder -import com.google.common.cache.CacheLoader -import com.google.gson.Gson -import com.google.gson.JsonObject -import io.ktor.application.* -import io.ktor.client.request.* -import io.ktor.client.request.forms.* -import io.ktor.features.* -import io.ktor.http.* -import io.ktor.http.cio.websocket.* -import io.ktor.http.content.* -import io.ktor.routing.* -import io.ktor.websocket.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.channels.consumeEach -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext -import org.slf4j.LoggerFactory -import org.slf4j.event.Level -import us.myles.ViaVersion.api.Via -import java.net.SocketAddress -import java.net.URLEncoder -import java.security.PublicKey -import java.time.Duration -import java.util.* -import java.util.UUID -import java.util.concurrent.CompletableFuture -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.TimeUnit -import java.util.concurrent.TimeoutException -import kotlin.collections.set - -val viaWebServer = WebDashboardServer() -val webLogger = LoggerFactory.getLogger("VIAaaS Web") - -class ViaWebApp { - fun Application.main() { - install(DefaultHeaders) - install(CallLogging) { - level = Level.INFO - } - install(WebSockets) { - pingPeriod = Duration.ofMinutes(1) - } - - routing { - webSocket("/ws") { - try { - viaWebServer.connected(this) - incoming.consumeEach { frame -> - if (frame is Frame.Text) { - viaWebServer.onMessage(this, frame.readText()) - } - } - } catch (e: Exception) { - webLogger.info("${call.request.local.remoteHost} (O: ${call.request.origin.remoteHost}) exception: $e") - viaWebServer.onException(this, e) - this.close(CloseReason(CloseReason.Codes.INTERNAL_ERROR, "INTERNAL ERROR")) - } finally { - viaWebServer.disconnected(this) - } - } - - static { - defaultResource("index.html", "web") - resources("web") - } - } - } -} - -// https://github.com/VelocityPowered/Velocity/blob/6467335f74a7d1617512a55cc9acef5e109b51ac/api/src/main/java/com/velocitypowered/api/util/UuidUtils.java -fun parseUndashedId(string: String): UUID { - Preconditions.checkArgument(string.length == 32, "Length is incorrect") - return UUID( - java.lang.Long.parseUnsignedLong(string.substring(0, 16), 16), - java.lang.Long.parseUnsignedLong(string.substring(16), 16) - ) -} - -class WebDashboardServer { - val clients = ConcurrentHashMap() - val loginTokens = CacheBuilder.newBuilder() - .expireAfterAccess(10, TimeUnit.DAYS) - .build() - - // Minecraft account -> WebClient - val listeners = ConcurrentHashMap>() - val usernameIdCache = CacheBuilder.newBuilder() - .expireAfterWrite(1, TimeUnit.HOURS) - .build(CacheLoader.from { name -> - runBlocking { - withContext(Dispatchers.IO) { - httpClient.get("https://api.mojang.com/users/profiles/minecraft/$name") - ?.get("id")?.asString?.let { parseUndashedId(it) } - } - } - }) - - val pendingSessionHashes = CacheBuilder.newBuilder() - .expireAfterWrite(30, TimeUnit.SECONDS) - .build>(CacheLoader.from { _ -> CompletableFuture() }) - - suspend fun requestSessionJoin( - id: UUID, name: String, hash: String, - address: SocketAddress, backKey: PublicKey - ) - : CompletableFuture { - val future = viaWebServer.pendingSessionHashes.get(hash) - var sent = 0 - viaWebServer.listeners[id]?.forEach { - it.ws.send( - """{"action": "session_hash_request", "user": "$name", "session_hash": "$hash", - | "client_address": "$address", "backend_public_key": - | "${Base64.getEncoder().encodeToString(backKey.encoded)}"}""".trimMargin() - ) - it.ws.flush() - sent++ - } - if (sent != 0) { - Via.getPlatform().runSync({ - future.completeExceptionally(TimeoutException("No response from browser")) - }, 15 * 20) - } else { - future.completeExceptionally(IllegalStateException("No browser listening")) - } - return future - } - - suspend fun connected(ws: WebSocketServerSession) { - val loginState = WebLogin() - val client = WebClient(this, ws, loginState) - clients[ws] = client - loginState.start(client) - } - - suspend fun onMessage(ws: WebSocketSession, msg: String) { - val client = clients[ws]!! - client.state.onMessage(client, msg) - } - - suspend fun disconnected(ws: WebSocketSession) { - val client = clients[ws]!! - client.state.disconnected(client) - clients.remove(ws) - } - - suspend fun onException(ws: WebSocketSession, exception: java.lang.Exception) { - val client = clients[ws]!! - client.state.onException(client, exception) - } -} - - -data class WebClient( - val server: WebDashboardServer, - val ws: WebSocketServerSession, - val state: WebState, - val listenedIds: MutableSet = mutableSetOf() -) - -interface WebState { - suspend fun start(webClient: WebClient) - suspend fun onMessage(webClient: WebClient, msg: String) - suspend fun disconnected(webClient: WebClient) - suspend fun onException(webClient: WebClient, exception: java.lang.Exception) -} - -class WebLogin : WebState { - override suspend fun start(webClient: WebClient) { - webClient.ws.send("""{"action": "ad_minecraft_id_login"}""") - webClient.ws.flush() - } - - override suspend fun onMessage(webClient: WebClient, msg: String) { - val obj = Gson().fromJson(msg, JsonObject::class.java) - - when (obj.getAsJsonPrimitive("action").asString) { - "offline_login" -> { - // todo add some spam check - val username = obj.get("username").asString - val token = UUID.randomUUID() - val uuid = generateOfflinePlayerUuid(username) - - webClient.server.loginTokens.put(token, uuid) - webClient.ws.send( - """{"action": "login_result", "success": true, - | "username": "$username", "uuid": "$uuid", "token": "$token"}""".trimMargin() - ) - - webLogger.info("${webClient.ws.call.request.local.remoteHost} (O: ${webClient.ws.call.request.origin.remoteHost}) generated a token for offline account $username") - } - "minecraft_id_login" -> { - val username = obj.get("username").asString - val code = obj.get("code").asString - - val check = httpClient.submitForm( - "https://api.minecraft.id/gateway/verify/${URLEncoder.encode(username, Charsets.UTF_8)}", - formParameters = parametersOf("code", code), - ) - - if (check.getAsJsonPrimitive("valid").asBoolean) { - val token = UUID.randomUUID() - val mcIdUser = check.get("username").asString - val uuid = webClient.server.usernameIdCache.get(mcIdUser) - - webClient.server.loginTokens.put(token, uuid) - webClient.ws.send( - """{"action": "login_result", "success": true, - | "username": "$mcIdUser", "uuid": "$uuid", "token": "$token"}""".trimMargin() - ) - - webLogger.info("${webClient.ws.call.request.local.remoteHost} (O: ${webClient.ws.call.request.origin.remoteHost}) generated a token for account $mcIdUser $uuid") - } else { - webClient.ws.send("""{"action": "login_result", "success": false}""") - webLogger.info("${webClient.ws.call.request.local.remoteHost} (O: ${webClient.ws.call.request.origin.remoteHost}) failed to generated a token for account $username") - } - } - "listen_login_requests" -> { - val token = UUID.fromString(obj.getAsJsonPrimitive("token").asString) - val user = webClient.server.loginTokens.getIfPresent(token) - if (user != null) { - webClient.ws.send("""{"action": "listen_login_requests_result", "token": "$token", "success": true, "user": "$user"}""") - webClient.listenedIds.add(user) - webClient.server.listeners.computeIfAbsent(user) { Collections.newSetFromMap(ConcurrentHashMap()) } - .add(webClient) - - webLogger.info("${webClient.ws.call.request.local.remoteHost} (O: ${webClient.ws.call.request.origin.remoteHost}) listening for logins for $user") - } else { - webClient.ws.send("""{"action": "listen_login_requests_result", "token": "$token", "success": false}""") - webLogger.info("${webClient.ws.call.request.local.remoteHost} (O: ${webClient.ws.call.request.origin.remoteHost}) failed token") - } - } - "session_hash_response" -> { - val hash = obj.get("session_hash").asString - webClient.server.pendingSessionHashes.getIfPresent(hash)?.complete(null) - } - else -> throw IllegalStateException("invalid action!") - } - - webClient.ws.flush() - } - - override suspend fun disconnected(webClient: WebClient) { - webClient.listenedIds.forEach { webClient.server.listeners[it]?.remove(webClient) } - } - - override suspend fun onException(webClient: WebClient, exception: java.lang.Exception) { - } -} diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/codec/CompressionCodec.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/codec/CompressionCodec.kt new file mode 100644 index 0000000..bcb713c --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/codec/CompressionCodec.kt @@ -0,0 +1,75 @@ +package com.github.creeper123123321.viaaas.codec + +import io.netty.buffer.ByteBuf +import io.netty.channel.ChannelHandlerContext +import io.netty.handler.codec.DecoderException +import io.netty.handler.codec.MessageToMessageCodec +import us.myles.ViaVersion.api.type.Type +import java.util.zip.Deflater +import java.util.zip.Inflater + +class CompressionCodec(val threshold: Int) : MessageToMessageCodec() { + // https://github.com/Gerrygames/ClientViaVersion/blob/master/src/main/java/de/gerrygames/the5zig/clientviaversion/netty/CompressionEncoder.java + private val inflater: Inflater = + Inflater()// https://github.com/Gerrygames/ClientViaVersion/blob/master/src/main/java/de/gerrygames/the5zig/clientviaversion/netty/CompressionEncoder.java + private val deflater: Deflater = Deflater() + + @Throws(Exception::class) + override fun encode(ctx: ChannelHandlerContext, input: ByteBuf, out: MutableList) { + val frameLength = input.readableBytes() + val outBuf = ctx.alloc().buffer() + try { + if (frameLength < threshold) { + outBuf.writeByte(0) + outBuf.writeBytes(input) + out.add(outBuf.retain()) + return + } + Type.VAR_INT.writePrimitive(outBuf, frameLength) + deflater.setInput(input.nioBuffer()) + deflater.finish() + while (!deflater.finished()) { + outBuf.ensureWritable(8192) + val wIndex = outBuf.writerIndex() + outBuf.writerIndex(wIndex + deflater.deflate(outBuf.nioBuffer(wIndex, outBuf.writableBytes()))) + } + out.add(outBuf.retain()) + } finally { + outBuf.release() + deflater.reset() + } + } + + @Throws(Exception::class) + override fun decode(ctx: ChannelHandlerContext, input: ByteBuf, out: MutableList) { + if (input.isReadable) { + val outLength = Type.VAR_INT.readPrimitive(input) + if (outLength == 0) { + out.add(input.retain()) + return + } + + if (outLength < threshold) { + throw DecoderException("Badly compressed packet - size of $outLength is below server threshold of $threshold") + } + if (outLength > 2097152) { + throw DecoderException("Badly compressed packet - size of $outLength is larger than protocol maximum of 2097152") + } + + inflater.setInput(input.nioBuffer()) + val output = ctx.alloc().buffer(outLength, outLength) + try { + output.writerIndex( + output.writerIndex() + inflater.inflate( + output.nioBuffer(output.writerIndex(), output.writableBytes()) + ) + ) + out.add(output.retain()) + } finally { + inflater.reset() + output.release() + } + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/codec/CryptoCodec.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/codec/CryptoCodec.kt new file mode 100644 index 0000000..5e4de30 --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/codec/CryptoCodec.kt @@ -0,0 +1,22 @@ +package com.github.creeper123123321.viaaas.codec + +import io.netty.buffer.ByteBuf +import io.netty.channel.ChannelHandlerContext +import io.netty.handler.codec.MessageToMessageCodec +import javax.crypto.Cipher + +class CryptoCodec(val cipherDecode: Cipher, var cipherEncode: Cipher) : MessageToMessageCodec() { + override fun decode(ctx: ChannelHandlerContext, msg: ByteBuf, out: MutableList) { + val i = msg.readerIndex() + val size = msg.readableBytes() + msg.writerIndex(i + cipherDecode.update(msg.nioBuffer(), msg.nioBuffer(i, cipherDecode.getOutputSize(size)))) + out.add(msg.retain()) + } + + override fun encode(ctx: ChannelHandlerContext, msg: ByteBuf, out: MutableList) { + val i = msg.readerIndex() + val size = msg.readableBytes() + msg.writerIndex(i + cipherEncode.update(msg.nioBuffer(), msg.nioBuffer(i, cipherEncode.getOutputSize(size)))) + out.add(msg.retain()) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/codec/FrameCodec.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/codec/FrameCodec.kt new file mode 100644 index 0000000..fb58a9b --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/codec/FrameCodec.kt @@ -0,0 +1,43 @@ +package com.github.creeper123123321.viaaas.codec + +import com.github.creeper123123321.viaaas.badLength +import io.netty.buffer.ByteBuf +import io.netty.channel.ChannelHandlerContext +import io.netty.handler.codec.ByteToMessageCodec +import us.myles.ViaVersion.api.type.Type + +class FrameCodec : ByteToMessageCodec() { + override fun decode(ctx: ChannelHandlerContext, input: ByteBuf, out: MutableList) { + if (!ctx.channel().isActive) { + input.clear() // Ignore, should prevent DoS https://github.com/SpigotMC/BungeeCord/pull/2908 + return + } + + val index = input.readerIndex() + var nByte = 0 + val result = input.forEachByte { + nByte++ + val hasNext = it.toInt().and(0x10000000) != 0 + if (nByte > 3) throw badLength + hasNext + } + input.readerIndex(index) + if (result == -1) return // not readable + + val length = Type.VAR_INT.readPrimitive(input) + + if (length >= 2097152 || length < 0) throw badLength + if (!input.isReadable(length)) { + input.readerIndex(index) + return + } + + out.add(input.readRetainedSlice(length)) + } + + override fun encode(ctx: ChannelHandlerContext, msg: ByteBuf, out: ByteBuf) { + if (msg.readableBytes() >= 2097152) throw badLength + Type.VAR_INT.writePrimitive(out, msg.readableBytes()) + out.writeBytes(msg) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/codec/MinecraftCodec.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/codec/MinecraftCodec.kt new file mode 100644 index 0000000..25f29c0 --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/codec/MinecraftCodec.kt @@ -0,0 +1,36 @@ +package com.github.creeper123123321.viaaas.codec + +import com.github.creeper123123321.viaaas.handler.CloudMinecraftHandler +import com.github.creeper123123321.viaaas.packet.Packet +import com.github.creeper123123321.viaaas.packet.PacketRegistry +import io.netty.buffer.ByteBuf +import io.netty.buffer.ByteBufAllocator +import io.netty.channel.ChannelHandlerContext +import io.netty.handler.codec.MessageToMessageCodec + +class MinecraftCodec : MessageToMessageCodec() { + override fun encode(ctx: ChannelHandlerContext, msg: Packet, out: MutableList) { + if (!ctx.channel().isActive) return + val buf = ByteBufAllocator.DEFAULT.buffer() + try { + val handler = ctx.pipeline().get(CloudMinecraftHandler::class.java) + PacketRegistry.encode(msg, buf, handler.data.frontVer!!) + out.add(buf.retain()) + } finally { + buf.release() + } + } + + override fun decode(ctx: ChannelHandlerContext, msg: ByteBuf, out: MutableList) { + if (!ctx.channel().isActive || !msg.isReadable) return + val handler = ctx.pipeline().get(CloudMinecraftHandler::class.java) + out.add( + PacketRegistry.decode( + msg, + handler.data.frontVer ?: 0, + handler.data.state.state, handler.frontEnd + ) + ) + if (msg.isReadable) throw IllegalStateException("Remaining bytes!!!") + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/command/CloudCommands.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/command/CloudCommands.kt new file mode 100644 index 0000000..f43e359 --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/command/CloudCommands.kt @@ -0,0 +1,5 @@ +package com.github.creeper123123321.viaaas.command + +import us.myles.ViaVersion.commands.ViaCommandHandler + +object CloudCommands : ViaCommandHandler() \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/command/VIAaaSConsole.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/command/VIAaaSConsole.kt new file mode 100644 index 0000000..85d1de2 --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/command/VIAaaSConsole.kt @@ -0,0 +1,116 @@ +package com.github.creeper123123321.viaaas.command + +import com.github.creeper123123321.viaaas.handler.CloudMinecraftHandler +import com.github.creeper123123321.viaaas.runningServer +import com.github.creeper123123321.viaaas.viaaasLogger +import com.github.creeper123123321.viaaas.viaaasVer +import net.minecrell.terminalconsole.SimpleTerminalConsole +import org.jline.reader.Candidate +import org.jline.reader.LineReader +import org.jline.reader.LineReaderBuilder +import org.slf4j.LoggerFactory +import us.myles.ViaVersion.api.Via +import us.myles.ViaVersion.api.command.ViaCommandSender +import us.myles.ViaVersion.api.protocol.ProtocolVersion +import java.util.* + +class VIAaaSConsole : SimpleTerminalConsole(), ViaCommandSender { + val commands = hashMapOf?, String, Array) -> Unit>() + override fun isRunning(): Boolean = runningServer + + init { + commands["stop"] = { suggestion, _, _ -> if (suggestion == null) this.shutdown() } + commands["end"] = commands["stop"]!! + commands["viaversion"] = { suggestion, _, args -> + if (suggestion == null) { + Via.getManager().commandHandler.onCommand(this, args) + } else { + suggestion.addAll(Via.getManager().commandHandler.onTabComplete(this, args)) + } + } + commands["viaver"] = commands["viaversion"]!! + commands["vvcloud"] = commands["viaversion"]!! + commands["help"] = { suggestion, _, _ -> + if (suggestion == null) sendMessage(commands.entries.groupBy { it.value }.entries.joinToString(", ") { + it.value.joinToString("/") { it.key } + }) + } + commands["?"] = commands["help"]!! + commands["ver"] = { suggestion, _, _ -> + if (suggestion == null) sendMessage(viaaasVer) + } + commands["list"] = { suggestion, _, _ -> + if (suggestion == null) { + sendMessage("List of player connections: ") + Via.getPlatform().connectionManager.connections.forEach { + val backAddr = it.channel?.remoteAddress() + val pVer = it.protocolInfo?.protocolVersion?.let { + ProtocolVersion.getProtocol(it) + } + val backName = it.protocolInfo?.username + val backVer = it.protocolInfo?.serverProtocolVersion?.let { + ProtocolVersion.getProtocol(it) + } + val pAddr = + it.channel?.pipeline()?.get(CloudMinecraftHandler::class.java)?.other?.remoteAddress() + val pName = it.channel?.pipeline()?.get(CloudMinecraftHandler::class.java)?.data?.frontName + sendMessage("$pAddr $pVer ($pName) -> $backVer ($backName) $backAddr") + } + } + } + } + + override fun buildReader(builder: LineReaderBuilder): LineReader { + // Stolen from Velocity + return super.buildReader(builder.appName("VIAaaS").completer { _, line, candidates -> + try { + val cmdArgs = line.line().substring(0, line.cursor()).split(" ") + val alias = cmdArgs[0] + val args = cmdArgs.filterIndexed { i, _ -> i > 0 } + if (cmdArgs.size == 1) { + candidates.addAll(commands.keys.filter { it.startsWith(alias, ignoreCase = true) } + .map { Candidate(it) }) + } else { + val cmd = commands[alias.toLowerCase()] + if (cmd != null) { + val suggestions = mutableListOf() + cmd(suggestions, alias, args.toTypedArray()) + candidates.addAll(suggestions.map(::Candidate)) + } + } + } catch (e: Exception) { + sendMessage("Error completing command: $e") + } + }) + } + + override fun runCommand(command: String) { + val cmd = command.split(" ") + try { + val alias = cmd[0].toLowerCase() + val args = cmd.subList(1, cmd.size).toTypedArray() + val runnable = commands[alias] + if (runnable == null) { + sendMessage("unknown command, try 'help'") + } else { + runnable(null, alias, args) + } + } catch (e: Exception) { + sendMessage("Error running command: $e") + } + } + + override fun shutdown() { + viaaasLogger.info("Shutting down...") + runningServer = false + } + + + override fun sendMessage(p0: String) { + LoggerFactory.getLogger(this.name).info(p0) + } + + override fun hasPermission(p0: String): Boolean = true + override fun getUUID(): UUID = UUID.fromString(name) + override fun getName(): String = "VIAaaS Console" +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/handler/BackEndInit.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/handler/BackEndInit.kt new file mode 100644 index 0000000..60f51a5 --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/handler/BackEndInit.kt @@ -0,0 +1,24 @@ +package com.github.creeper123123321.viaaas.handler + +import com.github.creeper123123321.viaaas.codec.FrameCodec +import com.github.creeper123123321.viaaas.codec.MinecraftCodec +import io.netty.channel.Channel +import io.netty.channel.ChannelInitializer +import io.netty.handler.timeout.ReadTimeoutHandler +import us.myles.ViaVersion.api.data.UserConnection +import us.myles.ViaVersion.api.protocol.ProtocolPipeline +import java.util.concurrent.TimeUnit + +class BackEndInit(val connectionData: ConnectionData) : ChannelInitializer() { + override fun initChannel(ch: Channel) { + val user = UserConnection(ch, true) + ProtocolPipeline(user) + ch.pipeline().addLast("timeout", ReadTimeoutHandler(30, TimeUnit.SECONDS)) + // "crypto" + .addLast("frame", FrameCodec()) + // compress + .addLast("via-codec", CloudViaCodec(user)) + .addLast("mc", MinecraftCodec()) + .addLast("handler", CloudMinecraftHandler(connectionData, connectionData.frontChannel, frontEnd = false)) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/handler/CloudMinecraftHandler.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/handler/CloudMinecraftHandler.kt new file mode 100644 index 0000000..56ad930 --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/handler/CloudMinecraftHandler.kt @@ -0,0 +1,51 @@ +package com.github.creeper123123321.viaaas.handler + +import com.github.creeper123123321.viaaas.packet.Packet +import com.github.creeper123123321.viaaas.mcLogger +import com.github.creeper123123321.viaaas.setAutoRead +import io.netty.channel.Channel +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler +import us.myles.ViaVersion.exception.CancelCodecException +import java.net.SocketAddress + +class CloudMinecraftHandler( + val data: ConnectionData, + var other: Channel?, + val frontEnd: Boolean +) : SimpleChannelInboundHandler() { + var remoteAddress: SocketAddress? = null + + override fun channelRead0(ctx: ChannelHandlerContext, packet: Packet) { + if (ctx.channel().isActive) { + data.state.handlePacket(this, ctx, packet) + } + } + + override fun channelActive(ctx: ChannelHandlerContext) { + remoteAddress = ctx.channel().remoteAddress() + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + other?.close() + data.state.onInactivated(this) + } + + override fun channelReadComplete(ctx: ChannelHandlerContext?) { + other?.flush() + } + + override fun channelWritabilityChanged(ctx: ChannelHandlerContext) { + other?.setAutoRead(ctx.channel().isWritable) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + if (cause is CancelCodecException) return + mcLogger.debug("Exception: ", cause) + disconnect("Exception: $cause") + } + + fun disconnect(s: String) { + data.state.disconnect(this, s) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/handler/CloudViaCodec.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/handler/CloudViaCodec.kt new file mode 100644 index 0000000..eee7a29 --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/handler/CloudViaCodec.kt @@ -0,0 +1,40 @@ +package com.github.creeper123123321.viaaas.handler + +import io.netty.buffer.ByteBuf +import io.netty.channel.ChannelHandlerContext +import io.netty.handler.codec.MessageToMessageCodec +import us.myles.ViaVersion.api.data.UserConnection +import us.myles.ViaVersion.exception.CancelDecoderException +import us.myles.ViaVersion.exception.CancelEncoderException + +class CloudViaCodec(val info: UserConnection) : MessageToMessageCodec() { + override fun decode(ctx: ChannelHandlerContext, bytebuf: ByteBuf, out: MutableList) { + if (!info.checkIncomingPacket()) throw CancelDecoderException.generate(null) + if (!info.shouldTransformPacket()) { + out.add(bytebuf.retain()) + return + } + val transformedBuf: ByteBuf = ctx.alloc().buffer().writeBytes(bytebuf) + try { + info.transformIncoming(transformedBuf, CancelDecoderException::generate) + out.add(transformedBuf.retain()) + } finally { + transformedBuf.release() + } + } + + override fun encode(ctx: ChannelHandlerContext, bytebuf: ByteBuf, out: MutableList) { + if (!info.checkOutgoingPacket()) throw CancelEncoderException.generate(null) + if (!info.shouldTransformPacket()) { + out.add(bytebuf.retain()) + return + } + val transformedBuf: ByteBuf = ctx.alloc().buffer().writeBytes(bytebuf) + try { + info.transformOutgoing(transformedBuf, CancelEncoderException::generate) + out.add(transformedBuf.retain()) + } finally { + transformedBuf.release() + } + } +} diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/handler/ConnectionData.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/handler/ConnectionData.kt new file mode 100644 index 0000000..1e90a0d --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/handler/ConnectionData.kt @@ -0,0 +1,19 @@ +package com.github.creeper123123321.viaaas.handler + +import com.github.creeper123123321.viaaas.handler.state.HandshakeState +import com.github.creeper123123321.viaaas.handler.state.MinecraftConnectionState +import io.netty.channel.Channel + +class ConnectionData( + val frontChannel: Channel, + var backChannel: Channel? = null, + var state: MinecraftConnectionState = HandshakeState(), + var frontOnline: Boolean? = null, // todo + var frontName: String? = null, + var backName: String? = null, + var frontVer: Int? = null, + var backVer: Int? = null, +) { + val frontHandler get() = frontChannel.pipeline().get(CloudMinecraftHandler::class.java) + val backHandler get() = backChannel?.pipeline()?.get(CloudMinecraftHandler::class.java) +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/handler/FrontEndInit.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/handler/FrontEndInit.kt new file mode 100644 index 0000000..5fe404c --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/handler/FrontEndInit.kt @@ -0,0 +1,26 @@ +package com.github.creeper123123321.viaaas.handler + +import com.github.creeper123123321.viaaas.codec.FrameCodec +import com.github.creeper123123321.viaaas.codec.MinecraftCodec +import io.netty.channel.Channel +import io.netty.channel.ChannelInitializer +import io.netty.handler.flow.FlowControlHandler +import io.netty.handler.timeout.ReadTimeoutHandler +import java.util.concurrent.TimeUnit + +object FrontEndInit : ChannelInitializer() { + override fun initChannel(ch: Channel) { + ch.pipeline() + .addLast("timeout", ReadTimeoutHandler(30, TimeUnit.SECONDS)) + // "crypto" + .addLast("frame", FrameCodec()) + // "compress" + .addLast("flow-handler", FlowControlHandler()) + .addLast("mc", MinecraftCodec()) + .addLast( + "handler", CloudMinecraftHandler( + ConnectionData(frontChannel = ch), other = null, frontEnd = true + ) + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/handler/Util.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/handler/Util.kt new file mode 100644 index 0000000..3d254ed --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/handler/Util.kt @@ -0,0 +1,9 @@ +package com.github.creeper123123321.viaaas.handler + +import com.github.creeper123123321.viaaas.packet.Packet +import com.github.creeper123123321.viaaas.send + + +fun forward(handler: CloudMinecraftHandler, packet: Packet, flush: Boolean = false) { + send(handler.other!!, packet, flush) +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/handler/state/HandshakeState.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/handler/state/HandshakeState.kt new file mode 100644 index 0000000..b3efde1 --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/handler/state/HandshakeState.kt @@ -0,0 +1,118 @@ +package com.github.creeper123123321.viaaas.handler.state + +import com.github.creeper123123321.viaaas.* +import com.github.creeper123123321.viaaas.packet.handshake.Handshake +import com.github.creeper123123321.viaaas.packet.Packet +import com.github.creeper123123321.viaaas.config.VIAaaSConfig +import com.github.creeper123123321.viaaas.handler.BackEndInit +import com.github.creeper123123321.viaaas.handler.CloudMinecraftHandler +import com.github.creeper123123321.viaaas.handler.forward +import io.netty.bootstrap.Bootstrap +import io.netty.channel.ChannelFuture +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.ChannelOption +import io.netty.channel.socket.SocketChannel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import us.myles.ViaVersion.packets.State +import java.net.InetAddress +import java.net.InetSocketAddress + +class HandshakeState : MinecraftConnectionState { + fun connectBack(handler: CloudMinecraftHandler, socketAddr: InetSocketAddress): ChannelFuture { + return Bootstrap() + .handler(BackEndInit(handler.data)) + .channelFactory(channelSocketFactory()) + .group(handler.data.frontChannel.eventLoop()) + .option(ChannelOption.IP_TOS, 0x18) + .option(ChannelOption.TCP_NODELAY, true) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 15_000) // Half of mc timeout + .connect(socketAddr) + } + + override val state: State + get() = State.HANDSHAKE + + override fun handlePacket(handler: CloudMinecraftHandler, ctx: ChannelHandlerContext, packet: Packet) { + if (packet !is Handshake) throw IllegalArgumentException("Invalid packet!") + + handler.data.frontVer = packet.protocolId + when (packet.nextState.ordinal) { + 1 -> handler.data.state = StatusState + 2 -> handler.data.state = LoginState() + else -> throw IllegalStateException("Invalid next state") + } + + val parsed = VIAaaSAddress().parse(packet.address.substringBefore(0.toChar()), VIAaaSConfig.hostName) + val backProto = parsed.protocol ?: 47 // todo autodetection + val hadHostname = parsed.viaSuffix != null + + packet.address = parsed.serverAddress!! + packet.port = parsed.port ?: if (VIAaaSConfig.defaultBackendPort == -1) { + packet.port + } else { + VIAaaSConfig.defaultBackendPort + } + + handler.data.backVer = backProto + handler.data.frontOnline = parsed.online + handler.data.backName = parsed.username + + val playerAddr = handler.data.frontHandler.remoteAddress + mcLogger.info("Connecting $playerAddr (${handler.data.frontVer}) -> ${packet.address}:${packet.port} ($backProto)") + + if (!hadHostname && VIAaaSConfig.requireHostName) { + throw UnsupportedOperationException("This VIAaaS instance requires you to use the hostname") + } + + handler.data.frontChannel.setAutoRead(false) + GlobalScope.launch(Dispatchers.IO) { + try { + val srvResolved = resolveSrv(packet.address, packet.port) + packet.address = srvResolved.first + packet.port = srvResolved.second + + val socketAddr = InetSocketAddress(InetAddress.getByName(packet.address), packet.port) + + if (checkLocalAddress(socketAddr.address) + || matchesAddress(socketAddr, VIAaaSConfig.blockedBackAddresses) + || !matchesAddress(socketAddr, VIAaaSConfig.allowedBackAddresses) + ) { + throw SecurityException("Not allowed") + } + + val future = connectBack(handler, socketAddr) + + future.addListener { + if (it.isSuccess) { + mcLogger.info("Connected ${handler.remoteAddress} -> $socketAddr") + + val backChan = future.channel() as SocketChannel + handler.data.backChannel = backChan + handler.other = backChan + + forward(handler, packet, true) + + handler.data.frontChannel.setAutoRead(true) + } else { + // We're in the event loop + handler.disconnect("Couldn't connect: " + it.cause().toString()) + } + } + } catch (e: Exception) { + handler.data.frontChannel.eventLoop().submit { + handler.disconnect("Couldn't connect: $e") + } + } + } + } + + override fun disconnect(handler: CloudMinecraftHandler, msg: String) { + handler.data.frontChannel.close() // Not worth logging + } + + override fun onInactivated(handler: CloudMinecraftHandler) { + // Not worth logging + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/handler/state/LoginState.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/handler/state/LoginState.kt new file mode 100644 index 0000000..dbf768f --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/handler/state/LoginState.kt @@ -0,0 +1,210 @@ +package com.github.creeper123123321.viaaas.handler.state + +import com.github.creeper123123321.viaaas.* +import com.github.creeper123123321.viaaas.codec.CompressionCodec +import com.github.creeper123123321.viaaas.codec.CryptoCodec +import com.github.creeper123123321.viaaas.packet.* +import com.github.creeper123123321.viaaas.packet.login.* +import com.github.creeper123123321.viaaas.handler.CloudMinecraftHandler +import com.github.creeper123123321.viaaas.handler.forward +import com.google.common.net.UrlEscapers +import com.google.gson.Gson +import com.google.gson.JsonObject +import io.ktor.client.request.* +import io.netty.channel.ChannelHandlerContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import us.myles.ViaVersion.packets.State +import java.util.* +import java.util.concurrent.CompletableFuture +import javax.crypto.Cipher + +class LoginState : MinecraftConnectionState { + val callbackPlayerId = CompletableFuture() + lateinit var frontToken: ByteArray + lateinit var frontServerId: String + override val state: State + get() = State.LOGIN + + override fun handlePacket(handler: CloudMinecraftHandler, ctx: ChannelHandlerContext, packet: Packet) { + when (packet) { + is LoginStart -> handleLoginStart(handler, packet) + is CryptoResponse -> handleCryptoResponse(handler, packet) + is PluginResponse -> forward(handler, packet) + is LoginDisconnect -> forward(handler, packet) + is CryptoRequest -> handleCryptoRequest(handler, packet) + is LoginSuccess -> handleLoginSuccess(handler, packet) + is SetCompression -> handleCompression(handler, packet) + is PluginRequest -> forward(handler, packet) + else -> throw IllegalArgumentException("Invalid packet!") + } + } + + private fun handleLoginSuccess(handler: CloudMinecraftHandler, loginSuccess: LoginSuccess) { + handler.data.state = PlayState + forward(handler, loginSuccess) + } + + private fun handleCompression(handler: CloudMinecraftHandler, setCompression: SetCompression) { + val pipe = handler.data.frontChannel.pipeline() + val threshold = setCompression.threshold + + val backPipe = pipe.get(CloudMinecraftHandler::class.java).other!!.pipeline() + if (threshold != -1) { + backPipe.addAfter("frame", "compress", CompressionCodec(threshold)) + } else if (backPipe.get("compress") != null) { + backPipe.remove("compress") + } + + forward(handler, setCompression) + + if (threshold != -1) { + pipe.addAfter("frame", "compress", CompressionCodec(threshold)) + // todo viarewind backend compression + } else if (pipe.get("compress") != null) { + pipe.remove("compress") + } + } + + fun authenticateOnlineFront(frontHandler: CloudMinecraftHandler) { + val id = "VIAaaS" + ByteArray(10).let { + secureRandom.nextBytes(it) + Base64.getEncoder().withoutPadding().encodeToString(it) + // https://developer.mozilla.org/en-US/docs/Glossary/Base64 133% of original + } + // We'll use non-vanilla server id, public key size and token size + val token = ByteArray(16).let { + secureRandom.nextBytes(it) + it + } + frontToken = token + frontServerId = id + + val cryptoRequest = CryptoRequest() + cryptoRequest.serverId = id + cryptoRequest.publicKey = mcCryptoKey.public + cryptoRequest.token = token + + send(frontHandler.data.frontChannel, cryptoRequest, true) + } + + fun handleCryptoRequest(handler: CloudMinecraftHandler, cryptoRequest: CryptoRequest) { + val data = handler.data + val backServerId = cryptoRequest.serverId + val backPublicKey = cryptoRequest.publicKey + val backToken = cryptoRequest.token + + if (data.frontOnline == null) { + authenticateOnlineFront(handler) + } + + val backKey = ByteArray(16).let { + secureRandom.nextBytes(it) + it + } + val backHash = generateServerHash(backServerId, backKey, backPublicKey) + + callbackPlayerId.whenComplete { playerId, e -> + if (e != null) return@whenComplete + val frontHandler = handler.data.frontHandler + GlobalScope.launch(Dispatchers.IO) { + try { + val sessionJoin = viaWebServer.requestSessionJoin( + parseUndashedId(playerId), + handler.data.backName!!, + backHash, + frontHandler.remoteAddress!!, // Frontend handler + backPublicKey + ) + + val backChan = handler.data.backChannel!! + sessionJoin.whenCompleteAsync({ _, throwable -> + if (throwable != null) { + frontHandler.data.backHandler!!.disconnect("Online mode error: $throwable") + } else { + val cryptoResponse = CryptoResponse() + cryptoResponse.encryptedKey = encryptRsa(backPublicKey, backKey) + cryptoResponse.encryptedToken = encryptRsa(backPublicKey, backToken) + forward(frontHandler, cryptoResponse, true) + + val backAesEn = mcCfb8(backKey, Cipher.ENCRYPT_MODE) + val backAesDe = mcCfb8(backKey, Cipher.DECRYPT_MODE) + backChan.pipeline().addBefore("frame", "crypto", CryptoCodec(backAesDe, backAesEn)) + } + }, backChan.eventLoop()) + } catch (e: Exception) { + frontHandler.disconnect("Online mode error: $e") + } + } + } + } + + fun handleCryptoResponse(handler: CloudMinecraftHandler, cryptoResponse: CryptoResponse) { + val frontHash = let { + val frontKey = decryptRsa(mcCryptoKey.private, cryptoResponse.encryptedKey) + // RSA token - wat??? why is it encrypted with RSA if it was sent unencrypted? + val decryptedToken = decryptRsa(mcCryptoKey.private, cryptoResponse.encryptedToken) + + if (!decryptedToken.contentEquals(frontToken)) throw IllegalStateException("invalid token!") + + val aesEn = mcCfb8(frontKey, Cipher.ENCRYPT_MODE) + val aesDe = mcCfb8(frontKey, Cipher.DECRYPT_MODE) + + handler.data.frontChannel.pipeline().addBefore("frame", "crypto", CryptoCodec(aesDe, aesEn)) + + generateServerHash(frontServerId, frontKey, mcCryptoKey.public) + } + + handler.data.frontChannel.setAutoRead(false) + GlobalScope.launch(Dispatchers.IO) { + try { + val profile = httpClient.get( + "https://sessionserver.mojang.com/session/minecraft/hasJoined?username=" + + UrlEscapers.urlFormParameterEscaper().escape(handler.data.frontName!!) + + "&serverId=$frontHash" + ) ?: throw IllegalArgumentException("Couldn't authenticate with session servers") + + val id = profile.get("id")!!.asString + mcLogger.info("Validated front-end session: ${handler.data.frontName} $id") + callbackPlayerId.complete(id) + } catch (e: Exception) { + callbackPlayerId.completeExceptionally(e) + } + handler.data.frontChannel.setAutoRead(true) + } + } + + fun handleLoginStart(handler: CloudMinecraftHandler, loginStart: LoginStart) { + if (loginStart.username.length > 16) throw badLength + if (handler.data.frontName != null) throw IllegalStateException("Login already started") + + handler.data.frontName = loginStart.username + handler.data.backName = handler.data.backName ?: handler.data.frontName + + loginStart.username = handler.data.backName!! + + callbackPlayerId.whenComplete { _, e -> if (e != null) disconnect(handler, "Profile error: $e") } + + if (handler.data.frontOnline == false) { + callbackPlayerId.complete(generateOfflinePlayerUuid(handler.data.frontName!!).toString().replace("-", "")) + } + + if (handler.data.frontOnline == true) { // forced + authenticateOnlineFront(handler.data.backHandler!!) + callbackPlayerId.whenComplete { _, e -> + if (e == null) forward(handler, loginStart, true) + } + } else { + forward(handler, loginStart) + } + } + + override fun disconnect(handler: CloudMinecraftHandler, msg: String) { + super.disconnect(handler, msg) + + val packet = LoginDisconnect() + packet.msg = Gson().toJson("[VIAaaS] §c$msg") + writeFlushClose(handler.data.frontChannel, packet) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/handler/state/MinecraftConnectionState.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/handler/state/MinecraftConnectionState.kt new file mode 100644 index 0000000..e896697 --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/handler/state/MinecraftConnectionState.kt @@ -0,0 +1,23 @@ +package com.github.creeper123123321.viaaas.handler.state + +import com.github.creeper123123321.viaaas.packet.Packet +import com.github.creeper123123321.viaaas.handler.CloudMinecraftHandler +import com.github.creeper123123321.viaaas.mcLogger +import io.netty.channel.ChannelHandlerContext +import us.myles.ViaVersion.packets.State + +interface MinecraftConnectionState { + val state: State + fun handlePacket( + handler: CloudMinecraftHandler, ctx: ChannelHandlerContext, + packet: Packet + ) + + fun disconnect(handler: CloudMinecraftHandler, msg: String) { + mcLogger.info("Disconnected ${handler.remoteAddress}: $msg") + } + + fun onInactivated(handler: CloudMinecraftHandler) { + mcLogger.info(handler.remoteAddress?.toString() + " inactivated") + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/handler/state/PlayState.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/handler/state/PlayState.kt new file mode 100644 index 0000000..f9ace9b --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/handler/state/PlayState.kt @@ -0,0 +1,23 @@ +package com.github.creeper123123321.viaaas.handler.state + +import com.github.creeper123123321.viaaas.packet.Packet +import com.github.creeper123123321.viaaas.packet.UnknownPacket +import com.github.creeper123123321.viaaas.handler.CloudMinecraftHandler +import com.github.creeper123123321.viaaas.handler.forward +import io.netty.channel.ChannelHandlerContext +import us.myles.ViaVersion.packets.State + +object PlayState : MinecraftConnectionState { + override val state: State + get() = State.PLAY + + override fun handlePacket(handler: CloudMinecraftHandler, ctx: ChannelHandlerContext, packet: Packet) { + if ((packet as UnknownPacket).id !in 0..127) throw IllegalArgumentException("Invalid packet id!") + forward(handler, packet) + } + + override fun disconnect(handler: CloudMinecraftHandler, msg: String) { + super.disconnect(handler, msg) + handler.data.frontChannel.close() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/handler/state/StatusState.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/handler/state/StatusState.kt new file mode 100644 index 0000000..2a2c470 --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/handler/state/StatusState.kt @@ -0,0 +1,30 @@ +package com.github.creeper123123321.viaaas.handler.state + +import com.github.creeper123123321.viaaas.packet.Packet +import com.github.creeper123123321.viaaas.packet.status.StatusResponse +import com.github.creeper123123321.viaaas.packet.UnknownPacket +import com.github.creeper123123321.viaaas.handler.CloudMinecraftHandler +import com.github.creeper123123321.viaaas.handler.forward +import com.github.creeper123123321.viaaas.writeFlushClose +import com.google.gson.Gson +import io.netty.channel.ChannelHandlerContext +import us.myles.ViaVersion.packets.State + +object StatusState : MinecraftConnectionState { + override val state: State + get() = State.STATUS + + override fun handlePacket(handler: CloudMinecraftHandler, ctx: ChannelHandlerContext, packet: Packet) { + if (packet is UnknownPacket) throw IllegalArgumentException("Invalid packet") + forward(handler, packet) + } + + override fun disconnect(handler: CloudMinecraftHandler, msg: String) { + super.disconnect(handler, msg) + + val packet = StatusResponse() + packet.json = """{"version": {"name": "VIAaaS", "protocol": -1}, "players": {"max": 0, "online": 0, + | "sample": []}, "description": {"text": ${Gson().toJson("§c$msg")}}}""".trimMargin() + writeFlushClose(handler.data.frontChannel, packet) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/packet/Packet.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/packet/Packet.kt new file mode 100644 index 0000000..0e0acd4 --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/packet/Packet.kt @@ -0,0 +1,11 @@ +package com.github.creeper123123321.viaaas.packet + +import io.netty.buffer.ByteBuf + +/** + * A mutable object which represents a Minecraft packet data + */ +interface Packet { + fun decode(byteBuf: ByteBuf, protocolVersion: Int) + fun encode(byteBuf: ByteBuf, protocolVersion: Int) +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/packet/PacketRegistry.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/packet/PacketRegistry.kt new file mode 100644 index 0000000..319e5c0 --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/packet/PacketRegistry.kt @@ -0,0 +1,90 @@ +package com.github.creeper123123321.viaaas.packet + +import com.github.creeper123123321.viaaas.packet.handshake.Handshake +import com.github.creeper123123321.viaaas.packet.login.* +import com.github.creeper123123321.viaaas.packet.status.StatusPing +import com.github.creeper123123321.viaaas.packet.status.StatusPong +import com.github.creeper123123321.viaaas.packet.status.StatusRequest +import com.github.creeper123123321.viaaas.packet.status.StatusResponse +import com.google.common.collect.Range +import io.netty.buffer.ByteBuf +import us.myles.ViaVersion.api.protocol.ProtocolVersion +import us.myles.ViaVersion.api.type.Type +import us.myles.ViaVersion.packets.State +import java.util.function.Supplier + +object PacketRegistry { + val entries = mutableListOf() + + init { + register(Range.all(), State.HANDSHAKE, 0, true, ::Handshake) + register(Range.all(), State.LOGIN, 0, true, ::LoginStart) + register(Range.all(), State.LOGIN, 1, true, ::CryptoResponse) + register(Range.atLeast(ProtocolVersion.v1_13.version), State.LOGIN, 2, true, ::PluginResponse) + register(Range.all(), State.LOGIN, 0, false, ::LoginDisconnect) + register(Range.all(), State.LOGIN, 1, false, ::CryptoRequest) + register(Range.all(), State.LOGIN, 2, false, ::LoginSuccess) + register(Range.all(), State.LOGIN, 3, false, ::SetCompression) + register(Range.all(), State.LOGIN, 4, false, ::PluginRequest) + register(Range.all(), State.STATUS, 0, true, ::StatusRequest) + register(Range.all(), State.STATUS, 1, true, ::StatusPing) + register(Range.all(), State.STATUS, 0, false, ::StatusResponse) + register(Range.all(), State.STATUS, 1, false, ::StatusPong) + } + + inline fun register( + protocol: Range, + state: State, + id: Int, + serverBound: Boolean, + constructor: Supplier

+ ) { + entries.add(RegistryEntry(protocol, state, id, serverBound, constructor, P::class.java)) + } + + data class RegistryEntry( + val versionRange: Range, + val state: State, + val id: Int, + val serverBound: Boolean, + val constructor: Supplier, + val packetClass: Class + ) + + fun getPacketConstructor( + protocolVersion: Int, + state: State, + id: Int, + serverBound: Boolean + ): Supplier? { + return entries.firstOrNull { + it.serverBound == serverBound && it.state == state + && it.versionRange.contains(protocolVersion) && it.id == id + }?.constructor + } + + fun getPacketId(packetClass: Class, protocolVersion: Int): Int? { + return entries.firstOrNull { + it.versionRange.contains(protocolVersion) && it.packetClass == packetClass + }?.id + } + + fun decode(byteBuf: ByteBuf, protocolVersion: Int, state: State, serverBound: Boolean): Packet { + val packetId = Type.VAR_INT.readPrimitive(byteBuf) + val packet = + getPacketConstructor(protocolVersion, state, packetId, serverBound)?.get() ?: UnknownPacket(packetId) + packet.decode(byteBuf, protocolVersion) + if (byteBuf.isReadable) throw IllegalStateException("Remaining bytes!") + return packet + } + + fun encode(packet: Packet, byteBuf: ByteBuf, protocolVersion: Int) { + val id = if (packet is UnknownPacket) { + packet.id + } else { + getPacketId(packet.javaClass, protocolVersion)!! + } + Type.VAR_INT.writePrimitive(byteBuf, id) + packet.encode(byteBuf, protocolVersion) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/packet/UnknownPacket.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/packet/UnknownPacket.kt new file mode 100644 index 0000000..30ed108 --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/packet/UnknownPacket.kt @@ -0,0 +1,15 @@ +package com.github.creeper123123321.viaaas.packet + +import io.netty.buffer.ByteBuf + +class UnknownPacket(val id: Int) : Packet { + lateinit var content: ByteArray + + override fun decode(byteBuf: ByteBuf, protocolVersion: Int) { + content = ByteArray(byteBuf.readableBytes()).also { byteBuf.readBytes(it) } + } + + override fun encode(byteBuf: ByteBuf, protocolVersion: Int) { + byteBuf.writeBytes(content) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/packet/handshake/Handshake.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/packet/handshake/Handshake.kt new file mode 100644 index 0000000..441a243 --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/packet/handshake/Handshake.kt @@ -0,0 +1,28 @@ +package com.github.creeper123123321.viaaas.packet.handshake + +import com.github.creeper123123321.viaaas.packet.Packet +import io.netty.buffer.ByteBuf +import us.myles.ViaVersion.api.type.Type +import us.myles.ViaVersion.packets.State +import kotlin.properties.Delegates + +class Handshake : Packet { + var protocolId by Delegates.notNull() + lateinit var address: String + var port by Delegates.notNull() + lateinit var nextState: State + + override fun decode(byteBuf: ByteBuf, protocolVersion: Int) { + protocolId = Type.VAR_INT.readPrimitive(byteBuf) + address = Type.STRING.read(byteBuf) + port = byteBuf.readUnsignedShort() + nextState = State.values()[Type.VAR_INT.readPrimitive(byteBuf)] + } + + override fun encode(byteBuf: ByteBuf, protocolVersion: Int) { + Type.VAR_INT.writePrimitive(byteBuf, protocolId) + Type.STRING.write(byteBuf, address) + byteBuf.writeShort(port) + byteBuf.writeByte(nextState.ordinal) // var int is too small, fits in a byte + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/packet/login/CryptoRequest.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/packet/login/CryptoRequest.kt new file mode 100644 index 0000000..11ccbe0 --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/packet/login/CryptoRequest.kt @@ -0,0 +1,42 @@ +package com.github.creeper123123321.viaaas.packet.login + +import com.github.creeper123123321.viaaas.packet.Packet +import io.netty.buffer.ByteBuf +import us.myles.ViaVersion.api.protocol.ProtocolVersion +import us.myles.ViaVersion.api.type.Type +import java.security.KeyFactory +import java.security.PublicKey +import java.security.spec.X509EncodedKeySpec + +class CryptoRequest : Packet { + lateinit var serverId: String + lateinit var publicKey: PublicKey + lateinit var token: ByteArray + + override fun decode(byteBuf: ByteBuf, protocolVersion: Int) { + serverId = Type.STRING.read(byteBuf) + if (protocolVersion >= ProtocolVersion.v1_8.version) { + publicKey = KeyFactory.getInstance("RSA") + .generatePublic(X509EncodedKeySpec(Type.BYTE_ARRAY_PRIMITIVE.read(byteBuf))) + token = Type.BYTE_ARRAY_PRIMITIVE.read(byteBuf) + } else { + publicKey = KeyFactory.getInstance("RSA") + .generatePublic(X509EncodedKeySpec(ByteArray(byteBuf.readUnsignedShort()).also { byteBuf.readBytes(it) })) + token = ByteArray(byteBuf.readUnsignedShort()).also { byteBuf.readBytes(it) } + } + } + + override fun encode(byteBuf: ByteBuf, protocolVersion: Int) { + Type.STRING.write(byteBuf, serverId) + if (protocolVersion >= ProtocolVersion.v1_8.version) { + Type.BYTE_ARRAY_PRIMITIVE.write(byteBuf, publicKey.encoded) + Type.BYTE_ARRAY_PRIMITIVE.write(byteBuf, token) + } else { + val encodedKey = publicKey.encoded + byteBuf.writeShort(encodedKey.size) + byteBuf.writeBytes(encodedKey) + byteBuf.writeShort(token.size) + byteBuf.writeBytes(token) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/packet/login/CryptoResponse.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/packet/login/CryptoResponse.kt new file mode 100644 index 0000000..6937faf --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/packet/login/CryptoResponse.kt @@ -0,0 +1,33 @@ +package com.github.creeper123123321.viaaas.packet.login + +import com.github.creeper123123321.viaaas.packet.Packet +import io.netty.buffer.ByteBuf +import us.myles.ViaVersion.api.protocol.ProtocolVersion +import us.myles.ViaVersion.api.type.Type + +class CryptoResponse : Packet { + lateinit var encryptedKey: ByteArray + lateinit var encryptedToken: ByteArray + + override fun decode(byteBuf: ByteBuf, protocolVersion: Int) { + if (protocolVersion >= ProtocolVersion.v1_8.version) { + encryptedKey = Type.BYTE_ARRAY_PRIMITIVE.read(byteBuf) + encryptedToken = Type.BYTE_ARRAY_PRIMITIVE.read(byteBuf) + } else { + encryptedKey = ByteArray(byteBuf.readUnsignedShort()).also { byteBuf.readBytes(it) } + encryptedToken = ByteArray(byteBuf.readUnsignedShort()).also { byteBuf.readBytes(it) } + } + } + + override fun encode(byteBuf: ByteBuf, protocolVersion: Int) { + if (protocolVersion >= ProtocolVersion.v1_8.version) { + Type.BYTE_ARRAY_PRIMITIVE.write(byteBuf, encryptedKey) + Type.BYTE_ARRAY_PRIMITIVE.write(byteBuf, encryptedToken) + } else { + byteBuf.writeShort(encryptedKey.size) + byteBuf.writeBytes(encryptedKey) + byteBuf.writeShort(encryptedToken.size) + byteBuf.writeBytes(encryptedToken) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/packet/login/LoginDisconnect.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/packet/login/LoginDisconnect.kt new file mode 100644 index 0000000..e905ef4 --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/packet/login/LoginDisconnect.kt @@ -0,0 +1,16 @@ +package com.github.creeper123123321.viaaas.packet.login + +import com.github.creeper123123321.viaaas.packet.Packet +import io.netty.buffer.ByteBuf +import us.myles.ViaVersion.api.type.Type + +class LoginDisconnect : Packet { + lateinit var msg: String + override fun decode(byteBuf: ByteBuf, protocolVersion: Int) { + msg = Type.STRING.read(byteBuf) + } + + override fun encode(byteBuf: ByteBuf, protocolVersion: Int) { + Type.STRING.write(byteBuf, msg) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/packet/login/LoginStart.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/packet/login/LoginStart.kt new file mode 100644 index 0000000..02178f5 --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/packet/login/LoginStart.kt @@ -0,0 +1,17 @@ +package com.github.creeper123123321.viaaas.packet.login + +import com.github.creeper123123321.viaaas.packet.Packet +import io.netty.buffer.ByteBuf +import us.myles.ViaVersion.api.type.Type + +class LoginStart : Packet { + lateinit var username: String + + override fun decode(byteBuf: ByteBuf, protocolVersion: Int) { + username = Type.STRING.read(byteBuf) + } + + override fun encode(byteBuf: ByteBuf, protocolVersion: Int) { + Type.STRING.write(byteBuf, username) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/packet/login/LoginSuccess.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/packet/login/LoginSuccess.kt new file mode 100644 index 0000000..31c31e2 --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/packet/login/LoginSuccess.kt @@ -0,0 +1,39 @@ +package com.github.creeper123123321.viaaas.packet.login + +import com.github.creeper123123321.viaaas.packet.Packet +import com.github.creeper123123321.viaaas.parseUndashedId +import io.netty.buffer.ByteBuf +import us.myles.ViaVersion.api.protocol.ProtocolVersion +import us.myles.ViaVersion.api.type.Type +import java.util.* + +class LoginSuccess : Packet { + lateinit var id: UUID + lateinit var username: String + + override fun decode(byteBuf: ByteBuf, protocolVersion: Int) { + id = when { + protocolVersion >= ProtocolVersion.v1_16.version -> { + Type.UUID_INT_ARRAY.read(byteBuf) + } + protocolVersion >= ProtocolVersion.v1_7_6.version -> { + UUID.fromString(Type.STRING.read(byteBuf)) + } + else -> parseUndashedId(Type.STRING.read(byteBuf)) + } + username = Type.STRING.read(byteBuf) + } + + override fun encode(byteBuf: ByteBuf, protocolVersion: Int) { + when { + protocolVersion >= ProtocolVersion.v1_16.version -> { + Type.UUID_INT_ARRAY.write(byteBuf, id) + } + protocolVersion >= ProtocolVersion.v1_7_6.version -> { + Type.STRING.write(byteBuf, id.toString()) + } + else -> Type.STRING.write(byteBuf, id.toString().replace("-", "")) + } + Type.STRING.write(byteBuf, username) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/packet/login/PluginRequest.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/packet/login/PluginRequest.kt new file mode 100644 index 0000000..422bd19 --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/packet/login/PluginRequest.kt @@ -0,0 +1,23 @@ +package com.github.creeper123123321.viaaas.packet.login + +import com.github.creeper123123321.viaaas.packet.Packet +import io.netty.buffer.ByteBuf +import us.myles.ViaVersion.api.type.Type +import kotlin.properties.Delegates + +class PluginRequest : Packet { + var id by Delegates.notNull() + lateinit var channel: String + lateinit var data: ByteArray + override fun decode(byteBuf: ByteBuf, protocolVersion: Int) { + id = Type.VAR_INT.readPrimitive(byteBuf) + channel = Type.STRING.read(byteBuf) + data = ByteArray(byteBuf.readableBytes()).also { byteBuf.readBytes(it) } + } + + override fun encode(byteBuf: ByteBuf, protocolVersion: Int) { + Type.VAR_INT.writePrimitive(byteBuf, id) + Type.STRING.write(byteBuf, channel) + byteBuf.writeBytes(data) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/packet/login/PluginResponse.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/packet/login/PluginResponse.kt new file mode 100644 index 0000000..5661cd1 --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/packet/login/PluginResponse.kt @@ -0,0 +1,27 @@ +package com.github.creeper123123321.viaaas.packet.login + +import com.github.creeper123123321.viaaas.packet.Packet +import io.netty.buffer.ByteBuf +import us.myles.ViaVersion.api.type.Type +import kotlin.properties.Delegates + +class PluginResponse : Packet { + var id by Delegates.notNull() + var success by Delegates.notNull() + lateinit var data: ByteArray + override fun decode(byteBuf: ByteBuf, protocolVersion: Int) { + id = Type.VAR_INT.readPrimitive(byteBuf) + success = byteBuf.readBoolean() + if (success) { + data = ByteArray(byteBuf.readableBytes()).also { byteBuf.readBytes(it) } + } + } + + override fun encode(byteBuf: ByteBuf, protocolVersion: Int) { + Type.VAR_INT.writePrimitive(byteBuf, id) + byteBuf.writeBoolean(success) + if (success) { + byteBuf.writeBytes(data) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/packet/login/SetCompression.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/packet/login/SetCompression.kt new file mode 100644 index 0000000..0dc809e --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/packet/login/SetCompression.kt @@ -0,0 +1,17 @@ +package com.github.creeper123123321.viaaas.packet.login + +import com.github.creeper123123321.viaaas.packet.Packet +import io.netty.buffer.ByteBuf +import us.myles.ViaVersion.api.type.Type +import kotlin.properties.Delegates + +class SetCompression : Packet { + var threshold by Delegates.notNull() + override fun decode(byteBuf: ByteBuf, protocolVersion: Int) { + threshold = Type.VAR_INT.readPrimitive(byteBuf) + } + + override fun encode(byteBuf: ByteBuf, protocolVersion: Int) { + Type.VAR_INT.writePrimitive(byteBuf, threshold) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/packet/status/StatusPing.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/packet/status/StatusPing.kt new file mode 100644 index 0000000..2b1a2cf --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/packet/status/StatusPing.kt @@ -0,0 +1,16 @@ +package com.github.creeper123123321.viaaas.packet.status + +import com.github.creeper123123321.viaaas.packet.Packet +import io.netty.buffer.ByteBuf +import kotlin.properties.Delegates + +class StatusPing : Packet { + var number by Delegates.notNull() + override fun decode(byteBuf: ByteBuf, protocolVersion: Int) { + number = byteBuf.readLong() + } + + override fun encode(byteBuf: ByteBuf, protocolVersion: Int) { + byteBuf.writeLong(number) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/packet/status/StatusPong.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/packet/status/StatusPong.kt new file mode 100644 index 0000000..8f75e4f --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/packet/status/StatusPong.kt @@ -0,0 +1,17 @@ +package com.github.creeper123123321.viaaas.packet.status + +import com.github.creeper123123321.viaaas.packet.Packet +import io.netty.buffer.ByteBuf +import kotlin.properties.Delegates + +// Some code based on https://github.com/VelocityPowered/Velocity/tree/dev/1.1.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet +class StatusPong : Packet { + var number by Delegates.notNull() + override fun decode(byteBuf: ByteBuf, protocolVersion: Int) { + number = byteBuf.readLong() + } + + override fun encode(byteBuf: ByteBuf, protocolVersion: Int) { + byteBuf.writeLong(number) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/packet/status/StatusRequest.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/packet/status/StatusRequest.kt new file mode 100644 index 0000000..e28082a --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/packet/status/StatusRequest.kt @@ -0,0 +1,12 @@ +package com.github.creeper123123321.viaaas.packet.status + +import com.github.creeper123123321.viaaas.packet.Packet +import io.netty.buffer.ByteBuf + +class StatusRequest: Packet { + override fun decode(byteBuf: ByteBuf, protocolVersion: Int) { + } + + override fun encode(byteBuf: ByteBuf, protocolVersion: Int) { + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/packet/status/StatusResponse.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/packet/status/StatusResponse.kt new file mode 100644 index 0000000..2d81381 --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/packet/status/StatusResponse.kt @@ -0,0 +1,16 @@ +package com.github.creeper123123321.viaaas.packet.status + +import com.github.creeper123123321.viaaas.packet.Packet +import io.netty.buffer.ByteBuf +import us.myles.ViaVersion.api.type.Type + +class StatusResponse : Packet { + lateinit var json: String + override fun decode(byteBuf: ByteBuf, protocolVersion: Int) { + json = Type.STRING.read(byteBuf) + } + + override fun encode(byteBuf: ByteBuf, protocolVersion: Int) { + Type.STRING.write(byteBuf, json) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/platform/CloudBackwards.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/platform/CloudBackwards.kt new file mode 100644 index 0000000..9bc9caa --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/platform/CloudBackwards.kt @@ -0,0 +1,15 @@ +package com.github.creeper123123321.viaaas.platform + +import nl.matsv.viabackwards.api.ViaBackwardsPlatform +import org.slf4j.LoggerFactory +import us.myles.ViaVersion.sponge.util.LoggerWrapper +import java.io.File +import java.util.logging.Logger + +object CloudBackwards : ViaBackwardsPlatform { + val log = LoggerWrapper(LoggerFactory.getLogger("ViaBackwards")) + override fun getDataFolder() = File("config/viabackwards") + override fun getLogger(): Logger = log + override fun disable() { + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/platform/CloudBossBar.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/platform/CloudBossBar.kt new file mode 100644 index 0000000..dd2d93c --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/platform/CloudBossBar.kt @@ -0,0 +1,8 @@ +package com.github.creeper123123321.viaaas.platform + +import us.myles.ViaVersion.api.boss.BossColor +import us.myles.ViaVersion.api.boss.BossStyle +import us.myles.ViaVersion.boss.CommonBoss + +class CloudBossBar(title: String, health: Float, style: BossStyle, color: BossColor) : + CommonBoss(title, health, color, style) \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/platform/CloudInjector.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/platform/CloudInjector.kt new file mode 100644 index 0000000..c55b3ad --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/platform/CloudInjector.kt @@ -0,0 +1,19 @@ +package com.github.creeper123123321.viaaas.platform + +import us.myles.ViaVersion.api.platform.ViaInjector +import us.myles.viaversion.libs.gson.JsonObject + +object CloudInjector : ViaInjector { + override fun getEncoderName(): String = "via-codec" + override fun getDecoderName() = "via-codec" + override fun getDump(): JsonObject = JsonObject() + + override fun uninject() { + } + + override fun inject() { + } + + + override fun getServerProtocolVersion() = 47 // Dummy +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/platform/CloudLoader.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/platform/CloudLoader.kt new file mode 100644 index 0000000..996c779 --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/platform/CloudLoader.kt @@ -0,0 +1,18 @@ +package com.github.creeper123123321.viaaas.platform + +import com.github.creeper123123321.viaaas.provider.CloudVersionProvider +import us.myles.ViaVersion.api.Via +import us.myles.ViaVersion.api.platform.ViaPlatformLoader +import us.myles.ViaVersion.bungee.providers.BungeeMovementTransmitter +import us.myles.ViaVersion.protocols.base.VersionProvider +import us.myles.ViaVersion.protocols.protocol1_9to1_8.providers.MovementTransmitterProvider + +object CloudLoader : ViaPlatformLoader { + override fun unload() { + } + + override fun load() { + Via.getManager().providers.use(MovementTransmitterProvider::class.java, BungeeMovementTransmitter()) + Via.getManager().providers.use(VersionProvider::class.java, CloudVersionProvider) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/platform/CloudPlatform.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/platform/CloudPlatform.kt new file mode 100644 index 0000000..05c3f16 --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/platform/CloudPlatform.kt @@ -0,0 +1,69 @@ +package com.github.creeper123123321.viaaas.platform + +import com.github.creeper123123321.viaaas.* +import com.github.creeper123123321.viaaas.config.CloudViaConfig +import com.google.common.util.concurrent.ThreadFactoryBuilder +import io.netty.channel.DefaultEventLoop +import org.slf4j.LoggerFactory +import us.myles.ViaVersion.api.ViaAPI +import us.myles.ViaVersion.api.ViaVersionConfig +import us.myles.ViaVersion.api.command.ViaCommandSender +import us.myles.ViaVersion.api.configuration.ConfigurationProvider +import us.myles.ViaVersion.api.platform.TaskId +import us.myles.ViaVersion.api.platform.ViaConnectionManager +import us.myles.ViaVersion.api.platform.ViaPlatform +import us.myles.ViaVersion.sponge.VersionInfo +import us.myles.ViaVersion.sponge.util.LoggerWrapper +import us.myles.viaversion.libs.gson.JsonObject +import java.io.File +import java.util.* +import java.util.concurrent.CompletableFuture +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.logging.Logger + +object CloudPlatform : ViaPlatform { + val connMan = ViaConnectionManager() + val executor = Executors.newCachedThreadPool(ThreadFactoryBuilder().setNameFormat("Via-%d").setDaemon(true).build()) + val eventLoop = DefaultEventLoop(executor) + + init { + eventLoop.execute(initFuture::join) + } + + override fun sendMessage(p0: UUID, p1: String) { + // todo + } + + override fun kickPlayer(p0: UUID, p1: String): Boolean = false // todo + override fun getApi(): ViaAPI = CloudViaAPI + override fun getDataFolder(): File = File("viaversion") + override fun getConf(): ViaVersionConfig = CloudViaConfig + override fun onReload() { + } + + override fun getDump(): JsonObject = JsonObject() + override fun runSync(runnable: Runnable): TaskId = CloudTask(eventLoop.submit(runnable)) + override fun runSync(p0: Runnable, p1: Long): TaskId = + CloudTask(eventLoop.schedule(p0, p1 * 50L, TimeUnit.MILLISECONDS)) + + override fun runRepeatingSync(p0: Runnable, p1: Long): TaskId = + CloudTask(eventLoop.scheduleAtFixedRate(p0, 0, p1 * 50L, TimeUnit.MILLISECONDS)) + + override fun runAsync(p0: Runnable): TaskId = CloudTask(CompletableFuture.runAsync(p0, executor)) + override fun getLogger(): Logger = LoggerWrapper(LoggerFactory.getLogger("ViaVersion")) + override fun getConnectionManager(): ViaConnectionManager = connMan + override fun getOnlinePlayers(): Array = arrayOf() + override fun cancelTask(p0: TaskId?) { + (p0 as CloudTask).obj.cancel(false) + } + + override fun isPluginEnabled(): Boolean = true + override fun getConfigurationProvider(): ConfigurationProvider = CloudViaConfig + + override fun getPlatformName(): String = "VIAaaS" + override fun getPlatformVersion(): String = viaaasVer + override fun getPluginVersion(): String = VersionInfo.VERSION + override fun isOldClientsAllowed(): Boolean = true + override fun isProxy(): Boolean = true +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/platform/CloudRewind.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/platform/CloudRewind.kt new file mode 100644 index 0000000..8101f6c --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/platform/CloudRewind.kt @@ -0,0 +1,11 @@ +package com.github.creeper123123321.viaaas.platform + +import de.gerrygames.viarewind.api.ViaRewindPlatform +import org.slf4j.LoggerFactory +import us.myles.ViaVersion.sponge.util.LoggerWrapper +import java.util.logging.Logger + +object CloudRewind : ViaRewindPlatform { + val log = LoggerWrapper(LoggerFactory.getLogger("ViaRewind")) + override fun getLogger(): Logger = log +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/platform/CloudTask.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/platform/CloudTask.kt new file mode 100644 index 0000000..9402dc5 --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/platform/CloudTask.kt @@ -0,0 +1,8 @@ +package com.github.creeper123123321.viaaas.platform + +import us.myles.ViaVersion.api.platform.TaskId +import java.util.concurrent.Future + +class CloudTask(val obj: Future<*>) : TaskId { + override fun getObject(): Any = obj +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/platform/CloudViaAPI.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/platform/CloudViaAPI.kt new file mode 100644 index 0000000..b815423 --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/platform/CloudViaAPI.kt @@ -0,0 +1,22 @@ +package com.github.creeper123123321.viaaas.platform + +import io.netty.buffer.ByteBuf +import us.myles.ViaVersion.api.ViaAPI +import us.myles.ViaVersion.api.boss.BossBar +import us.myles.ViaVersion.api.boss.BossColor +import us.myles.ViaVersion.api.boss.BossStyle +import us.myles.ViaVersion.api.protocol.ProtocolRegistry +import java.util.* +import kotlin.UnsupportedOperationException + +object CloudViaAPI : ViaAPI { + override fun isInjected(p0: UUID): Boolean = false + override fun createBossBar(p0: String, p1: BossColor, p2: BossStyle): BossBar<*> = CloudBossBar(p0, 0f, p2, p1) + override fun createBossBar(p0: String, p1: Float, p2: BossColor, p3: BossStyle): BossBar<*> = CloudBossBar(p0, p1, p3, p2) + override fun sendRawPacket(p0: Unit?, p1: ByteBuf?) = throw UnsupportedOperationException() + override fun sendRawPacket(p0: UUID?, p1: ByteBuf?) = throw UnsupportedOperationException() + override fun getPlayerVersion(p0: Unit?): Int = throw UnsupportedOperationException() + override fun getPlayerVersion(p0: UUID?): Int = throw UnsupportedOperationException() + override fun getVersion(): String = CloudPlatform.pluginVersion + override fun getSupportedVersions(): SortedSet = ProtocolRegistry.getSupportedVersions() +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/provider/CloudVersionProvider.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/provider/CloudVersionProvider.kt new file mode 100644 index 0000000..4f3f87b --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/provider/CloudVersionProvider.kt @@ -0,0 +1,13 @@ +package com.github.creeper123123321.viaaas.provider + +import com.github.creeper123123321.viaaas.handler.CloudMinecraftHandler +import us.myles.ViaVersion.api.data.UserConnection +import us.myles.ViaVersion.protocols.base.VersionProvider + +object CloudVersionProvider : VersionProvider() { + override fun getServerProtocol(connection: UserConnection): Int { + val ver = connection.channel!!.pipeline().get(CloudMinecraftHandler::class.java).data.backVer + if (ver != null) return ver + return super.getServerProtocol(connection) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/web/ViaWebApp.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/web/ViaWebApp.kt new file mode 100644 index 0000000..81bb8e7 --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/web/ViaWebApp.kt @@ -0,0 +1,50 @@ +package com.github.creeper123123321.viaaas.web + +import com.github.creeper123123321.viaaas.viaWebServer +import com.github.creeper123123321.viaaas.webLogger +import io.ktor.application.* +import io.ktor.features.* +import io.ktor.http.cio.websocket.* +import io.ktor.http.content.* +import io.ktor.routing.* +import io.ktor.websocket.* +import kotlinx.coroutines.channels.consumeEach +import org.slf4j.event.Level +import java.nio.channels.ClosedChannelException + +class ViaWebApp { + fun Application.main() { + install(DefaultHeaders) + install(CallLogging) { + level = Level.INFO + } + install(WebSockets) { + maxFrameSize = Short.MAX_VALUE.toLong() + } + + routing { + webSocket("/ws") { + try { + viaWebServer.connected(this) + incoming.consumeEach { frame -> + if (frame is Frame.Text) { + viaWebServer.onMessage(this, frame.readText()) + } + } + } catch (ignored: ClosedChannelException) { + } catch (e: Exception) { + webLogger.info("${call.request.local.remoteHost} (O: ${call.request.origin.remoteHost}) exception: $e") + viaWebServer.onException(this, e) + this.close(CloseReason(CloseReason.Codes.INTERNAL_ERROR, "INTERNAL ERROR")) + } finally { + viaWebServer.disconnected(this) + } + } + + static { + defaultResource("index.html", "web") + resources("web") + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/web/WebClient.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/web/WebClient.kt new file mode 100644 index 0000000..6e157d6 --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/web/WebClient.kt @@ -0,0 +1,11 @@ +package com.github.creeper123123321.viaaas.web + +import io.ktor.websocket.* +import java.util.* + +data class WebClient( + val server: WebDashboardServer, + val ws: WebSocketServerSession, + val state: WebState, + val listenedIds: MutableSet = mutableSetOf() +) \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/web/WebDashboardServer.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/web/WebDashboardServer.kt new file mode 100644 index 0000000..80ac47a --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/web/WebDashboardServer.kt @@ -0,0 +1,95 @@ +package com.github.creeper123123321.viaaas.web + +import com.github.creeper123123321.viaaas.httpClient +import com.github.creeper123123321.viaaas.parseUndashedId +import com.github.creeper123123321.viaaas.viaWebServer +import com.google.common.cache.CacheBuilder +import com.google.common.cache.CacheLoader +import com.google.gson.JsonObject +import io.ktor.client.request.* +import io.ktor.http.cio.websocket.* +import io.ktor.websocket.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import us.myles.ViaVersion.api.Via +import java.net.SocketAddress +import java.security.PublicKey +import java.util.* +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException + +class WebDashboardServer { + val clients = ConcurrentHashMap() + val loginTokens = CacheBuilder.newBuilder() + .expireAfterAccess(10, TimeUnit.DAYS) + .build() + + // Minecraft account -> WebClient + val listeners = ConcurrentHashMap>() + val usernameIdCache = CacheBuilder.newBuilder() + .expireAfterWrite(1, TimeUnit.HOURS) + .build(CacheLoader.from { name -> + runBlocking { + withContext(Dispatchers.IO) { + httpClient.get("https://api.mojang.com/users/profiles/minecraft/$name") + ?.get("id")?.asString?.let { parseUndashedId(it) } + } + } + }) + + val pendingSessionHashes = CacheBuilder.newBuilder() + .expireAfterWrite(30, TimeUnit.SECONDS) + .build>(CacheLoader.from { _ -> CompletableFuture() }) + + suspend fun requestSessionJoin( + id: UUID, name: String, hash: String, + address: SocketAddress, backKey: PublicKey + ) + : CompletableFuture { + val future = viaWebServer.pendingSessionHashes.get(hash) + var sent = 0 + viaWebServer.listeners[id]?.forEach { + it.ws.send( + """{"action": "session_hash_request", "user": "$name", "session_hash": "$hash", + | "client_address": "$address", "backend_public_key": + | "${Base64.getEncoder().encodeToString(backKey.encoded)}"}""".trimMargin() + ) + it.ws.flush() + sent++ + } + if (sent != 0) { + Via.getPlatform().runSync({ + future.completeExceptionally(TimeoutException("No response from browser")) + }, 15 * 20) + } else { + future.completeExceptionally(IllegalStateException("No browser listening")) + } + return future + } + + suspend fun connected(ws: WebSocketServerSession) { + val loginState = WebLogin() + val client = WebClient(this, ws, loginState) + clients[ws] = client + loginState.start(client) + } + + suspend fun onMessage(ws: WebSocketSession, msg: String) { + val client = clients[ws]!! + client.state.onMessage(client, msg) + } + + suspend fun disconnected(ws: WebSocketSession) { + val client = clients[ws]!! + client.state.disconnected(client) + clients.remove(ws) + } + + suspend fun onException(ws: WebSocketSession, exception: java.lang.Exception) { + val client = clients[ws]!! + client.state.onException(client, exception) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/web/WebLogin.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/web/WebLogin.kt new file mode 100644 index 0000000..8a0d881 --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/web/WebLogin.kt @@ -0,0 +1,97 @@ +package com.github.creeper123123321.viaaas.web + +import com.github.creeper123123321.viaaas.generateOfflinePlayerUuid +import com.github.creeper123123321.viaaas.httpClient +import com.github.creeper123123321.viaaas.webLogger +import com.google.gson.Gson +import com.google.gson.JsonObject +import io.ktor.client.request.forms.* +import io.ktor.features.* +import io.ktor.http.* +import io.ktor.http.cio.websocket.* +import java.net.URLEncoder +import java.util.* +import java.util.concurrent.ConcurrentHashMap + +class WebLogin : WebState { + override suspend fun start(webClient: WebClient) { + webClient.ws.send("""{"action": "ad_minecraft_id_login"}""") + webClient.ws.flush() + } + + override suspend fun onMessage(webClient: WebClient, msg: String) { + val obj = Gson().fromJson(msg, JsonObject::class.java) + + when (obj.getAsJsonPrimitive("action").asString) { + "offline_login" -> { + // todo add some spam check + val username = obj.get("username").asString + val token = UUID.randomUUID() + val uuid = generateOfflinePlayerUuid(username) + + webClient.server.loginTokens.put(token, uuid) + webClient.ws.send( + """{"action": "login_result", "success": true, + | "username": "$username", "uuid": "$uuid", "token": "$token"}""".trimMargin() + ) + + webLogger.info("${webClient.ws.call.request.local.remoteHost} (O: ${webClient.ws.call.request.origin.remoteHost}) generated a token for offline account $username") + } + "minecraft_id_login" -> { + val username = obj.get("username").asString + val code = obj.get("code").asString + + val check = httpClient.submitForm( + "https://api.minecraft.id/gateway/verify/${URLEncoder.encode(username, Charsets.UTF_8)}", + formParameters = parametersOf("code", code), + ) + + if (check.getAsJsonPrimitive("valid").asBoolean) { + val token = UUID.randomUUID() + val mcIdUser = check.get("username").asString + val uuid = webClient.server.usernameIdCache.get(mcIdUser) + + webClient.server.loginTokens.put(token, uuid) + webClient.ws.send( + """{"action": "login_result", "success": true, + | "username": "$mcIdUser", "uuid": "$uuid", "token": "$token"}""".trimMargin() + ) + + webLogger.info("${webClient.ws.call.request.local.remoteHost} (O: ${webClient.ws.call.request.origin.remoteHost}) generated a token for account $mcIdUser $uuid") + } else { + webClient.ws.send("""{"action": "login_result", "success": false}""") + webLogger.info("${webClient.ws.call.request.local.remoteHost} (O: ${webClient.ws.call.request.origin.remoteHost}) failed to generated a token for account $username") + } + } + "listen_login_requests" -> { + val token = UUID.fromString(obj.getAsJsonPrimitive("token").asString) + val user = webClient.server.loginTokens.getIfPresent(token) + if (user != null) { + webClient.ws.send("""{"action": "listen_login_requests_result", "token": "$token", "success": true, "user": "$user"}""") + webClient.listenedIds.add(user) + webClient.server.listeners.computeIfAbsent(user) { Collections.newSetFromMap(ConcurrentHashMap()) } + .add(webClient) + + webLogger.info("${webClient.ws.call.request.local.remoteHost} (O: ${webClient.ws.call.request.origin.remoteHost}) listening for logins for $user") + } else { + webClient.ws.send("""{"action": "listen_login_requests_result", "token": "$token", "success": false}""") + webLogger.info("${webClient.ws.call.request.local.remoteHost} (O: ${webClient.ws.call.request.origin.remoteHost}) failed token") + } + } + "session_hash_response" -> { + val hash = obj.get("session_hash").asString + webClient.server.pendingSessionHashes.getIfPresent(hash)?.complete(null) + } + else -> throw IllegalStateException("invalid action!") + } + + webClient.ws.flush() + } + + override suspend fun disconnected(webClient: WebClient) { + webClient.listenedIds.forEach { webClient.server.listeners[it]?.remove(webClient) } + } + + override suspend fun onException(webClient: WebClient, exception: java.lang.Exception) { + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/web/WebState.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/web/WebState.kt new file mode 100644 index 0000000..3043e51 --- /dev/null +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/web/WebState.kt @@ -0,0 +1,8 @@ +package com.github.creeper123123321.viaaas.web + +interface WebState { + suspend fun start(webClient: WebClient) + suspend fun onMessage(webClient: WebClient, msg: String) + suspend fun disconnected(webClient: WebClient) + suspend fun onException(webClient: WebClient, exception: java.lang.Exception) +} \ No newline at end of file