refactored handler, alternative username, fix online mode encryption

This commit is contained in:
creeper123123321 2020-11-01 16:55:38 -03:00
parent 23c75082a7
commit 7d883ffb2e
7 changed files with 433 additions and 324 deletions

View File

@ -2,11 +2,25 @@ VIAaaS
---
Idea: server.example.com._p25565._v1_12_2._otrue.viaaas.example.com (default backend 25565 port and version default as auto, online-mode can be optional/required) (similar to tor to web proxies)
- TODO: _o option for disabling online mode only in front end, protocol auto detection
TODO: Online mode, protocol auto detection
Usage:
- ./gradlew clean run
- Connection to private IP addresses are currently blocked
- VIAaaS auth page is designed for storing accounts in the browser local storage.
It requires a CORS Proxy for calling Mojang APIs, which may make Mojang see that
as suspicious and reset/block your account password.
- VIAaaS may have security vulnerabilities, make sure to block the ports in firewall and take care of browser local storage.
Usage for offline mode:
- ./gradlew clean run
- Connect to mc.example.com._v1_8.viaaas.localhost
Usage for online mode (may block your Mojang account):
- ./gradlew clean run
- You'll need 2 premium accounts for online mode
- Set up a CORS Proxy (something like https://github.com/Rob--W/cors-anywhere (less likely to look suspicious to Mojang if you run on your local machine) or https://github.com/Zibri/cloudflare-cors-anywhere (more suspicious)).
- Go to https://localhost:25543/auth.html, configure the CORS Proxy URL and listen to the username you're using to connect.
- Log in into Minecraft account with the username you'll use in _u option via browser.
- Connect to mc.example.com._v1_8.viaaas._u(BACKUSERNAME).localhost
- Approve the login

View File

@ -11,10 +11,8 @@ import io.netty.handler.flow.FlowControlHandler
import io.netty.handler.timeout.ReadTimeoutHandler
import us.myles.ViaVersion.api.data.UserConnection
import us.myles.ViaVersion.api.type.Type
import us.myles.ViaVersion.exception.CancelCodecException
import us.myles.ViaVersion.exception.CancelDecoderException
import us.myles.ViaVersion.exception.CancelEncoderException
import us.myles.ViaVersion.util.PipelineUtil
import java.util.concurrent.TimeUnit
import java.util.zip.Deflater
import java.util.zip.Inflater
@ -35,7 +33,7 @@ object ChannelInit : ChannelInitializer<Channel>() {
.addLast("flow-handler", FlowControlHandler())
.addLast("via-encoder", CloudEncodeHandler(user))
.addLast("via-decoder", CloudDecodeHandler(user))
.addLast("handler", CloudSideForwarder(user, null))
.addLast("handler", CloudMinecraftHandler(user, null, frontEnd = true))
}
}
@ -70,7 +68,7 @@ class BackendInit(val user: UserConnection) : ChannelInitializer<Channel>() {
.addLast("frame-decoder", FrameDecoder())
.addLast("compress", CloudCompressor())
.addLast("decompress", CloudDecompressor())
.addLast("handler", CloudSideForwarder(user, null))
.addLast("handler", CloudMinecraftHandler(user, null, frontEnd = false))
}
}

View File

