separate class files

This commit is contained in:
creeper123123321 2021-02-10 10:27:06 -03:00
parent f789dfea8f
commit fb29519b22
56 changed files with 1960 additions and 1775 deletions

View File

@ -53,8 +53,13 @@ dependencies {
implementation(kotlin("stdlib-jdk8")) implementation(kotlin("stdlib-jdk8"))
implementation("io.ktor:ktor-network-tls-certificates:$ktorVersion") implementation("io.ktor:ktor-network-tls-certificates:$ktorVersion")
implementation("io.ktor:ktor-server-netty:$ktorVersion") implementation("io.ktor:ktor-server-netty:$ktorVersion")
implementation("io.ktor:ktor-client-core:$ktorVersion")
implementation("io.ktor:ktor-client-core-jvm:$ktorVersion")
implementation("io.ktor:ktor-client-cio:$ktorVersion") implementation("io.ktor:ktor-client-cio:$ktorVersion")
implementation("io.ktor:ktor-client-logging-jvm:$ktorVersion")
implementation("io.ktor:ktor-server-core:$ktorVersion")
implementation("io.ktor:ktor-client-gson:$ktorVersion") implementation("io.ktor:ktor-client-gson:$ktorVersion")
implementation("io.ktor:ktor-server-host-common:$ktorVersion")
implementation("io.ktor:ktor-websockets:$ktorVersion") implementation("io.ktor:ktor-websockets:$ktorVersion")
testImplementation("io.ktor:ktor-server-test-host:$ktorVersion") testImplementation("io.ktor:ktor-server-test-host:$ktorVersion")
} }

View File

@ -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()
}
}
}

View File

@ -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))

View File

@ -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)
}
}

View File

@ -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)
}
}

View 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()

View File

