mirror of
https://github.com/ViaVersion/VIAaaS.git
synced 2024-11-25 12:35:36 +01:00
separate class files
This commit is contained in:
parent
f789dfea8f
commit
fb29519b22
@ -53,8 +53,13 @@ dependencies {
|
|||||||
implementation(kotlin("stdlib-jdk8"))
|
implementation(kotlin("stdlib-jdk8"))
|
||||||
implementation("io.ktor:ktor-network-tls-certificates:$ktorVersion")
|
implementation("io.ktor:ktor-network-tls-certificates:$ktorVersion")
|
||||||
implementation("io.ktor:ktor-server-netty:$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-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-client-gson:$ktorVersion")
|
||||||
|
implementation("io.ktor:ktor-server-host-common:$ktorVersion")
|
||||||
implementation("io.ktor:ktor-websockets:$ktorVersion")
|
implementation("io.ktor:ktor-websockets:$ktorVersion")
|
||||||
testImplementation("io.ktor:ktor-server-test-host:$ktorVersion")
|
testImplementation("io.ktor:ktor-server-test-host:$ktorVersion")
|
||||||
}
|
}
|
||||||
|
@ -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<Channel>() {
|
|
||||||
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<Channel>() {
|
|
||||||
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<ByteBuf, Packet>() {
|
|
||||||
override fun encode(ctx: ChannelHandlerContext, msg: Packet, out: MutableList<Any>) {
|
|
||||||
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<Any>) {
|
|
||||||
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<ByteBuf, ByteBuf>() {
|
|
||||||
override fun decode(ctx: ChannelHandlerContext, msg: ByteBuf, out: MutableList<Any>) {
|
|
||||||
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<Any>) {
|
|
||||||
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<ByteBuf, ByteBuf>() {
|
|
||||||
// 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<Any>) {
|
|
||||||
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<Any>) {
|
|
||||||
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<ByteBuf>() {
|
|
||||||
override fun decode(ctx: ChannelHandlerContext, input: ByteBuf, out: MutableList<Any>) {
|
|
||||||
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<ByteBuf, ByteBuf>() {
|
|
||||||
override fun decode(ctx: ChannelHandlerContext, bytebuf: ByteBuf, out: MutableList<Any>) {
|
|
||||||
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<Any>) {
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<Packet>() {
|
|
||||||
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<String>()
|
|
||||||
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<JsonObject?>(
|
|
||||||
"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<String, Int> {
|
|
||||||
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<String>): 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<String>): 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))
|
|
@ -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<RegistryEntry>()
|
|
||||||
|
|
||||||
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 <reified P : Packet> register(
|
|
||||||
protocol: Range<Int>,
|
|
||||||
state: State,
|
|
||||||
id: Int,
|
|
||||||
serverBound: Boolean,
|
|
||||||
constructor: Supplier<P>
|
|
||||||
) {
|
|
||||||
entries.add(RegistryEntry(protocol, state, id, serverBound, constructor, P::class.java))
|
|
||||||
}
|
|
||||||
|
|
||||||
data class RegistryEntry(
|
|
||||||
val versionRange: Range<Int>,
|
|
||||||
val state: State,
|
|
||||||
val id: Int,
|
|
||||||
val serverBound: Boolean,
|
|
||||||
val constructor: Supplier<out Packet>,
|
|
||||||
val packetClass: Class<out Packet>
|
|
||||||
)
|
|
||||||
|
|
||||||
fun getPacketConstructor(
|
|
||||||
protocolVersion: Int,
|
|
||||||
state: State,
|
|
||||||
id: Int,
|
|
||||||
serverBound: Boolean
|
|
||||||
): Supplier<out Packet>? {
|
|
||||||
return entries.firstOrNull {
|
|
||||||
it.serverBound == serverBound && it.state == state
|
|
||||||
&& it.versionRange.contains(protocolVersion) && it.id == id
|
|
||||||
}?.constructor
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getPacketId(packetClass: Class<out Packet>, 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<Int>()
|
|
||||||
lateinit var address: String
|
|
||||||
var port by Delegates.notNull<Int>()
|
|
||||||
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<Int>()
|
|
||||||
var success by Delegates.notNull<Boolean>()
|
|
||||||
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<Int>()
|
|
||||||
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<Int>()
|
|
||||||
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<Long>()
|
|
||||||
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<Long>()
|
|
||||||
override fun decode(byteBuf: ByteBuf, protocolVersion: Int) {
|
|
||||||
number = byteBuf.readLong()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun encode(byteBuf: ByteBuf, protocolVersion: Int) {
|
|
||||||
byteBuf.writeLong(number)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<Unit>(title, health, color, style)
|
|
||||||
|
|
||||||
object CloudAPI : ViaAPI<Unit> {
|
|
||||||
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<Int> = ProtocolRegistry.getSupportedVersions()
|
|
||||||
}
|
|
||||||
|
|
||||||
object CloudPlatform : ViaPlatform<Unit> {
|
|
||||||
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<Unit> = 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<ViaCommandSender> = 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<String, Any>) {
|
|
||||||
// Nothing Currently
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getUnsupportedOptions(): List<String> = 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)
|
|
||||||
}
|
|
||||||
}
|
|
135
src/main/kotlin/com/github/creeper123123321/viaaas/Util.kt
Normal file
135
src/main/kotlin/com/github/creeper123123321/viaaas/Util.kt
Normal file
@ -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<String, Int> {
|
||||||
|
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<String>): 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<String>): 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()
|
@ -1,5 +1,12 @@
|
|||||||
package com.github.creeper123123321.viaaas
|
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 de.gerrygames.viarewind.api.ViaRewindConfigImpl
|
||||||
import io.ktor.application.*
|
import io.ktor.application.*
|
||||||
import io.ktor.client.*
|
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.NioServerSocketChannel
|
||||||
import io.netty.channel.socket.nio.NioSocketChannel
|
import io.netty.channel.socket.nio.NioSocketChannel
|
||||||
import io.netty.util.concurrent.Future
|
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.ViaManager
|
||||||
import us.myles.ViaVersion.api.Via
|
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.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.util.GsonUtil
|
||||||
import us.myles.viaversion.libs.gson.JsonObject
|
import us.myles.viaversion.libs.gson.JsonObject
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.security.KeyPairGenerator
|
import java.security.KeyPairGenerator
|
||||||
import java.security.SecureRandom
|
|
||||||
import java.util.*
|
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
|
|
||||||
val viaaasVer = GsonUtil.getGson().fromJson(
|
val viaaasVer = GsonUtil.getGson().fromJson(
|
||||||
CloudPlatform::class.java.classLoader.getResourceAsStream("viaaas_info.json")!!
|
CloudPlatform::class.java.classLoader.getResourceAsStream("viaaas_info.json")!!.reader(Charsets.UTF_8).readText(),
|
||||||
.reader(Charsets.UTF_8).readText(), JsonObject::class.java
|
JsonObject::class.java
|
||||||
).get("version").asString
|
).get("version").asString
|
||||||
|
val viaWebServer = WebDashboardServer()
|
||||||
var runningServer = true
|
var runningServer = true
|
||||||
val viaaasLogger = LoggerFactory.getLogger("VIAaaS")
|
|
||||||
|
|
||||||
val httpClient = HttpClient {
|
val httpClient = HttpClient {
|
||||||
defaultRequest {
|
install(UserAgent) {
|
||||||
header("User-Agent", "VIAaaS/$viaaasVer")
|
agent = "VIAaaS/$viaaasVer"
|
||||||
}
|
}
|
||||||
install(JsonFeature) {
|
install(JsonFeature) {
|
||||||
serializer = GsonSerializer()
|
serializer = GsonSerializer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val initFuture = CompletableFuture<Unit>()
|
val initFuture = CompletableFuture<Unit>()
|
||||||
|
|
||||||
// Minecraft doesn't have forward secrecy
|
// Minecraft doesn't have forward secrecy
|
||||||
@ -72,8 +66,6 @@ val mcCryptoKey = KeyPairGenerator.getInstance("RSA").let {
|
|||||||
it.genKeyPair()
|
it.genKeyPair()
|
||||||
}
|
}
|
||||||
|
|
||||||
val secureRandom = if (VIAaaSConfig.useStrongRandom) SecureRandom.getInstanceStrong() else SecureRandom()
|
|
||||||
|
|
||||||
fun eventLoopGroup(): EventLoopGroup {
|
fun eventLoopGroup(): EventLoopGroup {
|
||||||
if (VIAaaSConfig.isNativeTransportMc) {
|
if (VIAaaSConfig.isNativeTransportMc) {
|
||||||
if (Epoll.isAvailable()) return EpollEventLoopGroup()
|
if (Epoll.isAvailable()) return EpollEventLoopGroup()
|
||||||
@ -101,7 +93,7 @@ fun channelSocketFactory(): ChannelFactory<SocketChannel> {
|
|||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
// Stolen from https://github.com/VelocityPowered/Velocity/blob/dev/1.1.0/proxy/src/main/java/com/velocitypowered/proxy/Velocity.java
|
// 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) {
|
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 {
|
File("config/https.jks").apply {
|
||||||
@ -127,7 +119,7 @@ fun main(args: Array<String>) {
|
|||||||
val future = ServerBootstrap()
|
val future = ServerBootstrap()
|
||||||
.group(parent, child)
|
.group(parent, child)
|
||||||
.channelFactory(channelServerSocketFactory())
|
.channelFactory(channelServerSocketFactory())
|
||||||
.childHandler(FrontChannelInit)
|
.childHandler(FrontEndInit)
|
||||||
.childOption(ChannelOption.IP_TOS, 0x18)
|
.childOption(ChannelOption.IP_TOS, 0x18)
|
||||||
.childOption(ChannelOption.TCP_NODELAY, true)
|
.childOption(ChannelOption.TCP_NODELAY, true)
|
||||||
.bind(InetAddress.getByName(VIAaaSConfig.bindAddress), VIAaaSConfig.port)
|
.bind(InetAddress.getByName(VIAaaSConfig.bindAddress), VIAaaSConfig.port)
|
||||||
@ -143,9 +135,7 @@ fun main(args: Array<String>) {
|
|||||||
|
|
||||||
initFuture.complete(Unit)
|
initFuture.complete(Unit)
|
||||||
|
|
||||||
if (runningServer) {
|
|
||||||
VIAaaSConsole().start()
|
VIAaaSConsole().start()
|
||||||
}
|
|
||||||
|
|
||||||
ktorServer?.stop(1000, 1000)
|
ktorServer?.stop(1000, 1000)
|
||||||
httpClient.close()
|
httpClient.close()
|
||||||
@ -155,208 +145,6 @@ fun main(args: Array<String>) {
|
|||||||
Via.getManager().destroy()
|
Via.getManager().destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
class VIAaaSConsole : SimpleTerminalConsole(), ViaCommandSender {
|
|
||||||
val commands = hashMapOf<String, (MutableList<String>?, String, Array<String>) -> 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<String>()
|
|
||||||
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() {
|
fun Application.mainWeb() {
|
||||||
ViaWebApp().apply { main() }
|
ViaWebApp().apply { main() }
|
||||||
}
|
}
|
||||||
|
|
||||||
object VIAaaSConfig : Config(File("config/viaaas.yml")) {
|
|
||||||
init {
|
|
||||||
reloadConfig()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getUnsupportedOptions() = emptyList<String>().toMutableList()
|
|
||||||
override fun getDefaultConfigURL() = VIAaaSConfig::class.java.classLoader.getResource("viaaas.yml")!!
|
|
||||||
override fun handleConfig(p0: MutableMap<String, Any>?) {
|
|
||||||
}
|
|
||||||
|
|
||||||
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<String>
|
|
||||||
get() = this.get(
|
|
||||||
"blocked-back-addresses",
|
|
||||||
List::class.java,
|
|
||||||
emptyList<String>()
|
|
||||||
)!!.map { it as String }
|
|
||||||
val allowedBackAddresses: List<String>
|
|
||||||
get() = this.get(
|
|
||||||
"allowed-back-addresses",
|
|
||||||
List::class.java,
|
|
||||||
emptyList<String>()
|
|
||||||
)!!.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<String>()
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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<String>()
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
@ -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<WebSocketSession, WebClient>()
|
|
||||||
val loginTokens = CacheBuilder.newBuilder()
|
|
||||||
.expireAfterAccess(10, TimeUnit.DAYS)
|
|
||||||
.build<UUID, UUID>()
|
|
||||||
|
|
||||||
// Minecraft account -> WebClient
|
|
||||||
val listeners = ConcurrentHashMap<UUID, MutableSet<WebClient>>()
|
|
||||||
val usernameIdCache = CacheBuilder.newBuilder()
|
|
||||||
.expireAfterWrite(1, TimeUnit.HOURS)
|
|
||||||
.build<String, UUID>(CacheLoader.from { name ->
|
|
||||||
runBlocking {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
httpClient.get<JsonObject?>("https://api.mojang.com/users/profiles/minecraft/$name")
|
|
||||||
?.get("id")?.asString?.let { parseUndashedId(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
val pendingSessionHashes = CacheBuilder.newBuilder()
|
|
||||||
.expireAfterWrite(30, TimeUnit.SECONDS)
|
|
||||||
.build<String, CompletableFuture<Unit>>(CacheLoader.from { _ -> CompletableFuture() })
|
|
||||||
|
|
||||||
suspend fun requestSessionJoin(
|
|
||||||
id: UUID, name: String, hash: String,
|
|
||||||
address: SocketAddress, backKey: PublicKey
|
|
||||||
)
|
|
||||||
: CompletableFuture<Unit> {
|
|
||||||
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<UUID> = 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<JsonObject>(
|
|
||||||
"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) {
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<ByteBuf, ByteBuf>() {
|
||||||
|
// 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<Any>) {
|
||||||
|
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<Any>) {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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<ByteBuf, ByteBuf>() {
|
||||||
|
override fun decode(ctx: ChannelHandlerContext, msg: ByteBuf, out: MutableList<Any>) {
|
||||||
|
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<Any>) {
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
@ -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<ByteBuf>() {
|
||||||
|
override fun decode(ctx: ChannelHandlerContext, input: ByteBuf, out: MutableList<Any>) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -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<ByteBuf, Packet>() {
|
||||||
|
override fun encode(ctx: ChannelHandlerContext, msg: Packet, out: MutableList<Any>) {
|
||||||
|
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<Any>) {
|
||||||
|
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!!!")
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
package com.github.creeper123123321.viaaas.command
|
||||||
|
|
||||||
|
import us.myles.ViaVersion.commands.ViaCommandHandler
|
||||||
|
|
||||||
|
object CloudCommands : ViaCommandHandler()
|
@ -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, (MutableList<String>?, String, Array<String>) -> 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<String>()
|
||||||
|
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"
|
||||||
|
}
|
@ -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<Channel>() {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
@ -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<Packet>() {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -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<ByteBuf, ByteBuf>() {
|
||||||
|
override fun decode(ctx: ChannelHandlerContext, bytebuf: ByteBuf, out: MutableList<Any>) {
|
||||||
|
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<Any>) {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
@ -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<Channel>() {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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<String>()
|
||||||
|
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<JsonObject?>(
|
||||||
|
"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)
|
||||||
|
}
|
||||||
|
}
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
@ -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<RegistryEntry>()
|
||||||
|
|
||||||
|
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 <reified P : Packet> register(
|
||||||
|
protocol: Range<Int>,
|
||||||
|
state: State,
|
||||||
|
id: Int,
|
||||||
|
serverBound: Boolean,
|
||||||
|
constructor: Supplier<P>
|
||||||
|
) {
|
||||||
|
entries.add(RegistryEntry(protocol, state, id, serverBound, constructor, P::class.java))
|
||||||
|
}
|
||||||
|
|
||||||
|
data class RegistryEntry(
|
||||||
|
val versionRange: Range<Int>,
|
||||||
|
val state: State,
|
||||||
|
val id: Int,
|
||||||
|
val serverBound: Boolean,
|
||||||
|
val constructor: Supplier<out Packet>,
|
||||||
|
val packetClass: Class<out Packet>
|
||||||
|
)
|
||||||
|
|
||||||
|
fun getPacketConstructor(
|
||||||
|
protocolVersion: Int,
|
||||||
|
state: State,
|
||||||
|
id: Int,
|
||||||
|
serverBound: Boolean
|
||||||
|
): Supplier<out Packet>? {
|
||||||
|
return entries.firstOrNull {
|
||||||
|
it.serverBound == serverBound && it.state == state
|
||||||
|
&& it.versionRange.contains(protocolVersion) && it.id == id
|
||||||
|
}?.constructor
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPacketId(packetClass: Class<out Packet>, 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)
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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<Int>()
|
||||||
|
lateinit var address: String
|
||||||
|
var port by Delegates.notNull<Int>()
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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<Int>()
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -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<Int>()
|
||||||
|
var success by Delegates.notNull<Boolean>()
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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<Int>()
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -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<Long>()
|
||||||
|
override fun decode(byteBuf: ByteBuf, protocolVersion: Int) {
|
||||||
|
number = byteBuf.readLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun encode(byteBuf: ByteBuf, protocolVersion: Int) {
|
||||||
|
byteBuf.writeLong(number)
|
||||||
|
}
|
||||||
|
}
|
@ -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<Long>()
|
||||||
|
override fun decode(byteBuf: ByteBuf, protocolVersion: Int) {
|
||||||
|
number = byteBuf.readLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun encode(byteBuf: ByteBuf, protocolVersion: Int) {
|
||||||
|
byteBuf.writeLong(number)
|
||||||
|
}
|
||||||
|
}
|
@ -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) {
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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() {
|
||||||
|
}
|
||||||
|
}
|
@ -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<Unit>(title, health, color, style)
|
@ -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
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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<Unit> {
|
||||||
|
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<Unit> = 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<ViaCommandSender> = 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
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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<Unit> {
|
||||||
|
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<Int> = ProtocolRegistry.getSupportedVersions()
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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<UUID> = mutableSetOf()
|
||||||
|
)
|
@ -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<WebSocketSession, WebClient>()
|
||||||
|
val loginTokens = CacheBuilder.newBuilder()
|
||||||
|
.expireAfterAccess(10, TimeUnit.DAYS)
|
||||||
|
.build<UUID, UUID>()
|
||||||
|
|
||||||
|
// Minecraft account -> WebClient
|
||||||
|
val listeners = ConcurrentHashMap<UUID, MutableSet<WebClient>>()
|
||||||
|
val usernameIdCache = CacheBuilder.newBuilder()
|
||||||
|
.expireAfterWrite(1, TimeUnit.HOURS)
|
||||||
|
.build<String, UUID>(CacheLoader.from { name ->
|
||||||
|
runBlocking {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
httpClient.get<JsonObject?>("https://api.mojang.com/users/profiles/minecraft/$name")
|
||||||
|
?.get("id")?.asString?.let { parseUndashedId(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
val pendingSessionHashes = CacheBuilder.newBuilder()
|
||||||
|
.expireAfterWrite(30, TimeUnit.SECONDS)
|
||||||
|
.build<String, CompletableFuture<Unit>>(CacheLoader.from { _ -> CompletableFuture() })
|
||||||
|
|
||||||
|
suspend fun requestSessionJoin(
|
||||||
|
id: UUID, name: String, hash: String,
|
||||||
|
address: SocketAddress, backKey: PublicKey
|
||||||
|
)
|
||||||
|
: CompletableFuture<Unit> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -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<JsonObject>(
|
||||||
|
"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) {
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user