@ -1,39 +1,84 @@
package com.github.creeper123123321.viaaas
import com.google.common.net.UrlEscapers
import com.google.gson.Gson
import com.google.gson.JsonObject
import io.ktor.client.request.*
import io.ktor.http.cio.websocket.*
import io.netty.bootstrap.Bootstrap
import io.netty.buffer.ByteBuf
import io.netty.buffer.ByteBufAllocator
import io.netty.channel.Channel
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelOption
import io.netty.channel.SimpleChannelInboundHandler
import io.netty.channel.socket.SocketChannel
import io.netty.channel.socket.nio.NioSocketChannel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.slf4j.LoggerFactory
import us.myles.ViaVersion.api.Via
import us.myles.ViaVersion.api.data.StoredObject
import us.myles.ViaVersion.api.data.UserConnection
import us.myles.ViaVersion.api.type.Type
import us.myles.ViaVersion.exception.CancelCodecException
import us.myles.ViaVersion.packets.State
import java.net.InetAddress
import java.net.InetSocketAddress
import java.security.KeyFactory
import java.security.PublicKey
import java.security.spec.X509EncodedKeySpec
import java.util.*
import java.util.concurrent.TimeUnit
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import javax.naming.NameNotFoundException
import javax.naming.directory.InitialDirContext
val chLogger = LoggerFactory.getLogger("VIAaaS CloudHandler")
val chLogger = LoggerFactory.getLogger("VIAaaS MC Handler")
class HandlerData(userConnection: UserConnection,
var state: MinecraftConnectionState,
var protocolId: Int? = null,
var frontName: String? = null,
var backName: String? = null,
var backServerId: String? = null,
var backPublicKey: PublicKey? = null,
var backToken: ByteArray? = null,
var frontToken: ByteArray? = null,
var frontId: String? = null
) : StoredObject(userConnection)
class CloudMinecraftHandler(val user: UserConnection,
var other: Channel?,
val frontEnd: Boolean) : SimpleChannelInboundHandler<ByteBuf>() {
val data get() = user.get(HandlerData::class.java)
class CloudSideForwarder(val userConnection: UserConnection, var other: Channel?) : SimpleChannelInboundHandler<ByteBuf>() {
override fun channelRead0(ctx: ChannelHandlerContext, msg: ByteBuf) {
if (!userConnection.isPendingDisconnect) {
other?.write(msg.retain())
if (!user.isPendingDisconnect) {
data!!.state.handleMessage(this, ctx, msg)
if (msg.isReadable) throw IllegalStateException("Remaining bytes!!!")
//other?.write(msg.retain())
}
}
override fun channelActive(ctx: ChannelHandlerContext?) {
if (data == null) {
user.put(HandlerData(user, HandshakeState()))
}
}
override fun channelInactive(ctx: ChannelHandlerContext) {
super.channelInactive(ctx)
chLogger.info(userConnection.channel?.remoteAddress().toString() + " was disconnected")
chLogger.info(ctx.channel().remoteAddress().toString() + " was disconnected")
other?.close()
}
override fun channelReadComplete(ctx: ChannelHandlerContext?) {
super.channelReadComplete(ctx)
other?.flush()
}
override fun channelWritabilityChanged(ctx: ChannelHandlerContext) {
super.channelWritabilityChanged(ctx)
other?.setAutoRead(ctx.channel().isWritable)
}
@ -43,36 +88,333 @@ class CloudSideForwarder(val userConnection: UserConnection, var other: Channel?
cause.printStackTrace()
}
fun disconnect(s: String) {
if (userConnection.channel?.isActive != true) return
if (user.channel?.isActive != true) return
chLogger.info("Disconnecting " + userConnection.channel!!.remoteAddress() + ": " + s)
when (userConnection.protocolInfo!!.state) {
State.LOGIN -> {
val packet = ByteBufAllocator.DEFAULT.buffer()
try {
packet.writeByte(0) // id 0 disconnect
Type.STRING.write(packet, Gson().toJson("[VIAaaS] §c$s"))
userConnection.sendRawPacketFuture(packet.retain()).addListener { userConnection.channel?.close() }
} finally {
packet.release()
chLogger.info("Disconnecting " + user.channel!!.remoteAddress() + ": " + s)
data!!.state.disconnect(this, s)
}
}
interface MinecraftConnectionState {
fun handleMessage(handler: CloudMinecraftHandler, ctx: ChannelHandlerContext,
msg: ByteBuf)
fun disconnect(handler: CloudMinecraftHandler, msg: String)
}
class HandshakeState : MinecraftConnectionState {
override fun handleMessage(handler: CloudMinecraftHandler, ctx: ChannelHandlerContext, msg: ByteBuf) {
if (!handler.frontEnd || Type.VAR_INT.readPrimitive(msg) != 0) throw IllegalArgumentException("Invalid packet ID!")
handler.data!!.protocolId = Type.VAR_INT.readPrimitive(msg)
val backAddr = Type.STRING.read(msg)
val backPort = Type.UNSIGNED_SHORT.read(msg)
val nextAddr = Type.VAR_INT.readPrimitive(msg)
when (nextAddr) {
1 -> handler.data!!.state = StatusState()
2 -> handler.data!!.state = LoginState()
else -> throw IllegalStateException("Invalid next state")
}
handler.user.channel!!.setAutoRead(false)
Via.getPlatform().runAsync {
val frontForwarder = handler.user.channel!!.pipeline().get(CloudMinecraftHandler::class.java)
try {
var srvResolvedAddr = backAddr
var srvResolvedPort = backPort
if (srvResolvedPort == 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.$backAddr", arrayOf("SRV"))["SRV"]
if (attr != null && attr.size() > 0) {
val record = (attr.get(0) as String).split(" ")
srvResolvedAddr = record[3]
srvResolvedPort = record[2].toInt()
}
} catch (ignored: NameNotFoundException) {
}
}
}
State.STATUS -> {
val packet = ByteBufAllocator.DEFAULT.buffer()
try {
packet.writeByte(0) // id 0 disconnect
Type.STRING.write(packet, """{"version": {"name": "VIAaaS","protocol": -1},
"players": {"max": 0,"online": 0,"sample": []},"description": {"text": ${Gson().toJson("§c$s")}}}""")
userConnection.sendRawPacketFuture(packet.retain()).addListener { userConnection.channel?.close() }
} finally {
packet.release()
val socketAddr = InetSocketAddress(InetAddress.getByName(srvResolvedAddr), srvResolvedPort)
val addrInfo = socketAddr.address
if (addrInfo.isSiteLocalAddress
|| addrInfo.isLoopbackAddress
|| addrInfo.isLinkLocalAddress
|| addrInfo.isAnyLocalAddress) throw SecurityException("Local addresses aren't allowed")
val bootstrap = Bootstrap().handler(BackendInit(handler.user))
.channel(NioSocketChannel::class.java)
.group(handler.user.channel!!.eventLoop())
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 15_000) // Half of mc timeout
.connect(socketAddr)
bootstrap.addListener {
if (it.isSuccess) {
CloudHeadProtocol.logger.info("conected ${handler.user.channel?.remoteAddress()} to $socketAddr")
val sockChan = bootstrap.channel() as SocketChannel
sockChan.pipeline().get(CloudMinecraftHandler::class.java).other = handler.user.channel
frontForwarder.other = sockChan
val backHandshake = ByteBufAllocator.DEFAULT.buffer()
try {
backHandshake.writeByte(0) // Packet 0 handshake
Type.VAR_INT.writePrimitive(backHandshake, handler.data!!.protocolId!!)
Type.STRING.write(backHandshake, srvResolvedAddr) // Server Address
backHandshake.writeShort(srvResolvedPort)
Type.VAR_INT.writePrimitive(backHandshake, nextAddr)
sockChan.writeAndFlush(backHandshake.retain())
} finally {
backHandshake.release()
}
handler.user.channel!!.setAutoRead(true)
} else {
handler.user.channel!!.eventLoop().submit {
frontForwarder.disconnect("Couldn't connect: " + it.cause().toString())
}
}
}
} catch (e: Exception) {
handler.user.channel!!.eventLoop().submit {
frontForwarder.disconnect("Couldn't connect: $e")
}
}
else -> {
userConnection.disconnect(s)
}
}
}
override fun disconnect(handler: CloudMinecraftHandler, msg: String) {
handler.user.disconnect(msg)
}
}
class LoginState : MinecraftConnectionState {
override fun handleMessage(handler: CloudMinecraftHandler, ctx: ChannelHandlerContext, msg: ByteBuf) {
msg.markReaderIndex()
val id = Type.VAR_INT.readPrimitive(msg)
when {
handler.frontEnd && id == 0 -> handleLoginStart(handler, msg)
handler.frontEnd && id == 1 -> handleCryptoResponse(handler, msg)
handler.frontEnd && id == 2 -> forward(handler, msg) // Plugin response
!handler.frontEnd && id == 0 -> forward(handler, msg) // Disconnect
!handler.frontEnd && id == 1 -> handleCryptoRequest(handler, msg)
!handler.frontEnd && id == 2 -> handleLoginSuccess(handler, msg)
!handler.frontEnd && id == 3 -> handleCompression(handler, msg)
!handler.frontEnd && id == 4 -> forward(handler, msg) // Plugin request
else -> throw IllegalArgumentException("Invalid packet ID")
}
}
private fun forward(handler: CloudMinecraftHandler, msg: ByteBuf) {
msg.resetReaderIndex()
handler.other!!.write(msg.retain())
}
private fun handleLoginSuccess(handler: CloudMinecraftHandler, msg: ByteBuf) {
handler.data!!.state = PlayState()
forward(handler, msg)
}
private fun handleCompression(handler: CloudMinecraftHandler, msg: ByteBuf) {
val pipe = handler.user.channel!!.pipeline()
val threshold = Type.VAR_INT.readPrimitive(msg)
val backPipe = pipe.get(CloudMinecraftHandler::class.java).other!!.pipeline()
backPipe.get(CloudCompressor::class.java)?.threshold = threshold
backPipe.get(CloudDecompressor::class.java)?.threshold = threshold
forward(handler, msg)
pipe.get(CloudCompressor::class.java).threshold = threshold
pipe.get(CloudDecompressor::class.java).threshold = threshold
}
fun handleCryptoRequest(handler: CloudMinecraftHandler, msg: ByteBuf) {
val data = handler.data!!
data.backServerId = Type.STRING.read(msg)
data.backPublicKey = KeyFactory.getInstance("RSA")
.generatePublic(X509EncodedKeySpec(Type.BYTE_ARRAY_PRIMITIVE.read(msg)))
data.backToken = Type.BYTE_ARRAY_PRIMITIVE.read(msg)
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
}
data.frontToken = token
data.frontId = id
val backMsg = ByteBufAllocator.DEFAULT.buffer()
try {
backMsg.writeByte(1) // Packet id
Type.STRING.write(backMsg, id)
Type.BYTE_ARRAY_PRIMITIVE.write(backMsg, mcCryptoKey.public.encoded)
Type.BYTE_ARRAY_PRIMITIVE.write(backMsg, token)
handler.other!!.write(backMsg.retain())
} finally {
backMsg.release()
}
}
fun handleCryptoResponse(handler: CloudMinecraftHandler, msg: ByteBuf) {
val frontHash = let {
val frontKey = Cipher.getInstance("RSA").let {
it.init(Cipher.DECRYPT_MODE, mcCryptoKey.private)
it.doFinal(Type.BYTE_ARRAY_PRIMITIVE.read(msg))
}
// RSA token - wat??? why is it encrypted with RSA if it was sent unencrypted?
val decryptedToken = Cipher.getInstance("RSA").let {
it.init(Cipher.DECRYPT_MODE, mcCryptoKey.private)
it.doFinal(Type.BYTE_ARRAY_PRIMITIVE.read(msg))
}
if (!decryptedToken.contentEquals(handler.data!!.frontToken!!)) throw IllegalStateException("invalid token!")
val spec = SecretKeySpec(frontKey, "AES")
val iv = IvParameterSpec(frontKey)
val aesEn = Cipher.getInstance("AES/CFB8/NoPadding").let {
it.init(Cipher.ENCRYPT_MODE, spec, iv)
it
}
val aesDe = Cipher.getInstance("AES/CFB8/NoPadding").let {
it.init(Cipher.DECRYPT_MODE, spec, iv)
it
}
handler.user.channel!!.pipeline().get(CloudEncryptor::class.java).cipher = aesEn
handler.user.channel!!.pipeline().get(CloudDecryptor::class.java).cipher = aesDe
generateServerHash(handler.data!!.frontId!!, frontKey, mcCryptoKey.public)
}
val backKey = ByteArray(16).let {
secureRandom.nextBytes(it)
it
}
val backSpec = SecretKeySpec(backKey, "AES")
val backIv = IvParameterSpec(backKey)
val backAesEn = Cipher.getInstance("AES/CFB8/NoPadding").let {
it.init(Cipher.ENCRYPT_MODE, backSpec, backIv)
it
}
val backAesDe = Cipher.getInstance("AES/CFB8/NoPadding").let {
it.init(Cipher.DECRYPT_MODE, backSpec, backIv)
it
}
val backHash = generateServerHash(handler.data!!.backServerId!!, backKey, handler.data!!.backPublicKey!!)
handler.user.channel!!.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!!.backName!!)}&serverId=$frontHash")
?: throw IllegalArgumentException("Couldn't authenticate with session servers")
var sent = false
viaWebServer.listeners[fromUndashed(profile.get("id")!!.asString)]?.forEach {
it.ws.send("""{"action": "session_hash_request", "user": "${handler.data!!.backName!!}", "session_hash": "$backHash",
| "client_address": "${handler.user.channel!!.remoteAddress()}", "backend_public_key":
| "${Base64.getEncoder().encodeToString(handler.data!!.backPublicKey!!.encoded)}"}""".trimMargin())
it.ws.flush()
sent = true
}
if (!sent) {
throw IllegalStateException("No connection to browser, connect in /auth.html")
} else {
viaWebServer.pendingSessionHashes.get(backHash).get(15, TimeUnit.SECONDS)
val backChan = handler.other!!
backChan.eventLoop().submit {
val backMsg = ByteBufAllocator.DEFAULT.buffer()
try {
backMsg.writeByte(1) // Packet id
Type.BYTE_ARRAY_PRIMITIVE.write(backMsg, Cipher.getInstance("RSA").let {
it.init(Cipher.ENCRYPT_MODE, handler.data!!.backPublicKey)
it.doFinal(backKey)
})
Type.BYTE_ARRAY_PRIMITIVE.write(backMsg, Cipher.getInstance("RSA").let {
it.init(Cipher.ENCRYPT_MODE, handler.data!!.backPublicKey)
it.doFinal(handler.data!!.backToken)
})
backChan.writeAndFlush(backMsg.retain())
backChan.pipeline().get(CloudEncryptor::class.java).cipher = backAesEn
backChan.pipeline().get(CloudDecryptor::class.java).cipher = backAesDe
} finally {
backMsg.release()
}
}
}
} catch (e: Exception) {
handler.disconnect("Online mode error: $e")
}
handler.user.channel!!.setAutoRead(true)
}
}
fun handleLoginStart(handler: CloudMinecraftHandler, msg: ByteBuf) {
handler.data!!.frontName = Type.STRING.read(msg)
handler.data!!.backName = handler.user.get(CloudData::class.java)!!.altName ?: handler.data!!.frontName
val backMsg = ByteBufAllocator.DEFAULT.buffer()
try {
msg.writeByte(0) // Id
Type.STRING.write(msg, handler.data!!.backName)
handler.other!!.write(msg.retain())
} finally {
backMsg.release()
}
}
override fun disconnect(handler: CloudMinecraftHandler, msg: String) {
val packet = ByteBufAllocator.DEFAULT.buffer()
try {
handler.user.isPendingDisconnect = true
packet.writeByte(0) // id 0 disconnect
Type.STRING.write(packet, Gson().toJson("[VIAaaS] §c$msg"))
handler.user
.sendRawPacketFuture(packet.retain())
.addListener { handler.user.channel?.close() }
} finally {
packet.release()
}
}
}
class StatusState : MinecraftConnectionState {
override fun handleMessage(handler: CloudMinecraftHandler, ctx: ChannelHandlerContext, msg: ByteBuf) {
handler.other!!.write(msg.retain())
}
override fun disconnect(handler: CloudMinecraftHandler, msg: String) {
handler.user.isPendingDisconnect = true
val packet = ByteBufAllocator.DEFAULT.buffer()
try {
packet.writeByte(0) // id 0 disconnect
Type.STRING.write(packet, """{"version": {"name": "VIAaaS", "protocol": -1}, "players":
| {"max": 0, "online": 0, "sample": []}, "description": {"text": ${Gson().toJson("§c$msg")}}}""".trimMargin())
handler.user.sendRawPacketFuture(packet.retain())
.addListener { handler.user.channel?.close() }
} finally {
packet.release()
}
}
}
class PlayState : MinecraftConnectionState {
override fun handleMessage(handler: CloudMinecraftHandler, ctx: ChannelHandlerContext, msg: ByteBuf) {
handler.other!!.write(msg.retain())
}
override fun disconnect(handler: CloudMinecraftHandler, msg: String) {
handler.user.disconnect(msg)
}
}

