close #34 close #33 close #31, add option to allow local addresses and an option to require hostname

This commit is contained in:
creeper123123321 2020-12-24 16:20:22 -03:00
parent 90fe7e80ce
commit 2bdc547b66
9 changed files with 530 additions and 205 deletions

View File

@ -1,7 +1,7 @@
VIAaaS
---
Idea: server.example.com._p25565._v1_12_2._otrue._uBACKUSERNAME.viaaas.example.com (default backend 25565 port and version
default as auto, online-mode can be optional/required) (similar to tor to web proxies)
How to use: server.example.com._p25565._v1_12_2._uBACKUSERNAME.viaaas.example.com (similar to tor to web proxies)
Default WS URL: wss://localhost:25543/ws
- TODO: _o option for disabling online mode only in front end, protocol auto detection
@ -28,9 +28,9 @@ Usage for online mode (may block your Mojang account):
https://www.curseforge.com/minecraft/mc-mods/auth-me for reauthenticate the client.)
- You should set up a CORS Proxy (something like https://github.com/Rob--W/cors-anywhere) on local machine.
- Go to https://localhost:25543/auth.html, configure the CORS Proxy URL (something like http://localhost:8080/) and listen to
the username you're using to connect.
- Add web page the account you used in _u parameter.
- Connect to mc.example.com._v1_8.viaaas._u(BACKUSERNAME).localhost
the username A that you're using to connect to the proxy.
- Add web page the account B you used in _u parameter.
- Connect to mc.example.com._v1_8.viaaas._u(account B).localhost
- Approve the login
- There are some information about Mojang password resetting: https://github.com/GeyserMC/Geyser/wiki/Common-Issues#mojang-resetting-account-credentials and https://mobile.twitter.com/MojangSupport/status/863697596350517248

View File

@ -1,7 +1,6 @@
package com.github.creeper123123321.viaaas
import io.netty.buffer.ByteBuf
import io.netty.buffer.Unpooled
import io.netty.channel.Channel
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelInitializer
@ -25,12 +24,12 @@ object ChannelInit : ChannelInitializer<Channel>() {
val user = UserConnection(ch)
CloudPipeline(user)
ch.pipeline().addLast("timeout", ReadTimeoutHandler(30, TimeUnit.SECONDS))
// "crypto"
.addLast("frame", FrameCodec())
// "compress" / dummy "decompress"
.addLast("flow-handler", FlowControlHandler())
.addLast("via-codec", CloudViaCodec(user))
.addLast("handler", CloudMinecraftHandler(user, null, frontEnd = true))
// "crypto"
.addLast("frame", FrameCodec())
// "compress" / dummy "decompress"
.addLast("flow-handler", FlowControlHandler())
.addLast("via-codec", CloudViaCodec(user))
.addLast("handler", CloudMinecraftHandler(user, null, frontEnd = true))
}
}
@ -53,22 +52,23 @@ class CloudCrypto(val cipherDecode: Cipher, var cipherEncode: Cipher) : MessageT
class BackendInit(val user: UserConnection) : ChannelInitializer<Channel>() {
override fun initChannel(ch: Channel) {
ch.pipeline().addLast("timeout", ReadTimeoutHandler(30, TimeUnit.SECONDS))
// "crypto"
.addLast("frame", FrameCodec())
// compress
.addLast("handler", CloudMinecraftHandler(user, null, frontEnd = false))
// "crypto"
.addLast("frame", FrameCodec())
// compress
.addLast("handler", CloudMinecraftHandler(user, null, frontEnd = false))
}
}
class CloudCompressionCodec(val threshold: Int) : MessageToMessageCodec<ByteBuf, ByteBuf>() {
// https://github.com/Gerrygames/ClientViaVersion/blob/master/src/main/java/de/gerrygames/the5zig/clientviaversion/netty/CompressionEncoder.java
private val inflater: Inflater = Inflater()// https://github.com/Gerrygames/ClientViaVersion/blob/master/src/main/java/de/gerrygames/the5zig/clientviaversion/netty/CompressionEncoder.java
private val inflater: Inflater =
Inflater()// https://github.com/Gerrygames/ClientViaVersion/blob/master/src/main/java/de/gerrygames/the5zig/clientviaversion/netty/CompressionEncoder.java
private val deflater: Deflater = Deflater()
@Throws(Exception::class)
override fun encode(ctx: ChannelHandlerContext, input: ByteBuf, out: MutableList<Any>) {
val frameLength = input.readableBytes()
val outBuf = ctx.alloc().heapBuffer()
val outBuf = ctx.alloc().buffer()
try {
if (frameLength < threshold) {
outBuf.writeByte(0)
@ -77,15 +77,12 @@ class CloudCompressionCodec(val threshold: Int) : MessageToMessageCodec<ByteBuf,
return
}
Type.VAR_INT.writePrimitive(outBuf, frameLength)
val inBytes = ByteArray(frameLength)
input.readBytes(inBytes)
deflater.setInput(inBytes, 0, frameLength)
deflater.setInput(input.nioBuffer())
deflater.finish()
while (!deflater.finished()) {
outBuf.ensureWritable(8192)
val wIndex = outBuf.writerIndex()
outBuf.writerIndex(wIndex + deflater.deflate(outBuf.array(),
outBuf.arrayOffset() + wIndex, outBuf.writableBytes()))
outBuf.writerIndex(wIndex + deflater.deflate(outBuf.nioBuffer(wIndex, outBuf.writableBytes())))
}
out.add(outBuf.retain())
} finally {
@ -110,13 +107,16 @@ class CloudCompressionCodec(val threshold: Int) : MessageToMessageCodec<ByteBuf,
throw DecoderException("Badly compressed packet - size of $outLength is larger than protocol maximum of 2097152")
}
val temp = ByteArray(input.readableBytes())
input.readBytes(temp)
inflater.setInput(temp)
val output = ByteArray(outLength)
inflater.inflate(output)
out.add(Unpooled.wrappedBuffer(output))
inflater.reset()
inflater.setInput(input.nioBuffer())
val output = ctx.alloc().buffer(outLength, outLength)
try {
output.writerIndex(output.writerIndex() + inflater.inflate(
output.nioBuffer(output.writerIndex(), output.writableBytes())))
out.add(output.retain())
} finally {
inflater.reset()
output.release()
}
}
}