@ -1,5 +1,12 @@
package com.github.creeper123123321.viaaas package com.github.creeper123123321.viaaas
import com.github.creeper123123321.viaaas.command.CloudCommands
import com.github.creeper123123321.viaaas.command.VIAaaSConsole
import com.github.creeper123123321.viaaas.config.VIAaaSConfig
import com.github.creeper123123321.viaaas.handler.FrontEndInit
import com.github.creeper123123321.viaaas.platform.*
import com.github.creeper123123321.viaaas.web.ViaWebApp
import com.github.creeper123123321.viaaas.web.WebDashboardServer
import de.gerrygames.viarewind.api.ViaRewindConfigImpl import de.gerrygames.viarewind.api.ViaRewindConfigImpl
import io.ktor.application.* import io.ktor.application.*
import io.ktor.client.* import io.ktor.client.*
@ -27,43 +34,30 @@ import io.netty.channel.socket.SocketChannel
import io.netty.channel.socket.nio.NioServerSocketChannel import io.netty.channel.socket.nio.NioServerSocketChannel
import io.netty.channel.socket.nio.NioSocketChannel import io.netty.channel.socket.nio.NioSocketChannel
import io.netty.util.concurrent.Future import io.netty.util.concurrent.Future
import net.minecrell.terminalconsole.SimpleTerminalConsole
import org.jline.reader.Candidate
import org.jline.reader.LineReader
import org.jline.reader.LineReaderBuilder
import org.slf4j.LoggerFactory
import us.myles.ViaVersion.ViaManager import us.myles.ViaVersion.ViaManager
import us.myles.ViaVersion.api.Via import us.myles.ViaVersion.api.Via
import us.myles.ViaVersion.api.command.ViaCommandSender
import us.myles.ViaVersion.api.data.MappingDataLoader import us.myles.ViaVersion.api.data.MappingDataLoader
import us.myles.ViaVersion.api.protocol.ProtocolVersion
import us.myles.ViaVersion.util.Config
import us.myles.ViaVersion.util.GsonUtil import us.myles.ViaVersion.util.GsonUtil
import us.myles.viaversion.libs.gson.JsonObject import us.myles.viaversion.libs.gson.JsonObject
import java.io.File import java.io.File
import java.net.InetAddress import java.net.InetAddress
import java.security.KeyPairGenerator import java.security.KeyPairGenerator
import java.security.SecureRandom
import java.util.*
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
val viaaasVer = GsonUtil.getGson().fromJson( val viaaasVer = GsonUtil.getGson().fromJson(
CloudPlatform::class.java.classLoader.getResourceAsStream("viaaas_info.json")!! CloudPlatform::class.java.classLoader.getResourceAsStream("viaaas_info.json")!!.reader(Charsets.UTF_8).readText(),
.reader(Charsets.UTF_8).readText(), JsonObject::class.java JsonObject::class.java
).get("version").asString ).get("version").asString
val viaWebServer = WebDashboardServer()
var runningServer = true var runningServer = true
val viaaasLogger = LoggerFactory.getLogger("VIAaaS")
val httpClient = HttpClient { val httpClient = HttpClient {
defaultRequest { install(UserAgent) {
header("User-Agent", "VIAaaS/$viaaasVer") agent = "VIAaaS/$viaaasVer"
} }
install(JsonFeature) { install(JsonFeature) {
serializer = GsonSerializer() serializer = GsonSerializer()
} }
} }
val initFuture = CompletableFuture<Unit>() val initFuture = CompletableFuture<Unit>()
// Minecraft doesn't have forward secrecy // Minecraft doesn't have forward secrecy
@ -72,8 +66,6 @@ val mcCryptoKey = KeyPairGenerator.getInstance("RSA").let {
it.genKeyPair() it.genKeyPair()
} }
val secureRandom = if (VIAaaSConfig.useStrongRandom) SecureRandom.getInstanceStrong() else SecureRandom()
fun eventLoopGroup(): EventLoopGroup { fun eventLoopGroup(): EventLoopGroup {
if (VIAaaSConfig.isNativeTransportMc) { if (VIAaaSConfig.isNativeTransportMc) {
if (Epoll.isAvailable()) return EpollEventLoopGroup() if (Epoll.isAvailable()) return EpollEventLoopGroup()
@ -101,7 +93,7 @@ fun channelSocketFactory(): ChannelFactory<SocketChannel> {
fun main(args: Array<String>) { fun main(args: Array<String>) {
// Stolen from https://github.com/VelocityPowered/Velocity/blob/dev/1.1.0/proxy/src/main/java/com/velocitypowered/proxy/Velocity.java // Stolen from https://github.com/VelocityPowered/Velocity/blob/dev/1.1.0/proxy/src/main/java/com/velocitypowered/proxy/Velocity.java
if (System.getProperty("io.netty.allocator.maxOrder") == null) { if (System.getProperty("io.netty.allocator.maxOrder") == null) {
System.setProperty("io.netty.allocator.maxOrder", "9"); System.setProperty("io.netty.allocator.maxOrder", "9")
} }
File("config/https.jks").apply { File("config/https.jks").apply {
@ -127,7 +119,7 @@ fun main(args: Array<String>) {
val future = ServerBootstrap() val future = ServerBootstrap()
.group(parent, child) .group(parent, child)
.channelFactory(channelServerSocketFactory()) .channelFactory(channelServerSocketFactory())
.childHandler(FrontChannelInit) .childHandler(FrontEndInit)
.childOption(ChannelOption.IP_TOS, 0x18) .childOption(ChannelOption.IP_TOS, 0x18)
.childOption(ChannelOption.TCP_NODELAY, true) .childOption(ChannelOption.TCP_NODELAY, true)
.bind(InetAddress.getByName(VIAaaSConfig.bindAddress), VIAaaSConfig.port) .bind(InetAddress.getByName(VIAaaSConfig.bindAddress), VIAaaSConfig.port)
@ -143,9 +135,7 @@ fun main(args: Array<String>) {
initFuture.complete(Unit) initFuture.complete(Unit)
if (runningServer) {
VIAaaSConsole().start() VIAaaSConsole().start()
}
ktorServer?.stop(1000, 1000) ktorServer?.stop(1000, 1000)
httpClient.close() httpClient.close()
@ -155,208 +145,6 @@ fun main(args: Array<String>) {
Via.getManager().destroy() Via.getManager().destroy()
} }
class VIAaaSConsole : SimpleTerminalConsole(), ViaCommandSender {
val commands = hashMapOf<String, (MutableList<String>?, String, Array<String>) -> Unit>()
override fun isRunning(): Boolean = runningServer
init {
commands["stop"] = { suggestion, _, _ -> if (suggestion == null) this.shutdown() }
commands["end"] = commands["stop"]!!
commands["viaversion"] = { suggestion, _, args ->
if (suggestion == null) {
Via.getManager().commandHandler.onCommand(this, args)
} else {
suggestion.addAll(Via.getManager().commandHandler.onTabComplete(this, args))
}
}
commands["viaver"] = commands["viaversion"]!!
commands["vvcloud"] = commands["viaversion"]!!
commands["help"] = { suggestion, _, _ ->
if (suggestion == null) sendMessage(commands.entries.groupBy { it.value }.entries.joinToString(", ") {
it.value.joinToString("/") { it.key }
})
}
commands["?"] = commands["help"]!!
commands["ver"] = { suggestion, _, _ ->
if (suggestion == null) sendMessage(viaaasVer)
}
commands["list"] = { suggestion, _, _ ->
if (suggestion == null) {
sendMessage("List of player connections: ")
Via.getPlatform().connectionManager.connections.forEach {
val backAddr = it.channel?.remoteAddress()
val pVer = it.protocolInfo?.protocolVersion?.let {
ProtocolVersion.getProtocol(it)
}
val backName = it.protocolInfo?.username
val backVer = it.protocolInfo?.serverProtocolVersion?.let {
ProtocolVersion.getProtocol(it)
}
val pAddr =
it.channel?.pipeline()?.get(CloudMinecraftHandler::class.java)?.other?.remoteAddress()
val pName = it.channel?.pipeline()?.get(CloudMinecraftHandler::class.java)?.data?.frontName
sendMessage("$pAddr $pVer ($pName) -> $backVer ($backName) $backAddr")
}
}
}
}
override fun buildReader(builder: LineReaderBuilder): LineReader {
// Stolen from Velocity
return super.buildReader(builder.appName("VIAaaS").completer { _, line, candidates ->
try {
val cmdArgs = line.line().substring(0, line.cursor()).split(" ")
val alias = cmdArgs[0]
val args = cmdArgs.filterIndexed { i, _ -> i > 0 }
if (cmdArgs.size == 1) {
candidates.addAll(commands.keys.filter { it.startsWith(alias, ignoreCase = true) }
.map { Candidate(it) })
} else {
val cmd = commands[alias.toLowerCase()]
if (cmd != null) {
val suggestions = mutableListOf<String>()
cmd(suggestions, alias, args.toTypedArray())
candidates.addAll(suggestions.map(::Candidate))
}
}
} catch (e: Exception) {
sendMessage("Error completing command: $e")
}
})
}
override fun runCommand(command: String) {
val cmd = command.split(" ")
try {
val alias = cmd[0].toLowerCase()
val args = cmd.subList(1, cmd.size).toTypedArray()
val runnable = commands[alias]
if (runnable == null) {
sendMessage("unknown command, try 'help'")
} else {
runnable(null, alias, args)
}
} catch (e: Exception) {
sendMessage("Error running command: $e")
}
}
override fun shutdown() {
viaaasLogger.info("Shutting down...")
runningServer = false
}
override fun sendMessage(p0: String) {
LoggerFactory.getLogger(this.name).info(p0)
}
override fun hasPermission(p0: String): Boolean = true
override fun getUUID(): UUID = UUID.fromString(name)
override fun getName(): String = "VIAaaS Console"
}
fun Application.mainWeb() { fun Application.mainWeb() {
ViaWebApp().apply { main() } ViaWebApp().apply { main() }
} }
object VIAaaSConfig : Config(File("config/viaaas.yml")) {
init {
reloadConfig()
}
override fun getUnsupportedOptions() = emptyList<String>().toMutableList()
override fun getDefaultConfigURL() = VIAaaSConfig::class.java.classLoader.getResource("viaaas.yml")!!
override fun handleConfig(p0: MutableMap<String, Any>?) {
}
val isNativeTransportMc: Boolean get() = this.getBoolean("native-transport-mc", true)
val port: Int get() = this.getInt("port", 25565)
val bindAddress: String get() = this.getString("bind-address", "localhost")!!
val hostName: String get() = this.getString("host-name", "viaaas.localhost")!!
val mcRsaSize: Int get() = this.getInt("mc-rsa-size", 4096)
val useStrongRandom: Boolean get() = this.getBoolean("use-strong-random", true)
val blockLocalAddress: Boolean get() = this.getBoolean("block-local-address", true)
val requireHostName: Boolean get() = this.getBoolean("require-host-name", true)
val defaultBackendPort: Int get() = this.getInt("default-backend-port", 25565)
val blockedBackAddresses: List<String>
get() = this.get(
"blocked-back-addresses",
List::class.java,
emptyList<String>()
)!!.map { it as String }
val allowedBackAddresses: List<String>
get() = this.get(
"allowed-back-addresses",
List::class.java,
emptyList<String>()
)!!.map { it as String }
}
class VIAaaSAddress {
var serverAddress: String? = null
var viaSuffix: String? = null
var viaOptions: String? = null
var protocol: Int? = null
var port: Int? = null
var online: Boolean? = null
var username: String? = null
fun parse(rawAddress: String, viaHostName: String): VIAaaSAddress {
val address = rawAddress.removeSuffix(".")
val suffixRemoved = address.removeSuffix(".$viaHostName")
if (suffixRemoved == address) {
serverAddress = address
return this
}
var endOfOptions = false
val optionsList = arrayListOf<String>()
serverAddress = suffixRemoved.split('.').asReversed().filter {
if (endOfOptions || !parseOption(it)) {
endOfOptions = true
true
} else {
optionsList.add(it)
false
}
}.asReversed().joinToString(".")
viaOptions = optionsList.asReversed().joinToString(".")
viaSuffix = viaHostName
return this
}
fun parseOption(part: String): Boolean {
if (part.startsWith("_")) {
val arg = part.substring(2)
when {
part.startsWith("_p", ignoreCase = true) -> port = arg.toInt()
part.startsWith("_o", ignoreCase = true) -> {
online = when {
arg.startsWith("t", ignoreCase = true) -> true
arg.startsWith("f", ignoreCase = true) -> false
else -> null
}
}
part.startsWith("_v", ignoreCase = true) -> {
try {
protocol = arg.toInt()
} catch (e: NumberFormatException) {
ProtocolVersion.getClosest(arg.replace("_", "."))?.also {
protocol = it.version
}
}
}
part.startsWith("_u", ignoreCase = true) -> {
if (arg.length > 16) throw IllegalArgumentException("Invalid username")
username = arg
}
}
return true
}
return false
}
}

View File

@ -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
}
}

View File

@ -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) {
}
}

View File

@ -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()
}
}
}
}

View File

@ -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())
}
}

View File

@ -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)
}
}

View File

@ -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!!!")
}
}

View File

@ -0,0 +1,5 @@
package com.github.creeper123123321.viaaas.command
import us.myles.ViaVersion.commands.ViaCommandHandler
object CloudCommands : ViaCommandHandler()

View File

@ -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"
}

View File

@ -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))
}
}

View File

@ -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)
}
}

View File

@ -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()
}
}
}

View File

@ -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)
}

View File

@ -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
)
)
}
}

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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")
}
}

View File

@ -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()
}
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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) {
}
}

View File

@ -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)
}
}

View File

@ -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() {
}
}

View File

@ -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)

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -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)
}
}

View File

@ -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")
}
}
}
}

View File

@ -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()
)

View File

@ -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)
}
}

View File

@ -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) {
}
}

View File

@ -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)
}