View File

@ -1,43 +1,20 @@
package com.github.creeper123123321.viaaas
import com.google.common.net.UrlEscapers
import com.google.gson.JsonObject
import io.ktor.client.request.*
import io.ktor.http.cio.websocket.*
import io.netty.bootstrap.Bootstrap
import io.netty.buffer.ByteBufAllocator
import io.netty.channel.Channel
import io.netty.channel.ChannelOption
import io.netty.channel.socket.SocketChannel
import io.netty.channel.socket.nio.NioSocketChannel
import kotlinx.coroutines.runBlocking
import org.slf4j.LoggerFactory
import us.myles.ViaVersion.api.PacketWrapper
import us.myles.ViaVersion.api.Via
import us.myles.ViaVersion.api.data.StoredObject
import us.myles.ViaVersion.api.data.UserConnection
import us.myles.ViaVersion.api.protocol.Protocol
import us.myles.ViaVersion.api.protocol.ProtocolPipeline
import us.myles.ViaVersion.api.protocol.ProtocolRegistry
import us.myles.ViaVersion.api.protocol.SimpleProtocol
import us.myles.ViaVersion.api.remapper.PacketRemapper
import us.myles.ViaVersion.api.type.Type
import us.myles.ViaVersion.packets.State
import java.math.BigInteger
import java.net.InetAddress
import java.net.InetSocketAddress
import java.security.KeyFactory
import java.security.MessageDigest
import java.security.PublicKey
import java.security.SecureRandom
import java.security.spec.X509EncodedKeySpec
import java.util.*
import java.util.concurrent.TimeUnit
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import javax.naming.NameNotFoundException
import javax.naming.directory.InitialDirContext
class CloudPipeline(userConnection: UserConnection) : ProtocolPipeline(userConnection) {
override fun registerPackets() {
@ -49,8 +26,6 @@ class CloudPipeline(userConnection: UserConnection) : ProtocolPipeline(userConne
super.add(protocol)
pipes().removeIf { it == CloudHeadProtocol }
pipes().add(0, CloudHeadProtocol)
pipes().removeIf { it == CloudTailProtocol }
pipes().add(CloudTailProtocol)
}
}
@ -61,250 +36,27 @@ object CloudHeadProtocol : SimpleProtocol() {
override fun registerMap() {
handler { wrapper: PacketWrapper ->
val playerVer = wrapper.passthrough(Type.VAR_INT)
val addr = wrapper.passthrough(Type.STRING) // Server Address
val receivedPort = wrapper.passthrough(Type.UNSIGNED_SHORT)
val nextState = wrapper.passthrough(Type.VAR_INT)
val addr = wrapper.read(Type.STRING) // Server Address
val receivedPort = wrapper.read(Type.UNSIGNED_SHORT)
val parsed = VIAaaSAddress().parse(addr, VIAaaSConfig.hostName)
if (parsed.port == 0) {
parsed.port = receivedPort
}
val backPort = parsed.port ?: receivedPort
val backAddr = parsed.realAddress
val backProto = parsed.protocol ?: 47
logger.info("connecting ${wrapper.user().channel!!.remoteAddress()} ($playerVer) to ${parsed.realAddress}:${parsed.port} (${parsed.protocol})")
wrapper.write(Type.STRING, backAddr)
wrapper.write(Type.UNSIGNED_SHORT, backPort)
val playerAddr = wrapper.user().channel!!.remoteAddress()
logger.info("connecting $playerAddr ($playerVer) -> $backAddr:$backPort ($backProto)")
wrapper.user().channel!!.setAutoRead(false)
wrapper.user().put(CloudData(
backendVer = parsed.protocol,
userConnection = wrapper.user(),
frontOnline = parsed.online
))
backendVer = backProto,
frontOnline = parsed.online,
altName = parsed.altUsername))
Via.getPlatform().runAsync {
val frontForwarder = wrapper.user().channel!!.pipeline().get(CloudSideForwarder::class.java)
try {
var srvResolvedAddr = parsed.realAddress
var srvResolvedPort = parsed.port
if (srvResolvedPort == 25565) {
try {
// https://github.com/GeyserMC/Geyser/blob/99e72f35b308542cf0dbfb5b58816503c3d6a129/connector/src/main/java/org/geysermc/connector/GeyserConnector.java
val ctx = InitialDirContext()
val attr = ctx.getAttributes("dns:///_minecraft._tcp.${parsed.realAddress}", arrayOf("SRV"))["SRV"]
if (attr != null && attr.size() > 0) {
val record = (attr.get(0) as String).split(" ").toTypedArray()
srvResolvedAddr = record[3]
srvResolvedPort = record[2].toInt()
}
} catch (ignored: NameNotFoundException) {
}
}
val socketAddr = InetSocketAddress(InetAddress.getByName(srvResolvedAddr), srvResolvedPort)
val addrInfo = socketAddr.address
if (addrInfo.isSiteLocalAddress
|| addrInfo.isLoopbackAddress
|| addrInfo.isLinkLocalAddress
|| addrInfo.isAnyLocalAddress) throw SecurityException("Local addresses aren't allowed")
val bootstrap = Bootstrap().handler(BackendInit(wrapper.user()))
.channel(NioSocketChannel::class.java)
.group(wrapper.user().channel!!.eventLoop())
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 15_000) // Half of mc timeout
.connect(socketAddr)
bootstrap.addListener {
if (it.isSuccess) {
logger.info("conected ${wrapper.user().channel?.remoteAddress()} to $socketAddr")
val chann = bootstrap.channel() as SocketChannel
chann.pipeline().get(CloudSideForwarder::class.java).other = wrapper.user().channel
frontForwarder.other = chann
val backHandshake = ByteBufAllocator.DEFAULT.buffer()
try {
val nullParts = addr.split(0.toChar())
backHandshake.writeByte(0) // Packet 0 handshake
val connProto = if (ProtocolRegistry.getProtocolPath(playerVer, parsed.protocol) != null) parsed.protocol else playerVer
Type.VAR_INT.writePrimitive(backHandshake, connProto)
Type.STRING.write(backHandshake, srvResolvedAddr + (if (nullParts.size == 2) 0.toChar() + nullParts[1] else "")) // Server Address
backHandshake.writeShort(srvResolvedPort)
Type.VAR_INT.writePrimitive(backHandshake, nextState)
chann.writeAndFlush(backHandshake.retain())
} finally {
backHandshake.release()
}
wrapper.user().channel!!.setAutoRead(true)
} else {
wrapper.user().channel!!.eventLoop().submit {
frontForwarder.disconnect("Couldn't connect: " + it.cause().toString())
}
}
}
} catch (e: Exception) {
wrapper.user().channel!!.eventLoop().submit {
frontForwarder.disconnect("Couldn't connect: $e")
}
}
}
}
}
})
}
}
object CloudTailProtocol : SimpleProtocol() {
override fun registerPackets() {
// Login start
this.registerIncoming(State.LOGIN, 0, 0, object : PacketRemapper() {
override fun registerMap() {
handler {
it.user().get(CloudData::class.java)!!.frontLoginName = it.passthrough(Type.STRING)
}
}
})
this.registerOutgoing(State.LOGIN, 3, 3, object : PacketRemapper() {
// set compression
override fun registerMap() {
handler {
val pipe = it.user().channel!!.pipeline()
val threshold = it.read(Type.VAR_INT)
it.cancel()
it.create(3) {
it.write(Type.VAR_INT, threshold)
}.send(CloudTailProtocol::class.java, true, true) // needs to be sent uncompressed
pipe.get(CloudCompressor::class.java).threshold = threshold
pipe.get(CloudDecompressor::class.java).threshold = threshold
val backPipe = pipe.get(CloudSideForwarder::class.java).other!!.pipeline()
backPipe.get(CloudCompressor::class.java)?.threshold = threshold
backPipe.get(CloudDecompressor::class.java)?.threshold = threshold
}
}
})
// Crypto request
this.registerOutgoing(State.LOGIN, 1, 1, object : PacketRemapper() {
override fun registerMap() {
map(Type.STRING) // Server id - unused
map(Type.BYTE_ARRAY_PRIMITIVE) // Public key
map(Type.BYTE_ARRAY_PRIMITIVE) // Token
handler {
val data = it.user().get(CloudData::class.java)!!
data.backServerId = it.get(Type.STRING, 0)
data.backPublicKey = KeyFactory.getInstance("RSA")
.generatePublic(X509EncodedKeySpec(it.get(Type.BYTE_ARRAY_PRIMITIVE, 0)))
data.backToken = it.get(Type.BYTE_ARRAY_PRIMITIVE, 1)
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
it.set(Type.STRING, 0, id)
it.set(Type.BYTE_ARRAY_PRIMITIVE, 0, mcCryptoKey.public.encoded)
val token = ByteArray(16)
secureRandom.nextBytes(token)
data.frontToken = token
data.frontId = id
it.set(Type.BYTE_ARRAY_PRIMITIVE, 1, token.clone())
}
}
})
this.registerIncoming(State.LOGIN, 1, 1, object : PacketRemapper() {
override fun registerMap() {
map(Type.BYTE_ARRAY_PRIMITIVE) // RSA shared secret
map(Type.BYTE_ARRAY_PRIMITIVE) // RSA token - wat??? why is it encrypted with RSA if it was sent unencrypted?
handler { wrapper ->
val data = wrapper.user().get(CloudData::class.java)!!
val encryptedSecret = wrapper.get(Type.BYTE_ARRAY_PRIMITIVE, 0)
val secret = Cipher.getInstance("RSA").let {
it.init(Cipher.DECRYPT_MODE, mcCryptoKey.private)
it.doFinal(encryptedSecret)
}
val encryptedToken = wrapper.get(Type.BYTE_ARRAY_PRIMITIVE, 1)
val decryptedToken = Cipher.getInstance("RSA").let {
it.init(Cipher.DECRYPT_MODE, mcCryptoKey.private)
it.doFinal(encryptedToken)
}!!
if (!decryptedToken.contentEquals(data.frontToken!!)) throw IllegalStateException("invalid token!")
val spec = SecretKeySpec(secret, "AES")
val iv = IvParameterSpec(secret)
val aesEn = Cipher.getInstance("AES/CFB8/NoPadding")
val aesDe = Cipher.getInstance("AES/CFB8/NoPadding")
aesEn.init(Cipher.ENCRYPT_MODE, spec, iv)
aesDe.init(Cipher.DECRYPT_MODE, spec, iv)
wrapper.user().channel!!.pipeline().get(CloudEncryptor::class.java).cipher = aesEn
wrapper.user().channel!!.pipeline().get(CloudDecryptor::class.java).cipher = aesDe
val frontHash = generateServerHash(data.frontId!!, secret, mcCryptoKey.public)
val backKey = ByteArray(16)
secureRandom.nextBytes(backKey)
val backSpec = SecretKeySpec(secret, "AES")
val backIv = IvParameterSpec(secret)
val backAesEn = Cipher.getInstance("AES/CFB8/NoPadding")
val backAesDe = Cipher.getInstance("AES/CFB8/NoPadding")
backAesEn.init(Cipher.ENCRYPT_MODE, backSpec, backIv)
backAesDe.init(Cipher.DECRYPT_MODE, backSpec, backIv)
val backHash = generateServerHash(data.backServerId!!, backKey, data.backPublicKey!!)
wrapper.cancel()
Via.getPlatform().runAsync {
// Don't need to disable autoread, server will wait us
runBlocking {
try {
val profile = httpClient.get<JsonObject?>("https://sessionserver.mojang.com/session/minecraft/hasJoined?username=" +
"${UrlEscapers.urlFormParameterEscaper().escape(data.frontLoginName!!)}&serverId=$frontHash")
?: throw IllegalArgumentException("Couldn't authenticate with session servers")
var sent = false
viaWebServer.listeners[fromUndashed(profile.get("id")!!.asString)]?.forEach {
it.ws.send("""{"action": "session_hash_request", "username": "${data.frontLoginName!!}", "session_hash": "$backHash",
| "client_address": "${wrapper.user().channel!!.remoteAddress()}", "backend_public_key":
| "${Base64.getEncoder().encodeToString(data.backPublicKey!!.encoded)}"}""".trimMargin())
it.ws.flush()
sent = true
}
if (!sent) {
throw IllegalStateException("No connection to browser, connect in /auth.html")
} else {
viaWebServer.pendingSessionHashes.get(backHash).get(15, TimeUnit.SECONDS)
val backChan = wrapper.user().channel!!.pipeline()
.get(CloudSideForwarder::class.java).other!!
backChan.eventLoop().submit {
val backCryptoAnswer = ByteBufAllocator.DEFAULT.buffer()
try {
backCryptoAnswer.writeByte(1) // Packet id
Type.BYTE_ARRAY_PRIMITIVE.write(backCryptoAnswer, Cipher.getInstance("RSA").let {
it.init(Cipher.ENCRYPT_MODE, data.backPublicKey)
it.doFinal(backKey)
})
Type.BYTE_ARRAY_PRIMITIVE.write(backCryptoAnswer, Cipher.getInstance("RSA").let {
it.init(Cipher.ENCRYPT_MODE, data.backPublicKey)
it.doFinal(data.backToken)
})
backChan.writeAndFlush(backCryptoAnswer.retain())
backChan.pipeline().get(CloudEncryptor::class.java).cipher = backAesEn
backChan.pipeline().get(CloudDecryptor::class.java).cipher = backAesDe
} finally {
backCryptoAnswer.release()
}
}
}
} catch (e: Exception) {
wrapper.user().channel!!.pipeline()
.get(CloudSideForwarder::class.java)
.disconnect("Online mode error: $e")
}
}
}
wrapper.passthrough(Type.VAR_INT) // Next state
}
}
})
@ -334,11 +86,5 @@ fun generateServerHash(serverId: String, sharedSecret: ByteArray?, key: PublicKe
data class CloudData(val userConnection: UserConnection,
var backendVer: Int,
var frontOnline: Boolean,
var pendingStatus: Boolean = false,
var backServerId: String? = null,
var backPublicKey: PublicKey? = null,
var backToken: ByteArray? = null,
var frontToken: ByteArray? = null,
var frontLoginName: String? = null,
var frontId: String? = null
var altName: String?
) : StoredObject(userConnection)

View File

@ -23,6 +23,7 @@ import us.myles.ViaVersion.api.data.MappingDataLoader
import us.myles.ViaVersion.api.protocol.ProtocolVersion
import us.myles.ViaVersion.util.Config
import java.io.File
import java.lang.IllegalArgumentException
import java.net.InetAddress
import java.security.KeyPairGenerator
import java.util.*
@ -165,11 +166,12 @@ object VIAaaSConfig : Config(File("config/viaaas.yml")) {
}
class VIAaaSAddress {
var protocol = 0
var protocol: Int? = null
var viaSuffix: String? = null
var realAddress: String? = null
var port = 0
var online = false
var port: Int? = null
var online = true
var altUsername : String? = null
fun parse(address: String, viaHostName: String): VIAaaSAddress {
val parts = address.split('.')
var foundDomain = false
@ -196,6 +198,10 @@ class VIAaaSAddress {
}
}
}
part.startsWith("_u", ignoreCase = true) -> {
if (arg.length > 16) throw IllegalArgumentException("Invalid alt username")
altUsername = arg
}
}
} else {
foundOptions = true

View File

@ -14,8 +14,10 @@ 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 java.net.URLEncoder
@ -81,7 +83,7 @@ fun fromUndashed(string: String): UUID {
class WebDashboardServer {
val clients = ConcurrentHashMap<WebSocketSession, WebClient>()
val loginTokens = CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.DAYS)
.expireAfterAccess(10, TimeUnit.DAYS)
.build<UUID, UUID>()
// Minecraft account -> WebClient
@ -90,8 +92,10 @@ class WebDashboardServer {
.expireAfterWrite(1, TimeUnit.HOURS)
.build<String, UUID>(CacheLoader.from { name ->
runBlocking {
httpClient.get<JsonObject?>("https://api.mojang.com/users/profiles/minecraft/$name")
?.get("id")?.asString?.let { fromUndashed(it) }
withContext(Dispatchers.IO) {
httpClient.get<JsonObject?>("https://api.mojang.com/users/profiles/minecraft/$name")
?.get("id")?.asString?.let { fromUndashed(it) }
}
}
})
@ -175,7 +179,7 @@ class WebLogin : WebState {
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, "username": "$user"}""")
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)
@ -183,7 +187,7 @@ class WebLogin : WebState {
webLogger.info("Listening for logins for $user")
} else {
webClient.ws.send("""{"action": "listen_login_requests_result", "token": "$token", "success": false}""")
webLogger.info("Failed token for $user")
webLogger.info("Failed token")
}
}
"session_hash_response" -> {

View File

@ -9,7 +9,6 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/uuid/8.3.1/uuid.min.js"></script>
</head>
<body>
TODO - what to do with invalid session error because it uses the same account?
<p>DO NOT TYPE YOUR CREDENTIALS IF YOU DON'T TRUST THIS VIAAAS INSTANCE OR THE CORS PROXY!</p>
<p>Mojang API calls in browser are called through a CORS Proxy. See https://github.com/Zibri/cloudflare-cors-anywhere
for setting up one.</p>
@ -250,14 +249,14 @@ TODO - what to do with invalid session error because it uses the same account?
} else if (parsed.action == "listen_login_requests_result") {
if (parsed.success) {
let msg = document.createElement("p");
msg.innerText = "Listening to logins with username: " + parsed.username;
msg.innerText = "Listening to login: " + parsed.user;
content.appendChild(msg);
} else {
removeToken(parsed.token);
}
} else if (parsed.action == "session_hash_request") {
if (confirm("auth request sent from server, info: " + event.data + ". Should we authenticate?")) {
let accounts = getMcAccounts().filter(it => it.user.toLowerCase() == parsed.username.toLowerCase());
let accounts = getMcAccounts().filter(it => it.user.toLowerCase() == parsed.user.toLowerCase());
accounts.forEach(it => {
$.ajax({type: "post",
url: localStorage.getItem("cors-proxy") + "https://sessionserver.mojang.com/session/minecraft/join",
@ -275,7 +274,7 @@ TODO - what to do with invalid session error because it uses the same account?
alert("Failed to authenticate to Minecraft backend server!");
});
});
if (accounts.length == 0) alert("Couldn't find " + parsed.username + " account in browser");
if (accounts.length == 0) alert("Couldn't find " + parsed.user + " account in browser");
}
}
};