mirror of
https://github.com/ViaVersion/VIAaaS.git
synced 2024-11-21 11:55:15 +01:00
refactored handler, alternative username, fix online mode encryption
This commit is contained in:
parent
23c75082a7
commit
7d883ffb2e
24
README.md
24
README.md
@ -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
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
@ -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
|
||||
|
@ -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" -> {
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user