mirror of
https://github.com/ViaVersion/VIAaaS.git
synced 2024-11-22 12:05:45 +01:00
separate class files
This commit is contained in:
parent
f789dfea8f
commit
fb29519b22
@ -53,8 +53,13 @@ dependencies {
|
||||
implementation(kotlin("stdlib-jdk8"))
|
||||
implementation("io.ktor:ktor-network-tls-certificates:$ktorVersion")
|
||||
implementation("io.ktor:ktor-server-netty:$ktorVersion")
|
||||
implementation("io.ktor:ktor-client-core:$ktorVersion")
|
||||
implementation("io.ktor:ktor-client-core-jvm:$ktorVersion")
|
||||
implementation("io.ktor:ktor-client-cio:$ktorVersion")
|
||||
implementation("io.ktor:ktor-client-logging-jvm:$ktorVersion")
|
||||
implementation("io.ktor:ktor-server-core:$ktorVersion")
|
||||
implementation("io.ktor:ktor-client-gson:$ktorVersion")
|
||||
implementation("io.ktor:ktor-server-host-common:$ktorVersion")
|
||||
implementation("io.ktor:ktor-websockets:$ktorVersion")
|
||||
testImplementation("io.ktor:ktor-server-test-host:$ktorVersion")
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
import com.github.creeper123123321.viaaas.command.CloudCommands
|
||||
import com.github.creeper123123321.viaaas.command.VIAaaSConsole
|
||||
import com.github.creeper123123321.viaaas.config.VIAaaSConfig
|
||||
import com.github.creeper123123321.viaaas.handler.FrontEndInit
|
||||
import com.github.creeper123123321.viaaas.platform.*
|
||||
import com.github.creeper123123321.viaaas.web.ViaWebApp
|
||||
import com.github.creeper123123321.viaaas.web.WebDashboardServer
|
||||
import de.gerrygames.viarewind.api.ViaRewindConfigImpl
|
||||
import io.ktor.application.*
|
||||
import io.ktor.client.*
|
||||
@ -27,43 +34,30 @@ import io.netty.channel.socket.SocketChannel
|
||||
import io.netty.channel.socket.nio.NioServerSocketChannel
|
||||
import io.netty.channel.socket.nio.NioSocketChannel
|
||||
import io.netty.util.concurrent.Future
|
||||
import net.minecrell.terminalconsole.SimpleTerminalConsole
|
||||
import org.jline.reader.Candidate
|
||||
import org.jline.reader.LineReader
|
||||
import org.jline.reader.LineReaderBuilder
|
||||
import org.slf4j.LoggerFactory
|
||||
import us.myles.ViaVersion.ViaManager
|
||||
import us.myles.ViaVersion.api.Via
|
||||
import us.myles.ViaVersion.api.command.ViaCommandSender
|
||||
import us.myles.ViaVersion.api.data.MappingDataLoader
|
||||
import us.myles.ViaVersion.api.protocol.ProtocolVersion
|
||||
import us.myles.ViaVersion.util.Config
|
||||
import us.myles.ViaVersion.util.GsonUtil
|
||||
import us.myles.viaversion.libs.gson.JsonObject
|
||||
import java.io.File
|
||||
import java.net.InetAddress
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.SecureRandom
|
||||
import java.util.*
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
val viaaasVer = GsonUtil.getGson().fromJson(
|
||||
CloudPlatform::class.java.classLoader.getResourceAsStream("viaaas_info.json")!!
|
||||
.reader(Charsets.UTF_8).readText(), JsonObject::class.java
|
||||
CloudPlatform::class.java.classLoader.getResourceAsStream("viaaas_info.json")!!.reader(Charsets.UTF_8).readText(),
|
||||
JsonObject::class.java
|
||||
).get("version").asString
|
||||
|
||||
val viaWebServer = WebDashboardServer()
|
||||
var runningServer = true
|
||||
val viaaasLogger = LoggerFactory.getLogger("VIAaaS")
|
||||
|
||||
val httpClient = HttpClient {
|
||||
defaultRequest {
|
||||
header("User-Agent", "VIAaaS/$viaaasVer")
|
||||
install(UserAgent) {
|
||||
agent = "VIAaaS/$viaaasVer"
|
||||
}
|
||||
install(JsonFeature) {
|
||||
serializer = GsonSerializer()
|
||||
}
|
||||
}
|
||||
|
||||
val initFuture = CompletableFuture<Unit>()
|
||||
|
||||
// Minecraft doesn't have forward secrecy
|
||||
@ -72,8 +66,6 @@ val mcCryptoKey = KeyPairGenerator.getInstance("RSA").let {
|
||||
it.genKeyPair()
|
||||
}
|
||||
|
||||
val secureRandom = if (VIAaaSConfig.useStrongRandom) SecureRandom.getInstanceStrong() else SecureRandom()
|
||||
|
||||
fun eventLoopGroup(): EventLoopGroup {
|
||||
if (VIAaaSConfig.isNativeTransportMc) {
|
||||
if (Epoll.isAvailable()) return EpollEventLoopGroup()
|
||||
@ -101,7 +93,7 @@ fun channelSocketFactory(): ChannelFactory<SocketChannel> {
|
||||
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
|
||||
if (System.getProperty("io.netty.allocator.maxOrder") == null) {
|
||||
System.setProperty("io.netty.allocator.maxOrder", "9");
|
||||
System.setProperty("io.netty.allocator.maxOrder", "9")
|
||||
}
|
||||
|
||||
File("config/https.jks").apply {
|
||||
@ -127,7 +119,7 @@ fun main(args: Array<String>) {
|
||||
val future = ServerBootstrap()
|
||||
.group(parent, child)
|
||||
.channelFactory(channelServerSocketFactory())
|
||||
.childHandler(FrontChannelInit)
|
||||
.childHandler(FrontEndInit)
|
||||
.childOption(ChannelOption.IP_TOS, 0x18)
|
||||
.childOption(ChannelOption.TCP_NODELAY, true)
|
||||
.bind(InetAddress.getByName(VIAaaSConfig.bindAddress), VIAaaSConfig.port)
|
||||
@ -143,9 +135,7 @@ fun main(args: Array<String>) {
|
||||
|
||||
initFuture.complete(Unit)
|
||||
|
||||
if (runningServer) {
|
||||
VIAaaSConsole().start()
|
||||
}
|
||||
VIAaaSConsole().start()
|
||||
|
||||
ktorServer?.stop(1000, 1000)
|
||||
httpClient.close()
|
||||
@ -155,208 +145,6 @@ fun main(args: Array<String>) {
|
||||
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() {
|
||||
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