Implement force online/offline mode and offline name listening, create separated packet codec handler

This commit is contained in:
creeper123123321 2021-02-04 12:18:02 -03:00
parent c7a126cf61
commit b41e29e5b7
5 changed files with 255 additions and 184 deletions

View File

@ -1,6 +1,7 @@
package com.github.creeper123123321.viaaas package com.github.creeper123123321.viaaas
import io.netty.buffer.ByteBuf import io.netty.buffer.ByteBuf
import io.netty.buffer.ByteBufAllocator
import io.netty.channel.Channel import io.netty.channel.Channel
import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelInitializer import io.netty.channel.ChannelInitializer
@ -27,6 +28,7 @@ object FrontChannelInit : ChannelInitializer<Channel>() {
.addLast("frame", FrameCodec()) .addLast("frame", FrameCodec())
// "compress" // "compress"
.addLast("flow-handler", FlowControlHandler()) .addLast("flow-handler", FlowControlHandler())
.addLast("mc", CloudMinecraftCodec())
.addLast( .addLast(
"handler", CloudMinecraftHandler( "handler", CloudMinecraftHandler(
ConnectionData(frontChannel = ch), other = null, frontEnd = true ConnectionData(frontChannel = ch), other = null, frontEnd = true
@ -44,10 +46,34 @@ class BackendInit(val connectionData: ConnectionData) : ChannelInitializer<Chann
.addLast("frame", FrameCodec()) .addLast("frame", FrameCodec())
// compress // compress
.addLast("via-codec", CloudViaCodec(user)) .addLast("via-codec", CloudViaCodec(user))
.addLast("mc", CloudMinecraftCodec())
.addLast("handler", CloudMinecraftHandler(connectionData, connectionData.frontChannel, frontEnd = false)) .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>() { class CloudCrypto(val cipherDecode: Cipher, var cipherEncode: Cipher) : MessageToMessageCodec<ByteBuf, ByteBuf>() {
override fun decode(ctx: ChannelHandlerContext, msg: ByteBuf, out: MutableList<Any>) { override fun decode(ctx: ChannelHandlerContext, msg: ByteBuf, out: MutableList<Any>) {
val i = msg.readerIndex() val i = msg.readerIndex()
@ -130,9 +156,9 @@ class CloudCompressionCodec(val threshold: Int) : MessageToMessageCodec<ByteBuf,
} }
class FrameCodec : ByteToMessageCodec<ByteBuf>() { val badLength = DecoderException("Invalid length!")
val badLength = DecoderException("Invalid length!")
class FrameCodec : ByteToMessageCodec<ByteBuf>() {
override fun decode(ctx: ChannelHandlerContext, input: ByteBuf, out: MutableList<Any>) { override fun decode(ctx: ChannelHandlerContext, input: ByteBuf, out: MutableList<Any>) {
if (!ctx.channel().isActive) { if (!ctx.channel().isActive) {
input.clear() // Ignore, should prevent DoS https://github.com/SpigotMC/BungeeCord/pull/2908 input.clear() // Ignore, should prevent DoS https://github.com/SpigotMC/BungeeCord/pull/2908

View File

@ -6,7 +6,6 @@ import com.google.gson.Gson
import com.google.gson.JsonObject import com.google.gson.JsonObject
import io.ktor.client.request.* import io.ktor.client.request.*
import io.netty.bootstrap.Bootstrap import io.netty.bootstrap.Bootstrap
import io.netty.buffer.ByteBuf
import io.netty.buffer.ByteBufAllocator import io.netty.buffer.ByteBufAllocator
import io.netty.channel.* import io.netty.channel.*
import io.netty.channel.socket.SocketChannel import io.netty.channel.socket.SocketChannel
@ -14,7 +13,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import us.myles.ViaVersion.api.protocol.ProtocolVersion
import us.myles.ViaVersion.api.type.Type import us.myles.ViaVersion.api.type.Type
import us.myles.ViaVersion.exception.CancelCodecException import us.myles.ViaVersion.exception.CancelCodecException
import us.myles.ViaVersion.packets.State import us.myles.ViaVersion.packets.State
@ -26,6 +24,8 @@ import java.security.MessageDigest
import java.security.PrivateKey import java.security.PrivateKey
import java.security.PublicKey import java.security.PublicKey
import java.util.* import java.util.*
import java.util.UUID
import java.util.concurrent.CompletableFuture
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
@ -42,11 +42,6 @@ class ConnectionData(
var frontOnline: Boolean? = null, // todo var frontOnline: Boolean? = null, // todo
var frontName: String? = null, var frontName: String? = null,
var backName: String? = null, var backName: String? = null,
var backPublicKey: PublicKey? = null,
var backToken: ByteArray? = null,
var frontToken: ByteArray? = null,
var frontServerId: String? = null,
var backServerId: String? = null,
var frontVer: Int? = null, var frontVer: Int? = null,
var backVer: Int? = null, var backVer: Int? = null,
) { ) {
@ -58,13 +53,12 @@ class CloudMinecraftHandler(
val data: ConnectionData, val data: ConnectionData,
var other: Channel?, var other: Channel?,
val frontEnd: Boolean val frontEnd: Boolean
) : SimpleChannelInboundHandler<ByteBuf>() { ) : SimpleChannelInboundHandler<Packet>() {
var remoteAddress: SocketAddress? = null var remoteAddress: SocketAddress? = null
override fun channelRead0(ctx: ChannelHandlerContext, msg: ByteBuf) { override fun channelRead0(ctx: ChannelHandlerContext, packet: Packet) {
if (ctx.channel().isActive && msg.isReadable) { if (ctx.channel().isActive) {
data.state.handleMessage(this, ctx, msg) data.state.handlePacket(this, ctx, packet)
if (msg.isReadable) throw IllegalStateException("Remaining bytes!!!")
} }
} }
@ -97,9 +91,10 @@ class CloudMinecraftHandler(
} }
interface MinecraftConnectionState { interface MinecraftConnectionState {
fun handleMessage( val state: State
fun handlePacket(
handler: CloudMinecraftHandler, ctx: ChannelHandlerContext, handler: CloudMinecraftHandler, ctx: ChannelHandlerContext,
msg: ByteBuf packet: Packet
) )
fun disconnect(handler: CloudMinecraftHandler, msg: String) { fun disconnect(handler: CloudMinecraftHandler, msg: String) {
@ -112,19 +107,27 @@ interface MinecraftConnectionState {
} }
class HandshakeState : MinecraftConnectionState { class HandshakeState : MinecraftConnectionState {
override fun handleMessage(handler: CloudMinecraftHandler, ctx: ChannelHandlerContext, msg: ByteBuf) { fun connectBack(handler: CloudMinecraftHandler, socketAddr: InetSocketAddress): ChannelFuture {
val packet = PacketRegistry.decode( return Bootstrap()
msg, .handler(BackendInit(handler.data))
ProtocolVersion.v1_8, // we still dont know what protocol it is .channelFactory(channelSocketFactory())
State.HANDSHAKE, .group(handler.data.frontChannel.eventLoop())
handler.frontEnd .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!") if (packet !is HandshakePacket) throw IllegalArgumentException("Invalid packet!")
handler.data.frontVer = packet.protocolId handler.data.frontVer = packet.protocolId
when (packet.nextState.ordinal) { when (packet.nextState.ordinal) {
1 -> handler.data.state = StatusState 1 -> handler.data.state = StatusState
2 -> handler.data.state = LoginState 2 -> handler.data.state = LoginState()
else -> throw IllegalStateException("Invalid next state") else -> throw IllegalStateException("Invalid next state")
} }
@ -161,22 +164,18 @@ class HandshakeState : MinecraftConnectionState {
if (checkLocalAddress(socketAddr.address) if (checkLocalAddress(socketAddr.address)
|| matchesAddress(socketAddr, VIAaaSConfig.blockedBackAddresses) || matchesAddress(socketAddr, VIAaaSConfig.blockedBackAddresses)
|| !matchesAddress(socketAddr, VIAaaSConfig.allowedBackAddresses)) { || !matchesAddress(socketAddr, VIAaaSConfig.allowedBackAddresses)
) {
throw SecurityException("Not allowed") throw SecurityException("Not allowed")
} }
val bootstrap = Bootstrap() val future = connectBack(handler, socketAddr)
.handler(BackendInit(handler.data))
.channelFactory(channelSocketFactory())
.group(handler.data.frontChannel.eventLoop())
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 15_000) // Half of mc timeout
.connect(socketAddr)
bootstrap.addListener { future.addListener {
if (it.isSuccess) { if (it.isSuccess) {
mcLogger.info("Connected ${handler.remoteAddress} -> $socketAddr") mcLogger.info("Connected ${handler.remoteAddress} -> $socketAddr")
val backChan = bootstrap.channel() as SocketChannel val backChan = future.channel() as SocketChannel
handler.data.backChannel = backChan handler.data.backChannel = backChan
handler.other = backChan handler.other = backChan
@ -205,15 +204,14 @@ class HandshakeState : MinecraftConnectionState {
} }
} }
object LoginState : MinecraftConnectionState { class LoginState : MinecraftConnectionState {
override fun handleMessage(handler: CloudMinecraftHandler, ctx: ChannelHandlerContext, msg: ByteBuf) { val callbackPlayerId = CompletableFuture<String>()
val packet = PacketRegistry.decode( lateinit var frontToken: ByteArray
msg, lateinit var frontServerId: String
ProtocolVersion.getProtocol(handler.data.frontVer!!), override val state: State
State.LOGIN, get() = State.LOGIN
handler.frontEnd
)
override fun handlePacket(handler: CloudMinecraftHandler, ctx: ChannelHandlerContext, packet: Packet) {
when (packet) { when (packet) {
is LoginStart -> handleLoginStart(handler, packet) is LoginStart -> handleLoginStart(handler, packet)
is CryptoResponse -> handleCryptoResponse(handler, packet) is CryptoResponse -> handleCryptoResponse(handler, packet)
@ -253,12 +251,7 @@ object LoginState : MinecraftConnectionState {
} }
} }
fun handleCryptoRequest(handler: CloudMinecraftHandler, cryptoRequest: CryptoRequest) { fun authenticateOnlineFront(frontHandler: CloudMinecraftHandler) {
val data = handler.data
data.backServerId = cryptoRequest.serverId
data.backPublicKey = cryptoRequest.publicKey
data.backToken = cryptoRequest.token
val id = "VIAaaS" + ByteArray(10).let { val id = "VIAaaS" + ByteArray(10).let {
secureRandom.nextBytes(it) secureRandom.nextBytes(it)
Base64.getEncoder().withoutPadding().encodeToString(it) Base64.getEncoder().withoutPadding().encodeToString(it)
@ -269,14 +262,66 @@ object LoginState : MinecraftConnectionState {
secureRandom.nextBytes(it) secureRandom.nextBytes(it)
it it
} }
data.frontToken = token frontToken = token
data.frontServerId = id frontServerId = id
val cryptoRequest = CryptoRequest()
cryptoRequest.serverId = id cryptoRequest.serverId = id
cryptoRequest.publicKey = mcCryptoKey.public cryptoRequest.publicKey = mcCryptoKey.public
cryptoRequest.token = token cryptoRequest.token = token
forward(handler, cryptoRequest) frontHandler.data.frontChannel.writeAndFlush(cryptoRequest)
}
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) { fun handleCryptoResponse(handler: CloudMinecraftHandler, cryptoResponse: CryptoResponse) {
@ -285,23 +330,16 @@ object LoginState : MinecraftConnectionState {
// RSA token - wat??? why is it encrypted with RSA if it was sent unencrypted? // RSA token - wat??? why is it encrypted with RSA if it was sent unencrypted?
val decryptedToken = decryptRsa(mcCryptoKey.private, cryptoResponse.encryptedToken) val decryptedToken = decryptRsa(mcCryptoKey.private, cryptoResponse.encryptedToken)
if (!decryptedToken.contentEquals(handler.data.frontToken!!)) throw IllegalStateException("invalid token!") if (!decryptedToken.contentEquals(frontToken)) throw IllegalStateException("invalid token!")
val aesEn = mcCfb8(frontKey, Cipher.ENCRYPT_MODE) val aesEn = mcCfb8(frontKey, Cipher.ENCRYPT_MODE)
val aesDe = mcCfb8(frontKey, Cipher.DECRYPT_MODE) val aesDe = mcCfb8(frontKey, Cipher.DECRYPT_MODE)
handler.data.frontChannel.pipeline().addBefore("frame", "crypto", CloudCrypto(aesDe, aesEn)) handler.data.frontChannel.pipeline().addBefore("frame", "crypto", CloudCrypto(aesDe, aesEn))
generateServerHash(handler.data.frontServerId!!, frontKey, mcCryptoKey.public) generateServerHash(frontServerId, frontKey, mcCryptoKey.public)
} }
val backKey = ByteArray(16).let {
secureRandom.nextBytes(it)
it
}
val backHash = generateServerHash(handler.data.backServerId!!, backKey, handler.data.backPublicKey!!)
handler.data.frontChannel.setAutoRead(false) handler.data.frontChannel.setAutoRead(false)
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
try { try {
@ -309,45 +347,40 @@ object LoginState : MinecraftConnectionState {
"https://sessionserver.mojang.com/session/minecraft/hasJoined?username=" + "https://sessionserver.mojang.com/session/minecraft/hasJoined?username=" +
UrlEscapers.urlFormParameterEscaper().escape(handler.data.frontName!!) + UrlEscapers.urlFormParameterEscaper().escape(handler.data.frontName!!) +
"&serverId=$frontHash" "&serverId=$frontHash"
) ) ?: throw IllegalArgumentException("Couldn't authenticate with session servers")
?: throw IllegalArgumentException("Couldn't authenticate with session servers")
val sessionJoin = viaWebServer.requestSessionJoin( val id = profile.get("id")!!.asString
fromUndashed(profile.get("id")!!.asString), mcLogger.info("Validated front-end session: ${handler.data.frontName} $id")
handler.data.backName!!, callbackPlayerId.complete(id)
backHash,
handler.remoteAddress!!, // Frontend handler
handler.data.backPublicKey!!
)
val backChan = handler.other!!
sessionJoin.whenCompleteAsync({ _, throwable ->
if (throwable != null) {
handler.disconnect("Online mode error: $throwable")
} else {
cryptoResponse.encryptedKey = encryptRsa(handler.data.backPublicKey!!, backKey)
cryptoResponse.encryptedToken =
encryptRsa(handler.data.backPublicKey!!, handler.data.backToken!!)
forward(handler, 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) { } catch (e: Exception) {
handler.disconnect("Online mode error: $e") callbackPlayerId.completeExceptionally(e)
} }
handler.data.frontChannel.setAutoRead(true) handler.data.frontChannel.setAutoRead(true)
} }
} }
fun handleLoginStart(handler: CloudMinecraftHandler, loginStart: LoginStart) { fun handleLoginStart(handler: CloudMinecraftHandler, loginStart: LoginStart) {
if (loginStart.username.length > 16) throw badLength
handler.data.frontName = loginStart.username handler.data.frontName = loginStart.username
handler.data.backName = handler.data.backName ?: handler.data.frontName handler.data.backName = handler.data.backName ?: handler.data.frontName
loginStart.username = handler.data.backName!! loginStart.username = handler.data.backName!!
forward(handler, loginStart)
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) { override fun disconnect(handler: CloudMinecraftHandler, msg: String) {
@ -367,12 +400,12 @@ object LoginState : MinecraftConnectionState {
} }
object StatusState : MinecraftConnectionState { object StatusState : MinecraftConnectionState {
override fun handleMessage(handler: CloudMinecraftHandler, ctx: ChannelHandlerContext, msg: ByteBuf) { override val state: State
val i = msg.readerIndex() get() = State.STATUS
if (Type.VAR_INT.readPrimitive(msg) !in 0..1) throw IllegalArgumentException("Invalid packet id!")
msg.readerIndex(i) override fun handlePacket(handler: CloudMinecraftHandler, ctx: ChannelHandlerContext, packet: Packet) {
forward(handler, msg.retainedSlice()) if ((packet as UnknownPacket).id !in 0..1) throw IllegalArgumentException("Invalid packet id!")
msg.clear() forward(handler, packet)
} }
override fun disconnect(handler: CloudMinecraftHandler, msg: String) { override fun disconnect(handler: CloudMinecraftHandler, msg: String) {
@ -394,12 +427,12 @@ object StatusState : MinecraftConnectionState {
} }
object PlayState : MinecraftConnectionState { object PlayState : MinecraftConnectionState {
override fun handleMessage(handler: CloudMinecraftHandler, ctx: ChannelHandlerContext, msg: ByteBuf) { override val state: State
val i = msg.readerIndex() get() = State.PLAY
if (Type.VAR_INT.readPrimitive(msg) !in 0..127) throw IllegalArgumentException("Invalid packet id!")
msg.readerIndex(i) override fun handlePacket(handler: CloudMinecraftHandler, ctx: ChannelHandlerContext, packet: Packet) {
forward(handler, msg.retainedSlice()) if ((packet as UnknownPacket).id !in 0..127) throw IllegalArgumentException("Invalid packet id!")
msg.clear() forward(handler, packet)
} }
override fun disconnect(handler: CloudMinecraftHandler, msg: String) { override fun disconnect(handler: CloudMinecraftHandler, msg: String) {
@ -449,22 +482,17 @@ fun generateServerHash(serverId: String, sharedSecret: ByteArray?, key: PublicKe
private fun forward(handler: CloudMinecraftHandler, packet: Packet, flush: Boolean = false) { private fun forward(handler: CloudMinecraftHandler, packet: Packet, flush: Boolean = false) {
val msg = ByteBufAllocator.DEFAULT.buffer() val msg = ByteBufAllocator.DEFAULT.buffer()
try { try {
PacketRegistry.encode(packet, msg, ProtocolVersion.getProtocol(handler.data.frontVer!!)) val ch = handler.other!!
forward(handler, msg.retain(), flush) if (flush) {
ch.writeAndFlush(packet, ch.voidPromise())
} else {
ch.write(packet, ch.voidPromise())
}
} finally { } finally {
msg.release() msg.release()
} }
} }
private fun forward(handler: CloudMinecraftHandler, byteBuf: ByteBuf, flush: Boolean = false) {
val ch = handler.other!!
if (flush) {
ch.writeAndFlush(byteBuf, ch.voidPromise())
} else {
ch.write(byteBuf, ch.voidPromise())
}
}
private fun resolveSrv(address: String, port: Int): Pair<String, Int> { private fun resolveSrv(address: String, port: Int): Pair<String, Int> {
if (port == 25565) { if (port == 25565) {
try { try {
@ -509,3 +537,7 @@ private fun matchAddress(addr: String, list: List<String>): Boolean {
list.contains(query) 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

@ -16,8 +16,8 @@ import kotlin.properties.Delegates
* A mutable object which represents a Minecraft packet data * A mutable object which represents a Minecraft packet data
*/ */
interface Packet { interface Packet {
fun decode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) fun decode(byteBuf: ByteBuf, protocolVersion: Int)
fun encode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) fun encode(byteBuf: ByteBuf, protocolVersion: Int)
} }
object PacketRegistry { object PacketRegistry {
@ -70,24 +70,24 @@ object PacketRegistry {
) )
fun getPacketConstructor( fun getPacketConstructor(
protocolVersion: ProtocolVersion, protocolVersion: Int,
state: State, state: State,
id: Int, id: Int,
serverBound: Boolean serverBound: Boolean
): Supplier<out Packet>? { ): Supplier<out Packet>? {
return entries.firstOrNull { return entries.firstOrNull {
it.serverBound == serverBound && it.state == state it.serverBound == serverBound && it.state == state
&& it.versionRange.contains(protocolVersion.version) && it.id == id && it.versionRange.contains(protocolVersion) && it.id == id
}?.constructor }?.constructor
} }
fun getPacketId(packetClass: Class<out Packet>, protocolVersion: ProtocolVersion): Int? { fun getPacketId(packetClass: Class<out Packet>, protocolVersion: Int): Int? {
return entries.firstOrNull { return entries.firstOrNull {
it.versionRange.contains(protocolVersion.version) && it.packetClass == packetClass it.versionRange.contains(protocolVersion) && it.packetClass == packetClass
}?.id }?.id
} }
fun decode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion, state: State, serverBound: Boolean): Packet { fun decode(byteBuf: ByteBuf, protocolVersion: Int, state: State, serverBound: Boolean): Packet {
val packetId = Type.VAR_INT.readPrimitive(byteBuf) val packetId = Type.VAR_INT.readPrimitive(byteBuf)
val packet = val packet =
getPacketConstructor(protocolVersion, state, packetId, serverBound)?.get() ?: UnknownPacket(packetId) getPacketConstructor(protocolVersion, state, packetId, serverBound)?.get() ?: UnknownPacket(packetId)
@ -96,7 +96,7 @@ object PacketRegistry {
return packet return packet
} }
fun encode(packet: Packet, byteBuf: ByteBuf, protocolVersion: ProtocolVersion) { fun encode(packet: Packet, byteBuf: ByteBuf, protocolVersion: Int) {
val id = if (packet is UnknownPacket) { val id = if (packet is UnknownPacket) {
packet.id packet.id
} else { } else {
@ -108,13 +108,13 @@ object PacketRegistry {
} }
class UnknownPacket(val id: Int) : Packet { class UnknownPacket(val id: Int) : Packet {
lateinit var content: ByteBuf lateinit var content: ByteArray
override fun decode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) { override fun decode(byteBuf: ByteBuf, protocolVersion: Int) {
content = byteBuf.slice().clear() content = ByteArray(byteBuf.readableBytes()).also { byteBuf.readBytes(it) }
} }
override fun encode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) { override fun encode(byteBuf: ByteBuf, protocolVersion: Int) {
byteBuf.writeBytes(content) byteBuf.writeBytes(content)
} }
} }
@ -127,14 +127,14 @@ class HandshakePacket : Packet {
var port by Delegates.notNull<Int>() var port by Delegates.notNull<Int>()
lateinit var nextState: State lateinit var nextState: State
override fun decode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) { override fun decode(byteBuf: ByteBuf, protocolVersion: Int) {
protocolId = Type.VAR_INT.readPrimitive(byteBuf) protocolId = Type.VAR_INT.readPrimitive(byteBuf)
address = Type.STRING.read(byteBuf) address = Type.STRING.read(byteBuf)
port = byteBuf.readUnsignedShort() port = byteBuf.readUnsignedShort()
nextState = State.values()[Type.VAR_INT.readPrimitive(byteBuf)] nextState = State.values()[Type.VAR_INT.readPrimitive(byteBuf)]
} }
override fun encode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) { override fun encode(byteBuf: ByteBuf, protocolVersion: Int) {
Type.VAR_INT.writePrimitive(byteBuf, protocolId) Type.VAR_INT.writePrimitive(byteBuf, protocolId)
Type.STRING.write(byteBuf, address) Type.STRING.write(byteBuf, address)
byteBuf.writeShort(port) byteBuf.writeShort(port)
@ -145,11 +145,11 @@ class HandshakePacket : Packet {
class LoginStart : Packet { class LoginStart : Packet {
lateinit var username: String lateinit var username: String
override fun decode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) { override fun decode(byteBuf: ByteBuf, protocolVersion: Int) {
username = Type.STRING.read(byteBuf) username = Type.STRING.read(byteBuf)
} }
override fun encode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) { override fun encode(byteBuf: ByteBuf, protocolVersion: Int) {
Type.STRING.write(byteBuf, username) Type.STRING.write(byteBuf, username)
} }
} }
@ -158,8 +158,8 @@ class CryptoResponse : Packet {
lateinit var encryptedKey: ByteArray lateinit var encryptedKey: ByteArray
lateinit var encryptedToken: ByteArray lateinit var encryptedToken: ByteArray
override fun decode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) { override fun decode(byteBuf: ByteBuf, protocolVersion: Int) {
if (protocolVersion.version >= ProtocolVersion.v1_8.version) { if (protocolVersion >= ProtocolVersion.v1_8.version) {
encryptedKey = Type.BYTE_ARRAY_PRIMITIVE.read(byteBuf) encryptedKey = Type.BYTE_ARRAY_PRIMITIVE.read(byteBuf)
encryptedToken = Type.BYTE_ARRAY_PRIMITIVE.read(byteBuf) encryptedToken = Type.BYTE_ARRAY_PRIMITIVE.read(byteBuf)
} else { } else {
@ -168,8 +168,8 @@ class CryptoResponse : Packet {
} }
} }
override fun encode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) { override fun encode(byteBuf: ByteBuf, protocolVersion: Int) {
if (protocolVersion.version >= ProtocolVersion.v1_8.version) { if (protocolVersion >= ProtocolVersion.v1_8.version) {
Type.BYTE_ARRAY_PRIMITIVE.write(byteBuf, encryptedKey) Type.BYTE_ARRAY_PRIMITIVE.write(byteBuf, encryptedKey)
Type.BYTE_ARRAY_PRIMITIVE.write(byteBuf, encryptedToken) Type.BYTE_ARRAY_PRIMITIVE.write(byteBuf, encryptedToken)
} else { } else {
@ -184,16 +184,16 @@ class CryptoResponse : Packet {
class PluginResponse : Packet { class PluginResponse : Packet {
var id by Delegates.notNull<Int>() var id by Delegates.notNull<Int>()
var success by Delegates.notNull<Boolean>() var success by Delegates.notNull<Boolean>()
lateinit var data: ByteBuf lateinit var data: ByteArray
override fun decode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) { override fun decode(byteBuf: ByteBuf, protocolVersion: Int) {
id = Type.VAR_INT.readPrimitive(byteBuf) id = Type.VAR_INT.readPrimitive(byteBuf)
success = byteBuf.readBoolean() success = byteBuf.readBoolean()
if (success) { if (success) {
data = byteBuf.slice().clear() data = ByteArray(byteBuf.readableBytes()).also { byteBuf.readBytes(it) }
} }
} }
override fun encode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) { override fun encode(byteBuf: ByteBuf, protocolVersion: Int) {
Type.VAR_INT.writePrimitive(byteBuf, id) Type.VAR_INT.writePrimitive(byteBuf, id)
byteBuf.writeBoolean(success) byteBuf.writeBoolean(success)
if (success) { if (success) {
@ -204,11 +204,11 @@ class PluginResponse : Packet {
class LoginDisconnect : Packet { class LoginDisconnect : Packet {
lateinit var msg: String lateinit var msg: String
override fun decode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) { override fun decode(byteBuf: ByteBuf, protocolVersion: Int) {
msg = Type.STRING.read(byteBuf) msg = Type.STRING.read(byteBuf)
} }
override fun encode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) { override fun encode(byteBuf: ByteBuf, protocolVersion: Int) {
Type.STRING.write(byteBuf, msg) Type.STRING.write(byteBuf, msg)
} }
} }
@ -218,9 +218,9 @@ class CryptoRequest : Packet {
lateinit var publicKey: PublicKey lateinit var publicKey: PublicKey
lateinit var token: ByteArray lateinit var token: ByteArray
override fun decode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) { override fun decode(byteBuf: ByteBuf, protocolVersion: Int) {
serverId = Type.STRING.read(byteBuf) serverId = Type.STRING.read(byteBuf)
if (protocolVersion.version >= ProtocolVersion.v1_8.version) { if (protocolVersion >= ProtocolVersion.v1_8.version) {
publicKey = KeyFactory.getInstance("RSA") publicKey = KeyFactory.getInstance("RSA")
.generatePublic(X509EncodedKeySpec(Type.BYTE_ARRAY_PRIMITIVE.read(byteBuf))) .generatePublic(X509EncodedKeySpec(Type.BYTE_ARRAY_PRIMITIVE.read(byteBuf)))
token = Type.BYTE_ARRAY_PRIMITIVE.read(byteBuf) token = Type.BYTE_ARRAY_PRIMITIVE.read(byteBuf)
@ -231,9 +231,9 @@ class CryptoRequest : Packet {
} }
} }
override fun encode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) { override fun encode(byteBuf: ByteBuf, protocolVersion: Int) {
Type.STRING.write(byteBuf, serverId) Type.STRING.write(byteBuf, serverId)
if (protocolVersion.version >= ProtocolVersion.v1_8.version) { if (protocolVersion >= ProtocolVersion.v1_8.version) {
Type.BYTE_ARRAY_PRIMITIVE.write(byteBuf, publicKey.encoded) Type.BYTE_ARRAY_PRIMITIVE.write(byteBuf, publicKey.encoded)
Type.BYTE_ARRAY_PRIMITIVE.write(byteBuf, token) Type.BYTE_ARRAY_PRIMITIVE.write(byteBuf, token)
} else { } else {
@ -250,25 +250,25 @@ class LoginSuccess : Packet {
lateinit var id: UUID lateinit var id: UUID
lateinit var username: String lateinit var username: String
override fun decode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) { override fun decode(byteBuf: ByteBuf, protocolVersion: Int) {
id = when { id = when {
protocolVersion.version >= ProtocolVersion.v1_16.version -> { protocolVersion >= ProtocolVersion.v1_16.version -> {
Type.UUID_INT_ARRAY.read(byteBuf) Type.UUID_INT_ARRAY.read(byteBuf)
} }
protocolVersion.version >= ProtocolVersion.v1_7_6.version -> { protocolVersion >= ProtocolVersion.v1_7_6.version -> {
UUID.fromString(Type.STRING.read(byteBuf)) UUID.fromString(Type.STRING.read(byteBuf))
} }
else -> fromUndashed(Type.STRING.read(byteBuf)) else -> parseUndashedId(Type.STRING.read(byteBuf))
} }
username = Type.STRING.read(byteBuf) username = Type.STRING.read(byteBuf)
} }
override fun encode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) { override fun encode(byteBuf: ByteBuf, protocolVersion: Int) {
when { when {
protocolVersion.version >= ProtocolVersion.v1_16.version -> { protocolVersion >= ProtocolVersion.v1_16.version -> {
Type.UUID_INT_ARRAY.write(byteBuf, id) Type.UUID_INT_ARRAY.write(byteBuf, id)
} }
protocolVersion.version >= ProtocolVersion.v1_7_6.version -> { protocolVersion >= ProtocolVersion.v1_7_6.version -> {
Type.STRING.write(byteBuf, id.toString()) Type.STRING.write(byteBuf, id.toString())
} }
else -> Type.STRING.write(byteBuf, id.toString().replace("-", "")) else -> Type.STRING.write(byteBuf, id.toString().replace("-", ""))
@ -279,11 +279,11 @@ class LoginSuccess : Packet {
class SetCompression : Packet { class SetCompression : Packet {
var threshold by Delegates.notNull<Int>() var threshold by Delegates.notNull<Int>()
override fun decode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) { override fun decode(byteBuf: ByteBuf, protocolVersion: Int) {
threshold = Type.VAR_INT.readPrimitive(byteBuf) threshold = Type.VAR_INT.readPrimitive(byteBuf)
} }
override fun encode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) { override fun encode(byteBuf: ByteBuf, protocolVersion: Int) {
Type.VAR_INT.writePrimitive(byteBuf, threshold) Type.VAR_INT.writePrimitive(byteBuf, threshold)
} }
} }
@ -291,14 +291,14 @@ class SetCompression : Packet {
class PluginRequest : Packet { class PluginRequest : Packet {
var id by Delegates.notNull<Int>() var id by Delegates.notNull<Int>()
lateinit var channel: String lateinit var channel: String
lateinit var data: ByteBuf lateinit var data: ByteArray
override fun decode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) { override fun decode(byteBuf: ByteBuf, protocolVersion: Int) {
id = Type.VAR_INT.readPrimitive(byteBuf) id = Type.VAR_INT.readPrimitive(byteBuf)
channel = Type.STRING.read(byteBuf) channel = Type.STRING.read(byteBuf)
data = byteBuf.slice().clear() data = ByteArray(byteBuf.readableBytes()).also { byteBuf.readBytes(it) }
} }
override fun encode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) { override fun encode(byteBuf: ByteBuf, protocolVersion: Int) {
Type.VAR_INT.writePrimitive(byteBuf, id) Type.VAR_INT.writePrimitive(byteBuf, id)
Type.STRING.write(byteBuf, channel) Type.STRING.write(byteBuf, channel)
byteBuf.writeBytes(data) byteBuf.writeBytes(data)

View File

@ -4,20 +4,15 @@ import com.google.common.base.Preconditions
import com.google.common.cache.CacheBuilder import com.google.common.cache.CacheBuilder
import com.google.common.cache.CacheLoader import com.google.common.cache.CacheLoader
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.JsonArray
import com.google.gson.JsonObject import com.google.gson.JsonObject
import io.ktor.application.* import io.ktor.application.*
import io.ktor.client.features.json.*
import io.ktor.client.request.* import io.ktor.client.request.*
import io.ktor.client.request.forms.* import io.ktor.client.request.forms.*
import io.ktor.features.* import io.ktor.features.*
import io.ktor.http.* import io.ktor.http.*
import io.ktor.http.cio.websocket.* import io.ktor.http.cio.websocket.*
import io.ktor.http.content.* import io.ktor.http.content.*
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.* import io.ktor.routing.*
import io.ktor.util.*
import io.ktor.websocket.* import io.ktor.websocket.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.consumeEach import kotlinx.coroutines.channels.consumeEach
@ -61,7 +56,7 @@ class ViaWebApp {
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() webLogger.info("${call.request.local.remoteHost} (O: ${call.request.origin.remoteHost}) exception: $e")
viaWebServer.onException(this, e) viaWebServer.onException(this, e)
this.close(CloseReason(CloseReason.Codes.INTERNAL_ERROR, "INTERNAL ERROR")) this.close(CloseReason(CloseReason.Codes.INTERNAL_ERROR, "INTERNAL ERROR"))
} finally { } finally {
@ -78,7 +73,7 @@ class ViaWebApp {
} }
// https://github.com/VelocityPowered/Velocity/blob/6467335f74a7d1617512a55cc9acef5e109b51ac/api/src/main/java/com/velocitypowered/api/util/UuidUtils.java // https://github.com/VelocityPowered/Velocity/blob/6467335f74a7d1617512a55cc9acef5e109b51ac/api/src/main/java/com/velocitypowered/api/util/UuidUtils.java
fun fromUndashed(string: String): UUID { fun parseUndashedId(string: String): UUID {
Preconditions.checkArgument(string.length == 32, "Length is incorrect") Preconditions.checkArgument(string.length == 32, "Length is incorrect")
return UUID( return UUID(
java.lang.Long.parseUnsignedLong(string.substring(0, 16), 16), java.lang.Long.parseUnsignedLong(string.substring(0, 16), 16),
@ -100,7 +95,7 @@ class WebDashboardServer {
runBlocking { runBlocking {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
httpClient.get<JsonObject?>("https://api.mojang.com/users/profiles/minecraft/$name") httpClient.get<JsonObject?>("https://api.mojang.com/users/profiles/minecraft/$name")
?.get("id")?.asString?.let { fromUndashed(it) } ?.get("id")?.asString?.let { parseUndashedId(it) }
} }
} }
}) })
@ -184,9 +179,23 @@ class WebLogin : WebState {
val obj = Gson().fromJson(msg, JsonObject::class.java) val obj = Gson().fromJson(msg, JsonObject::class.java)
when (obj.getAsJsonPrimitive("action").asString) { 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" -> { "minecraft_id_login" -> {
val username = obj.getAsJsonPrimitive("username").asString val username = obj.get("username").asString
val code = obj.getAsJsonPrimitive("code").asString val code = obj.get("code").asString
val check = httpClient.submitForm<JsonObject>( val check = httpClient.submitForm<JsonObject>(
"https://api.minecraft.id/gateway/verify/${URLEncoder.encode(username, Charsets.UTF_8)}", "https://api.minecraft.id/gateway/verify/${URLEncoder.encode(username, Charsets.UTF_8)}",
@ -200,13 +209,13 @@ class WebLogin : WebState {
webClient.server.loginTokens.put(token, uuid) webClient.server.loginTokens.put(token, uuid)
webClient.ws.send( webClient.ws.send(
"""{"action": "minecraft_id_result", "success": true, """{"action": "login_result", "success": true,
| "username": "$mcIdUser", "uuid": "$uuid", "token": "$token"}""".trimMargin() | "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") webLogger.info("${webClient.ws.call.request.local.remoteHost} (O: ${webClient.ws.call.request.origin.remoteHost}) generated a token for account $mcIdUser $uuid")
} else { } else {
webClient.ws.send("""{"action": "minecraft_id_result", "success": false}""") 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") webLogger.info("${webClient.ws.call.request.local.remoteHost} (O: ${webClient.ws.call.request.origin.remoteHost}) failed to generated a token for account $username")
} }
} }

View File

@ -219,44 +219,48 @@ function renderActions() {
actions.innerHTML = ""; actions.innerHTML = "";
if (listenVisible) { if (listenVisible) {
if (username != null && mcauth_code != null) { if (username != null && mcauth_code != null) {
let p = document.createElement("p"); addAction("Listen to " + username, () => {
let add = document.createElement("a");
p.appendChild(add);
add.innerText = "Listen to " + username;
add.href = "#";
add.onclick = () => {
socket.send(JSON.stringify({ socket.send(JSON.stringify({
"action": "minecraft_id_login", "action": "minecraft_id_login",
"username": username, "username": username,
"code": mcauth_code})); "code": mcauth_code}));
mcauth_code = null; mcauth_code = null;
}; });
actions.appendChild(p);
} }
let p = document.createElement("p"); addAction("Listen to premium login in VIAaaS instance", () => {
let link = document.createElement("a"); let user = prompt("Premium username (case-sensitive): ", "");
p.appendChild(link);
link.innerText = "Listen to username in VIAaaS instance";
link.href = "#";
link.onclick = () => {
let user = prompt("Username (Minecraft.ID is case-sensitive): ", "");
if (!user) return; if (!user) return;
let callbackUrl = new URL(location.origin + location.pathname + "#username=" + encodeURIComponent(user)); let callbackUrl = new URL(location.origin + location.pathname + "#username=" + encodeURIComponent(user));
location = "https://api.minecraft.id/gateway/start/" + encodeURIComponent(user) + "?callback=" + encodeURIComponent(callbackUrl); location = "https://api.minecraft.id/gateway/start/" + encodeURIComponent(user)
}; + "?callback=" + encodeURIComponent(callbackUrl);
actions.appendChild(p); });
addAction("Listen to offline login in VIAaaS instance", () => {
let user = prompt("Offline username (case-sensitive):", "");
if (!user) return;
socket.send(JSON.stringify({"action": "offline_login", "username": user}));
});
} }
} }
function addAction(text, onClick) {
let p = document.createElement("p");
let link = document.createElement("a");
p.appendChild(link);
link.innerText = text;
link.href = "#";
link.onclick = onClick;
actions.appendChild(p);
}
function onSocketMsg(event) { function onSocketMsg(event) {
console.log(event.data.toString()); console.log(event.data.toString());
let parsed = JSON.parse(event.data); let parsed = JSON.parse(event.data);
if (parsed.action == "ad_minecraft_id_login") { if (parsed.action == "ad_minecraft_id_login") {
listenVisible = true; listenVisible = true;
renderActions(); renderActions();
} else if (parsed.action == "minecraft_id_result") { } else if (parsed.action == "login_result") {
if (!parsed.success) { if (!parsed.success) {
alert("VIAaaS instance couldn't verify account via Minecraft.ID"); alert("VIAaaS instance couldn't verify Minecraft account");
} else { } else {
listen(parsed.token); listen(parsed.token);
saveToken(parsed.token); saveToken(parsed.token);