View File

@ -19,15 +19,17 @@ import kotlinx.coroutines.launch
import org.slf4j.LoggerFactory
import us.myles.ViaVersion.api.data.StoredObject
import us.myles.ViaVersion.api.data.UserConnection
import us.myles.ViaVersion.api.protocol.ProtocolVersion
import us.myles.ViaVersion.api.type.Type
import us.myles.ViaVersion.exception.CancelCodecException
import us.myles.ViaVersion.packets.State
import java.math.BigInteger
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.SocketAddress
import java.security.KeyFactory
import java.security.MessageDigest
import java.security.PrivateKey
import java.security.PublicKey
import java.security.spec.X509EncodedKeySpec
import java.util.*
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
@ -37,27 +39,30 @@ import javax.naming.directory.InitialDirContext
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
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 CloudMinecraftHandler(
val user: UserConnection,
var other: Channel?,
val frontEnd: Boolean
) : SimpleChannelInboundHandler<ByteBuf>() {
val data get() = user.get(HandlerData::class.java)!!
var address: SocketAddress? = null
override fun channelRead0(ctx: ChannelHandlerContext, msg: ByteBuf) {
if (ctx.channel().isActive && !user.isPendingDisconnect && msg.isReadable) {
data!!.state.handleMessage(this, ctx, msg)
data.state.handleMessage(this, ctx, msg)
if (msg.isReadable) throw IllegalStateException("Remaining bytes!!!")
//other?.write(msg.retain())
}
@ -65,14 +70,14 @@ class CloudMinecraftHandler(val user: UserConnection,
override fun channelActive(ctx: ChannelHandlerContext) {
address = ctx.channel().remoteAddress()
if (data == null) {
if (user.get(HandlerData::class.java) == null) {
user.put(HandlerData(user, HandshakeState()))
}
}
override fun channelInactive(ctx: ChannelHandlerContext) {
other?.close()
data!!.state.onInactivated(this)
data.state.onInactivated(this)
}
override fun channelReadComplete(ctx: ChannelHandlerContext?) {
@ -90,13 +95,15 @@ class CloudMinecraftHandler(val user: UserConnection,
}
fun disconnect(s: String) {
data!!.state.disconnect(this, s)
data.state.disconnect(this, s)
}
}
interface MinecraftConnectionState {
fun handleMessage(handler: CloudMinecraftHandler, ctx: ChannelHandlerContext,
msg: ByteBuf)
fun handleMessage(
handler: CloudMinecraftHandler, ctx: ChannelHandlerContext,
msg: ByteBuf
)
fun disconnect(handler: CloudMinecraftHandler, msg: String) {
chLogger.info("Disconnected ${handler.address}: $msg")
@ -110,14 +117,17 @@ interface MinecraftConnectionState {
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
val packet = PacketRegistry.decode(
msg,
ProtocolVersion.getProtocol(handler.user.protocolInfo!!.serverProtocolVersion),
State.HANDSHAKE,
handler.frontEnd
)
if (packet !is HandshakePacket) throw IllegalArgumentException("Invalid packet!")
handler.data.protocolId = packet.protocolId
when (packet.nextState.ordinal) {
1 -> handler.data.state = StatusState
2 -> handler.data.state = LoginState
else -> throw IllegalStateException("Invalid next state")
}
@ -125,13 +135,13 @@ class HandshakeState : MinecraftConnectionState {
GlobalScope.launch(Dispatchers.IO) {
val frontHandler = handler.user.channel!!.pipeline().get(CloudMinecraftHandler::class.java)
try {
var srvResolvedAddr = backAddr
var srvResolvedPort = backPort
var srvResolvedAddr = packet.address
var srvResolvedPort = packet.port
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"]
.getAttributes("dns:///_minecraft._tcp.$srvResolvedAddr", arrayOf("SRV"))["SRV"]
if (attr != null && attr.size() > 0) {
val record = (attr.get(0) as String).split(" ")
srvResolvedAddr = record[3]
@ -142,16 +152,17 @@ class HandshakeState : MinecraftConnectionState {
}
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")
if (VIAaaSConfig.blockLocalAddress && (addrInfo.isSiteLocalAddress
|| addrInfo.isLoopbackAddress
|| addrInfo.isLinkLocalAddress
|| addrInfo.isAnyLocalAddress)
) throw SecurityException("Local addresses aren't allowed")
val bootstrap = Bootstrap().handler(BackendInit(handler.user))
.channelFactory(channelSocketFactory())
.group(handler.user.channel!!.eventLoop())
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 15_000) // Half of mc timeout
.connect(socketAddr)
.channelFactory(channelSocketFactory())
.group(handler.user.channel!!.eventLoop())
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 15_000) // Half of mc timeout
.connect(socketAddr)
bootstrap.addListener {
if (it.isSuccess) {
@ -161,23 +172,15 @@ class HandshakeState : MinecraftConnectionState {
backChan.pipeline().get(CloudMinecraftHandler::class.java).other = handler.user.channel
frontHandler.other = backChan
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)
backChan.writeAndFlush(backHandshake.retain())
} finally {
backHandshake.release()
}
packet.address = srvResolvedAddr
packet.port = srvResolvedPort
forward(handler, packet)
backChan.flush()
handler.user.channel!!.setAutoRead(true)
} else {
handler.user.channel!!.eventLoop().submit {
frontHandler.disconnect("Couldn't connect: " + it.cause().toString())
}
// We're in the event loop
frontHandler.disconnect("Couldn't connect: " + it.cause().toString())
}
}
} catch (e: Exception) {
@ -199,50 +202,49 @@ class HandshakeState : MinecraftConnectionState {
object 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")
val packet = PacketRegistry.decode(
msg,
ProtocolVersion.getProtocol(handler.user.protocolInfo!!.serverProtocolVersion),
State.LOGIN,
handler.frontEnd
)
when (packet) {
is LoginStart -> handleLoginStart(handler, packet)
is CryptoResponse -> handleCryptoResponse(handler, packet)
is PluginResponse -> forward(handler, packet)
is LoginDisconnect -> forward(handler, packet)
is CryptoRequest -> handleCryptoRequest(handler, packet)
is LoginSuccess -> handleLoginSuccess(handler, packet)
is SetCompression -> handleCompression(handler, packet)
is PluginRequest -> forward(handler, packet)
else -> throw IllegalArgumentException("Invalid packet!")
}
}
private fun forward(handler: CloudMinecraftHandler, msg: ByteBuf) {
msg.resetReaderIndex()
handler.other!!.write(msg.retain())
private fun handleLoginSuccess(handler: CloudMinecraftHandler, loginSuccess: LoginSuccess) {
handler.data.state = PlayState
forward(handler, loginSuccess)
}
private fun handleLoginSuccess(handler: CloudMinecraftHandler, msg: ByteBuf) {
handler.data!!.state = PlayState
forward(handler, msg)
}
private fun handleCompression(handler: CloudMinecraftHandler, msg: ByteBuf) {
private fun handleCompression(handler: CloudMinecraftHandler, setCompression: SetCompression) {
val pipe = handler.user.channel!!.pipeline()
val threshold = Type.VAR_INT.readPrimitive(msg)
val threshold = setCompression.threshold
val backPipe = pipe.get(CloudMinecraftHandler::class.java).other!!.pipeline()
backPipe.addAfter("frame", "compress", CloudCompressionCodec(threshold))
forward(handler, msg)
forward(handler, setCompression)
pipe.addAfter("frame", "compress", CloudCompressionCodec(threshold))
pipe.addAfter("frame", "decompress", EmptyChannelHandler()) // ViaRewind compat workaround
}
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)
fun handleCryptoRequest(handler: CloudMinecraftHandler, cryptoRequest: CryptoRequest) {
val data = handler.data
data.backServerId = cryptoRequest.serverId
data.backPublicKey = cryptoRequest.publicKey
data.backToken = cryptoRequest.token
val id = "VIAaaS" + ByteArray(10).let {
secureRandom.nextBytes(it)
@ -257,32 +259,27 @@ object LoginState : MinecraftConnectionState {
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()
}
cryptoRequest.serverId = id
cryptoRequest.publicKey = mcCryptoKey.public
cryptoRequest.token = token
forward(handler, cryptoRequest)
}
fun handleCryptoResponse(handler: CloudMinecraftHandler, msg: ByteBuf) {
fun handleCryptoResponse(handler: CloudMinecraftHandler, cryptoResponse: CryptoResponse) {
val frontHash = let {
val frontKey = decryptRsa(mcCryptoKey.private, Type.BYTE_ARRAY_PRIMITIVE.read(msg))
val frontKey = decryptRsa(mcCryptoKey.private, cryptoResponse.encryptedKey)
// RSA token - wat??? why is it encrypted with RSA if it was sent unencrypted?
val decryptedToken = decryptRsa(mcCryptoKey.private, Type.BYTE_ARRAY_PRIMITIVE.read(msg))
val decryptedToken = decryptRsa(mcCryptoKey.private, cryptoResponse.encryptedToken)
if (!decryptedToken.contentEquals(handler.data!!.frontToken!!)) throw IllegalStateException("invalid token!")
if (!decryptedToken.contentEquals(handler.data.frontToken!!)) throw IllegalStateException("invalid token!")
val aesEn = mcCfb8(frontKey, Cipher.ENCRYPT_MODE)
val aesDe = mcCfb8(frontKey, Cipher.DECRYPT_MODE)
handler.user.channel!!.pipeline().addBefore("frame", "crypto", CloudCrypto(aesDe, aesEn))
generateServerHash(handler.data!!.frontId!!, frontKey, mcCryptoKey.public)
generateServerHash(handler.data.frontId!!, frontKey, mcCryptoKey.public)
}
val backKey = ByteArray(16).let {
@ -290,22 +287,25 @@ object LoginState : MinecraftConnectionState {
it
}
val backHash = generateServerHash(handler.data!!.backServerId!!, backKey, handler.data!!.backPublicKey!!)
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!!.frontName!!)}&serverId=$frontHash")
?: throw IllegalArgumentException("Couldn't authenticate with session servers")
"https://sessionserver.mojang.com/session/minecraft/hasJoined?username=" +
"${
UrlEscapers.urlFormParameterEscaper().escape(handler.data.frontName!!)
}&serverId=$frontHash"
)
?: throw IllegalArgumentException("Couldn't authenticate with session servers")
val sessionJoin = viaWebServer.requestSessionJoin(
fromUndashed(profile.get("id")!!.asString),
handler.data!!.backName!!,
backHash,
handler.address!!, // Frontend handler
handler.data!!.backPublicKey!!
fromUndashed(profile.get("id")!!.asString),
handler.data.backName!!,
backHash,
handler.address!!, // Frontend handler
handler.data.backPublicKey!!
)
val backChan = handler.other!!
@ -313,21 +313,15 @@ object LoginState : MinecraftConnectionState {
if (throwable != null) {
handler.disconnect("Online mode error: $throwable")
} else {
val backMsg = ByteBufAllocator.DEFAULT.buffer()
try {
backMsg.writeByte(1) // Packet id
Type.BYTE_ARRAY_PRIMITIVE.write(backMsg, encryptRsa(handler.data!!.backPublicKey!!, backKey))
Type.BYTE_ARRAY_PRIMITIVE.write(backMsg, encryptRsa(handler.data!!.backPublicKey!!, handler.data!!.backToken!!))
backChan.writeAndFlush(backMsg.retain())
cryptoResponse.encryptedKey = encryptRsa(handler.data.backPublicKey!!, backKey)
cryptoResponse.encryptedToken =
encryptRsa(handler.data.backPublicKey!!, handler.data.backToken!!)
forward(handler, cryptoResponse)
backChan.flush()
val backAesEn = mcCfb8(backKey, Cipher.ENCRYPT_MODE)
val backAesDe = mcCfb8(backKey, Cipher.DECRYPT_MODE)
backChan.pipeline().addBefore("frame", "crypto", CloudCrypto(backAesDe, backAesEn))
} finally {
backMsg.release()
}
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) {
@ -337,17 +331,12 @@ object LoginState : MinecraftConnectionState {
}
}
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()
}
fun handleLoginStart(handler: CloudMinecraftHandler, loginStart: LoginStart) {
handler.data.frontName = loginStart.username
handler.data.backName = handler.user.get(CloudData::class.java)!!.altName ?: handler.data.frontName
loginStart.username = handler.data.backName!!
forward(handler, loginStart)
}
override fun disconnect(handler: CloudMinecraftHandler, msg: String) {
@ -358,8 +347,8 @@ object LoginState : MinecraftConnectionState {
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() }
.sendRawPacketFuture(packet.retain())
.addListener { handler.user.channel?.close() }
} finally {
packet.release()
}
@ -380,10 +369,12 @@ object StatusState : MinecraftConnectionState {
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())
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() }
.addListener { handler.user.channel?.close() }
} finally {
packet.release()
}
@ -422,3 +413,32 @@ fun mcCfb8(key: ByteArray, mode: Int): Cipher {
it
}
}
fun Channel.setAutoRead(b: Boolean) {
this.config().isAutoRead = b
if (b) this.read()
}
fun twosComplementHexdigest(digest: ByteArray): String {
return BigInteger(digest).toString(16)
}
// https://github.com/VelocityPowered/Velocity/blob/0dd6fe1ef2783fe1f9322af06c6fd218aa67cdb1/proxy/src/main/java/com/velocitypowered/proxy/util/EncryptionUtils.java
fun generateServerHash(serverId: String, sharedSecret: ByteArray?, key: PublicKey): String {
val digest = MessageDigest.getInstance("SHA-1")
digest.update(serverId.toByteArray(Charsets.ISO_8859_1))
digest.update(sharedSecret)
digest.update(key.encoded)
return twosComplementHexdigest(digest.digest())
}
private fun forward(handler: CloudMinecraftHandler, packet: Packet) {
val msg = ByteBufAllocator.DEFAULT.buffer()
try {
PacketRegistry.encode(packet, msg, ProtocolVersion.getProtocol(handler.data.protocolId!!))
handler.other!!.write(msg.retain())
} finally {
msg.release()
}
}

View File

@ -0,0 +1,306 @@
package com.github.creeper123123321.viaaas
import com.google.common.collect.Range
import io.netty.buffer.ByteBuf
import us.myles.ViaVersion.api.protocol.ProtocolVersion
import us.myles.ViaVersion.api.type.Type
import us.myles.ViaVersion.packets.State
import java.security.KeyFactory
import java.security.PublicKey
import java.security.spec.X509EncodedKeySpec
import java.util.*
import java.util.function.Supplier
import kotlin.properties.Delegates
/**
* A mutable object which represents a Minecraft packet data
*/
interface Packet {
fun decode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion)
fun encode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion)
}
object PacketRegistry {
val entries = mutableListOf<RegistryEntry>()
init {
entries.add(
RegistryEntry(Range.all(), State.HANDSHAKE, 0, true, ::HandshakePacket, HandshakePacket::class.java)
)
entries.add(
RegistryEntry(Range.all(), State.LOGIN, 0, true, ::LoginStart, LoginStart::class.java)
)
entries.add(
RegistryEntry(Range.all(), State.LOGIN, 1, true, ::CryptoResponse, CryptoResponse::class.java)
)
entries.add(
RegistryEntry(
Range.atLeast(ProtocolVersion.v1_13.version),
State.LOGIN,
2,
true,
::PluginResponse,
PluginResponse::class.java
)
)
entries.add(
RegistryEntry(Range.all(), State.LOGIN, 0, false, ::LoginDisconnect, LoginDisconnect::class.java)
)
entries.add(
RegistryEntry(Range.all(), State.LOGIN, 1, false, ::CryptoRequest, CryptoRequest::class.java)
)
entries.add(
RegistryEntry(Range.all(), State.LOGIN, 2, false, ::LoginSuccess, LoginSuccess::class.java)
)
entries.add(
RegistryEntry(Range.all(), State.LOGIN, 3, false, ::SetCompression, SetCompression::class.java)
)
entries.add(
RegistryEntry(Range.all(), State.LOGIN, 4, false, ::PluginRequest, PluginRequest::class.java)
)
}
data class RegistryEntry(
val versionRange: Range<Int>,
val state: State,
val id: Int,
val serverBound: Boolean,
val constructor: Supplier<Packet>,
val packetClass: Class<out Packet>
)
fun getPacketConstructor(
protocolVersion: ProtocolVersion,
state: State,
id: Int,
serverBound: Boolean
): Supplier<out Packet>? {
return entries.firstOrNull {
it.serverBound == serverBound && it.state == state
&& it.versionRange.contains(protocolVersion.version) && it.id == id
}?.constructor
}
fun getPacketId(packetClass: Class<out Packet>, protocolVersion: ProtocolVersion): Int? {
return entries.firstOrNull {
it.versionRange.contains(protocolVersion.version) && it.packetClass == packetClass
}?.id
}
fun decode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion, state: State, serverBound: Boolean): Packet {
val packetId = Type.VAR_INT.readPrimitive(byteBuf)
val packet =
getPacketConstructor(protocolVersion, state, packetId, serverBound)?.get() ?: UnknownPacket(packetId)
packet.decode(byteBuf, protocolVersion)
if (byteBuf.isReadable) throw IllegalStateException("Remaining bytes!")
return packet
}
fun encode(packet: Packet, byteBuf: ByteBuf, protocolVersion: ProtocolVersion) {
val id = if (packet is UnknownPacket) {
packet.id
} else {
getPacketId(packet.javaClass, protocolVersion)!!
}
Type.VAR_INT.writePrimitive(byteBuf, id)
packet.encode(byteBuf, protocolVersion)
}
}
class UnknownPacket(val id: Int) : Packet {
lateinit var content: ByteBuf
override fun decode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) {
content = byteBuf.slice().clear()
}
override fun encode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) {
byteBuf.writeBytes(content)
}
}
// Some code based on https://github.com/VelocityPowered/Velocity/tree/dev/1.1.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet
class HandshakePacket : Packet {
var protocolId by Delegates.notNull<Int>()
lateinit var address: String
var port by Delegates.notNull<Int>()
lateinit var nextState: State
override fun decode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) {
protocolId = Type.VAR_INT.readPrimitive(byteBuf)
address = Type.STRING.read(byteBuf)
port = byteBuf.readUnsignedShort()
nextState = State.values()[Type.VAR_INT.readPrimitive(byteBuf)]
}
override fun encode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) {
Type.VAR_INT.writePrimitive(byteBuf, protocolId)
Type.STRING.write(byteBuf, address)
byteBuf.writeShort(port)
byteBuf.writeByte(nextState.ordinal) // var int is too small, fits in a byte
}
}
class LoginStart : Packet {
lateinit var username: String
override fun decode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) {
username = Type.STRING.read(byteBuf)
}
override fun encode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) {
Type.STRING.write(byteBuf, username)
}
}
class CryptoResponse : Packet {
lateinit var encryptedKey: ByteArray
lateinit var encryptedToken: ByteArray
override fun decode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) {
if (protocolVersion.version >= ProtocolVersion.v1_8.version) {
encryptedKey = Type.BYTE_ARRAY_PRIMITIVE.read(byteBuf)
encryptedToken = Type.BYTE_ARRAY_PRIMITIVE.read(byteBuf)
} else {
encryptedKey = ByteArray(byteBuf.readUnsignedShort()).also { byteBuf.readBytes(it) }
encryptedToken = ByteArray(byteBuf.readUnsignedShort()).also { byteBuf.readBytes(it) }
}
}
override fun encode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) {
if (protocolVersion.version >= ProtocolVersion.v1_8.version) {
Type.BYTE_ARRAY_PRIMITIVE.write(byteBuf, encryptedKey)
Type.BYTE_ARRAY_PRIMITIVE.write(byteBuf, encryptedToken)
} else {
byteBuf.writeShort(encryptedKey.size)
byteBuf.writeBytes(encryptedKey)
byteBuf.writeShort(encryptedToken.size)
byteBuf.writeBytes(encryptedToken)
}
}
}
class PluginResponse : Packet {
var id by Delegates.notNull<Int>()
var success by Delegates.notNull<Boolean>()
lateinit var data: ByteBuf
override fun decode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) {
id = Type.VAR_INT.readPrimitive(byteBuf)
success = byteBuf.readBoolean()
if (success) {
data = byteBuf.slice().clear()
}
}
override fun encode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) {
Type.VAR_INT.writePrimitive(byteBuf, id)
byteBuf.writeBoolean(success)
if (success) {
byteBuf.writeBytes(data)
}
}
}
class LoginDisconnect : Packet {
lateinit var msg: String
override fun decode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) {
msg = Type.STRING.read(byteBuf)
}
override fun encode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) {
Type.STRING.write(byteBuf, msg)
}
}
class CryptoRequest : Packet {
lateinit var serverId: String
lateinit var publicKey: PublicKey
lateinit var token: ByteArray
override fun decode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) {
serverId = Type.STRING.read(byteBuf)
if (protocolVersion.version >= ProtocolVersion.v1_8.version) {
publicKey = KeyFactory.getInstance("RSA")
.generatePublic(X509EncodedKeySpec(Type.BYTE_ARRAY_PRIMITIVE.read(byteBuf)))
token = Type.BYTE_ARRAY_PRIMITIVE.read(byteBuf)
} else {
publicKey = KeyFactory.getInstance("RSA")
.generatePublic(X509EncodedKeySpec(ByteArray(byteBuf.readUnsignedShort()).also { byteBuf.readBytes(it) }))
token = ByteArray(byteBuf.readUnsignedShort()).also { byteBuf.readBytes(it) }
}
}
override fun encode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) {
Type.STRING.write(byteBuf, serverId)
if (protocolVersion.version >= ProtocolVersion.v1_8.version) {
Type.BYTE_ARRAY_PRIMITIVE.write(byteBuf, publicKey.encoded)
Type.BYTE_ARRAY_PRIMITIVE.write(byteBuf, token)
} else {
val encodedKey = publicKey.encoded
byteBuf.writeShort(encodedKey.size)
byteBuf.writeBytes(encodedKey)
byteBuf.writeShort(token.size)
byteBuf.writeBytes(token)
}
}
}
class LoginSuccess : Packet {
lateinit var id: UUID
lateinit var username: String
override fun decode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) {
id = when {
protocolVersion.version >= ProtocolVersion.v1_16.version -> {
Type.UUID_INT_ARRAY.read(byteBuf)
}
protocolVersion.version >= ProtocolVersion.v1_7_6.version -> {
UUID.fromString(Type.STRING.read(byteBuf))
}
else -> fromUndashed(Type.STRING.read(byteBuf))
}
username = Type.STRING.read(byteBuf)
}
override fun encode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) {
when {
protocolVersion.version >= ProtocolVersion.v1_16.version -> {
Type.UUID_INT_ARRAY.write(byteBuf, id)
}
protocolVersion.version >= ProtocolVersion.v1_7_6.version -> {
Type.STRING.write(byteBuf, id.toString())
}
else -> Type.STRING.write(byteBuf, id.toString().replace("-", ""))
}
Type.STRING.write(byteBuf, username)
}
}
class SetCompression : Packet {
var threshold by Delegates.notNull<Int>()
override fun decode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) {
threshold = Type.VAR_INT.readPrimitive(byteBuf)
}
override fun encode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) {
Type.VAR_INT.writePrimitive(byteBuf, threshold)
}
}
class PluginRequest : Packet {
var id by Delegates.notNull<Int>()
lateinit var channel: String
lateinit var data: ByteBuf
override fun decode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) {
id = Type.VAR_INT.readPrimitive(byteBuf)
channel = Type.STRING.read(byteBuf)
data = byteBuf.slice().clear()
}
override fun encode(byteBuf: ByteBuf, protocolVersion: ProtocolVersion) {
Type.VAR_INT.writePrimitive(byteBuf, id)
Type.STRING.write(byteBuf, channel)
byteBuf.writeBytes(data)
}
}

View File

@ -1,6 +1,5 @@
package com.github.creeper123123321.viaaas
import io.netty.channel.Channel
import org.slf4j.LoggerFactory
import us.myles.ViaVersion.api.PacketWrapper
import us.myles.ViaVersion.api.data.StoredObject
@ -11,10 +10,6 @@ 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.security.MessageDigest
import java.security.PublicKey
import java.security.SecureRandom
class CloudPipeline(userConnection: UserConnection) : ProtocolPipeline(userConnection) {
override fun registerPackets() {
@ -39,7 +34,8 @@ object CloudHeadProtocol : SimpleProtocol() {
val addr = wrapper.read(Type.STRING) // Server Address
val receivedPort = wrapper.read(Type.UNSIGNED_SHORT)
val parsed = VIAaaSAddress().parse(addr, VIAaaSConfig.hostName)
val parsed = VIAaaSAddress().parse(addr.substringBefore(0.toChar()), VIAaaSConfig.hostName)
if (parsed.viaSuffix == null && VIAaaSConfig.requireHostName) throw IllegalStateException("VIAaaS hostname is required")
val backPort = parsed.port ?: receivedPort
val backAddr = parsed.realAddress
val backProto = parsed.protocol ?: 47
@ -48,14 +44,17 @@ object CloudHeadProtocol : SimpleProtocol() {
wrapper.write(Type.UNSIGNED_SHORT, backPort)
val playerAddr = wrapper.user().channel!!.pipeline()
.get(CloudMinecraftHandler::class.java)!!.address
.get(CloudMinecraftHandler::class.java)!!.address
logger.info("Connecting $playerAddr ($playerVer) -> $backAddr:$backPort ($backProto)")
wrapper.user().put(CloudData(
wrapper.user().put(
CloudData(
userConnection = wrapper.user(),
backendVer = backProto,
frontOnline = parsed.online,
altName = parsed.altUsername))
altName = parsed.altUsername
)
)
wrapper.passthrough(Type.VAR_INT) // Next state
}
@ -64,28 +63,9 @@ object CloudHeadProtocol : SimpleProtocol() {
}
}
fun Channel.setAutoRead(b: Boolean) {
this.config().isAutoRead = b
if (b) this.read()
}
val secureRandom = SecureRandom.getInstanceStrong()
fun twosComplementHexdigest(digest: ByteArray): String {
return BigInteger(digest).toString(16)
}
// https://github.com/VelocityPowered/Velocity/blob/0dd6fe1ef2783fe1f9322af06c6fd218aa67cdb1/proxy/src/main/java/com/velocitypowered/proxy/util/EncryptionUtils.java
fun generateServerHash(serverId: String, sharedSecret: ByteArray?, key: PublicKey): String {
val digest = MessageDigest.getInstance("SHA-1")
digest.update(serverId.toByteArray(Charsets.ISO_8859_1))
digest.update(sharedSecret)
digest.update(key.encoded)
return twosComplementHexdigest(digest.digest())
}
data class CloudData(val userConnection: UserConnection,
var backendVer: Int,
var frontOnline: Boolean,
var altName: String?
data class CloudData(
val userConnection: UserConnection,
var backendVer: Int,
var frontOnline: Boolean,
var altName: String?
) : StoredObject(userConnection)

View File

@ -41,6 +41,7 @@ import us.myles.ViaVersion.util.Config
import java.io.File
import java.net.InetAddress
import java.security.KeyPairGenerator
import java.security.SecureRandom
import java.util.*
import java.util.concurrent.CompletableFuture
@ -60,10 +61,12 @@ val initFuture = CompletableFuture<Unit>()
// Minecraft doesn't have forward secrecy
val mcCryptoKey = KeyPairGenerator.getInstance("RSA").let {
it.initialize(4096) // https://stackoverflow.com/questions/1904516/is-1024-bit-rsa-secure
it.initialize(VIAaaSConfig.mcRsaSize) // https://stackoverflow.com/questions/1904516/is-1024-bit-rsa-secure
it.genKeyPair()
}
val secureRandom = if (VIAaaSConfig.useStrongRandom) SecureRandom.getInstanceStrong() else SecureRandom()
fun eventLoopGroup(): EventLoopGroup {
if (VIAaaSConfig.isNativeTransportMc) {
if (Epoll.isAvailable()) return EpollEventLoopGroup()
@ -249,6 +252,10 @@ object VIAaaSConfig : Config(File("config/viaaas.yml")) {
val port: Int get() = this.getInt("port", 25565)
val bindAddress: String get() = this.getString("bind-address", "localhost")!!
val hostName: String get() = this.getString("host-name", "viaaas.localhost")!!
val mcRsaSize: Int get() = this.getInt("mc-rsa-size", 4096)
val useStrongRandom: Boolean get() = this.getBoolean("use-strong-random", true)
val blockLocalAddress: Boolean get() = this.getBoolean("block-local-address", true)
val requireHostName: Boolean get() = this.getBoolean("require-host-name", true)
}
class VIAaaSAddress {

View File

@ -7,4 +7,15 @@ bind-address: localhost
# Host name of this instance, that will be used in the virtual host
host-name: viaaas.localhost
# Use netty native transport for Minecraft when available.
native-transport-mc: true
native-transport-mc: true
# Sets the RSA key size used by client for encrypting the AES symmetric key when using online mode.
# Minecraft default is 1024. See https://stackoverflow.com/questions/1904516/is-1024-bit-rsa-secure
mc-rsa-size: 4096
# Use SecureRandom.getInstanceStrong(). May block if there's not enough entropy when using online mode.
# See https://wiki.archlinux.org/index.php/Rng-tools
use-strong-random: true
# Blocks connection to local addresses
block-local-address: true
# Requires virtual host to contain the value from "host-name"
# A false value could be used for transparent proxying.
require-host-name: true

View File

@ -11,7 +11,8 @@
<meta property="og:image" content="https://raw.githubusercontent.com/ViaVersion/ViaVersion/a13c417352298c2269aed8736a76205f0040b705/fabric/src/main/resources/assets/viaversion/textures/squarelogo.png">
<meta property="og:type" content="game">
<link rel="icon" href="https://raw.githubusercontent.com/ViaVersion/ViaVersion/a13c417352298c2269aed8736a76205f0040b705/fabric/src/main/resources/assets/viaversion/textures/squarelogo.png">
<meta http-equiv="Content-Security-Policy" content="default-src 'self' https://cdnjs.cloudflare.com/; img-src https://*; connect-src 'self' http: https: ws: wss:">
<meta http-equiv="Content-Security-Policy" content="default-src 'self' https://cdnjs.cloudflare.com/; img-src https://*; connect-src 'self' http://localhost:*/ https: ws: wss:">
<!-- only accept http from localhost -->
<title>VIAaaS Authenticator</title>
<link rel="stylesheet" href="style.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>

View File

@ -14,8 +14,8 @@ $(() => {
var content = document.getElementById("content");
var acounts = document.getElementById("accounts");
$("#cors-proxy").val(localStorage.getItem("cors-proxy"));
$("#cors-proxy").on("change", () => localStorage.setItem('cors-proxy', $("#cors-proxy").val()));
$("#cors-proxy").val(localStorage.getItem("cors-proxy"));
$("#login_submit_mc").on("click", loginMc);
function loginMc() {