some ugly css, allow continuing without auth, code refactors, add gradle versions plugin, fix null address, add snakeyaml, coroutines, override viaconnectionmanager

This commit is contained in:
creeper123123321 2020-11-06 21:35:40 -03:00
parent f57ec67803
commit dacd630d73
7 changed files with 137 additions and 94 deletions

View File

@ -13,11 +13,11 @@ Idea: server.example.com._p25565._v1_12_2._otrue.viaaas.example.com (default bac
- 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
- Run the shadow jar or ./gradlew clean run
- Connect to mc.example.com._v1_8.viaaas.localhost
Usage for online mode (may block your Mojang account):
- ./gradlew clean run
- Run the shadow jar or ./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.

View File

@ -1,5 +1,6 @@
plugins {
id("com.github.johnrengelman.shadow") version "6.1.0"
id("com.github.ben-manes.versions") version "0.34.0"
application
kotlin("jvm") version "1.4.10"
}
@ -28,6 +29,7 @@ dependencies {
implementation("nl.matsv:viabackwards-all:3.2.0")
implementation("de.gerrygames:viarewind-all:1.5.2")
implementation("io.netty:netty-all:4.1.53.Final")
implementation("org.yaml:snakeyaml:1.26")
implementation("org.apache.logging.log4j:log4j-core:2.13.3")
implementation("org.apache.logging.log4j:log4j-slf4j-impl:2.13.3")
@ -59,5 +61,6 @@ tasks {
}
build {
dependsOn(shadowJar)
dependsOn(named("dependencyUpdates"))
}
}

View File

@ -4,7 +4,6 @@ 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
@ -18,14 +17,15 @@ 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 java.net.InetAddress
import java.net.InetSocketAddress
import java.net.SocketAddress
import java.security.KeyFactory
import java.security.PrivateKey
import java.security.PublicKey
import java.security.spec.X509EncodedKeySpec
import java.util.*
@ -54,6 +54,7 @@ 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 (!user.isPendingDisconnect) {
@ -63,14 +64,15 @@ class CloudMinecraftHandler(val user: UserConnection,
}
}
override fun channelActive(ctx: ChannelHandlerContext?) {
override fun channelActive(ctx: ChannelHandlerContext) {
address = ctx.channel().remoteAddress()
if (data == null) {
user.put(HandlerData(user, HandshakeState()))
}
}
override fun channelInactive(ctx: ChannelHandlerContext) {
chLogger.info(ctx.channel().remoteAddress().toString() + " was disconnected")
chLogger.info(address?.toString() + " was disconnected")
other?.close()
}
@ -91,7 +93,7 @@ class CloudMinecraftHandler(val user: UserConnection,
fun disconnect(s: String) {
if (user.channel?.isActive != true) return
chLogger.info("Disconnecting " + user.channel!!.remoteAddress() + ": " + s)
chLogger.info("Disconnecting $address: $s")
data!!.state.disconnect(this, s)
}
}
@ -117,8 +119,8 @@ class HandshakeState : MinecraftConnectionState {
}
handler.user.channel!!.setAutoRead(false)
Via.getPlatform().runAsync {
val frontForwarder = handler.user.channel!!.pipeline().get(CloudMinecraftHandler::class.java)
GlobalScope.launch(Dispatchers.IO) {
val frontHandler = handler.user.channel!!.pipeline().get(CloudMinecraftHandler::class.java)
try {
var srvResolvedAddr = backAddr
var srvResolvedPort = backPort
@ -150,11 +152,11 @@ class HandshakeState : MinecraftConnectionState {
bootstrap.addListener {
if (it.isSuccess) {
CloudHeadProtocol.logger.info("conected ${handler.user.channel?.remoteAddress()} to $socketAddr")
CloudHeadProtocol.logger.info("Connected ${frontHandler} to $socketAddr")
val sockChan = bootstrap.channel() as SocketChannel
sockChan.pipeline().get(CloudMinecraftHandler::class.java).other = handler.user.channel
frontForwarder.other = sockChan
val backChan = bootstrap.channel() as SocketChannel
backChan.pipeline().get(CloudMinecraftHandler::class.java).other = handler.user.channel
frontHandler.other = backChan
val backHandshake = ByteBufAllocator.DEFAULT.buffer()
try {
@ -163,7 +165,7 @@ class HandshakeState : MinecraftConnectionState {
Type.STRING.write(backHandshake, srvResolvedAddr) // Server Address
backHandshake.writeShort(srvResolvedPort)
Type.VAR_INT.writePrimitive(backHandshake, nextAddr)
sockChan.writeAndFlush(backHandshake.retain())
backChan.writeAndFlush(backHandshake.retain())
} finally {
backHandshake.release()
}
@ -171,13 +173,13 @@ class HandshakeState : MinecraftConnectionState {
handler.user.channel!!.setAutoRead(true)
} else {
handler.user.channel!!.eventLoop().submit {
frontForwarder.disconnect("Couldn't connect: " + it.cause().toString())
frontHandler.disconnect("Couldn't connect: " + it.cause().toString())
}
}
}
} catch (e: Exception) {
handler.user.channel!!.eventLoop().submit {
frontForwarder.disconnect("Couldn't connect: $e")
frontHandler.disconnect("Couldn't connect: $e")
}
}
}
@ -263,28 +265,14 @@ class LoginState : MinecraftConnectionState {
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))
}
val frontKey = decryptRsa(mcCryptoKey.private, 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))
}
val decryptedToken = decryptRsa(mcCryptoKey.private, 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
}
val aesEn = mcCfb8(frontKey, Cipher.ENCRYPT_MODE)
val aesDe = mcCfb8(frontKey, Cipher.DECRYPT_MODE)
handler.user.channel!!.pipeline().get(CloudEncryptor::class.java).cipher = aesEn
handler.user.channel!!.pipeline().get(CloudDecryptor::class.java).cipher = aesDe
@ -297,17 +285,6 @@ class LoginState : MinecraftConnectionState {
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)
@ -315,36 +292,33 @@ class LoginState : MinecraftConnectionState {
try {
val profile = httpClient.get<JsonObject?>(
"https://sessionserver.mojang.com/session/minecraft/hasJoined?username=" +
"${UrlEscapers.urlFormParameterEscaper().escape(handler.data!!.backName!!)}&serverId=$frontHash")
"${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
}
val sessionJoin = viaWebServer.requestSessionJoin(
fromUndashed(profile.get("id")!!.asString),
handler.data!!.backName!!,
backHash,
handler.address!!, // Frontend handler
handler.data!!.backPublicKey!!
)
if (!sent) {
throw IllegalStateException("No connection to browser, connect in /auth.html")
if (sessionJoin.first == 0) {
throw IllegalStateException("No browsers listening to this account, connect in /auth.html")
} else {
viaWebServer.pendingSessionHashes.get(backHash).get(15, TimeUnit.SECONDS)
sessionJoin.second.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)
})
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())
val backAesEn = mcCfb8(backKey, Cipher.ENCRYPT_MODE)
val backAesDe = mcCfb8(backKey, Cipher.DECRYPT_MODE)
backChan.pipeline().get(CloudEncryptor::class.java).cipher = backAesEn
backChan.pipeline().get(CloudDecryptor::class.java).cipher = backAesDe
} finally {
@ -417,4 +391,23 @@ class PlayState : MinecraftConnectionState {
override fun disconnect(handler: CloudMinecraftHandler, msg: String) {
handler.user.disconnect(msg)
}
}
fun decryptRsa(privateKey: PrivateKey, data: ByteArray) = Cipher.getInstance("RSA").let {
it.init(Cipher.DECRYPT_MODE, privateKey)
it.doFinal(data)
}
fun encryptRsa(publicKey: PublicKey, data: ByteArray) = Cipher.getInstance("RSA").let {
it.init(Cipher.ENCRYPT_MODE, publicKey)
it.doFinal(data)
}
fun mcCfb8(key: ByteArray, mode: Int) : Cipher {
val spec = SecretKeySpec(key, "AES")
val iv = IvParameterSpec(key)
return Cipher.getInstance("AES/CFB8/NoPadding").let {
it.init(mode, spec, iv)
it
}
}

View File

@ -101,8 +101,8 @@ object CloudAPI : ViaAPI<Unit> {
}
object CloudPlatform : ViaPlatform<Unit> {
val connMan = ViaConnectionManager()
val executor = Executors.newCachedThreadPool(ThreadFactoryBuilder().setNameFormat("VIAaaS").setDaemon(true).build())
val connMan = CloudConnectionManager()
val executor = Executors.newCachedThreadPool(ThreadFactoryBuilder().setNameFormat("Via-%d").setDaemon(true).build())
val eventLoop = DefaultEventLoop(executor)
init {
@ -139,8 +139,12 @@ object CloudPlatform : ViaPlatform<Unit> {
override fun getPlatformName(): String = "VIAaaS"
override fun getPluginVersion(): String = VersionInfo.VERSION
override fun isOldClientsAllowed(): Boolean = true
override fun isProxy(): Boolean = true
}
class CloudConnectionManager: ViaConnectionManager() {
override fun isFrontEnd(conn: UserConnection): Boolean = false
}
object CloudConfig : AbstractViaConfig(File("config/viaversion.yml")) {
// https://github.com/ViaVersion/ViaFabric/blob/mc-1.16/src/main/java/com/github/creeper123123321/viafabric/platform/VRViaConfig.java

View File

@ -47,8 +47,9 @@ object CloudHeadProtocol : SimpleProtocol() {
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)")
val playerAddr = wrapper.user().channel!!.pipeline()
.get(CloudMinecraftHandler::class.java)!!.address
logger.info("Connecting $playerAddr ($playerVer) -> $backAddr:$backPort ($backProto)")
wrapper.user().put(CloudData(
userConnection = wrapper.user(),

View File

@ -20,7 +20,9 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.slf4j.LoggerFactory
import org.slf4j.event.Level
import java.net.SocketAddress
import java.net.URLEncoder
import java.security.PublicKey
import java.time.Duration
import java.util.*
import java.util.UUID
@ -101,7 +103,21 @@ class WebDashboardServer {
val pendingSessionHashes = CacheBuilder.newBuilder()
.expireAfterWrite(30, TimeUnit.SECONDS)
.build<String, CompletableFuture<Void>>(CacheLoader.from { _ -> CompletableFuture() })
.build<String, CompletableFuture<Unit>>(CacheLoader.from { _ -> CompletableFuture() })
suspend fun requestSessionJoin(id: UUID, name: String, hash: String,
address: SocketAddress, backKey: PublicKey)
: Pair<Int, CompletableFuture<Unit>> {
var sent = 0
viaWebServer.listeners[id]?.forEach {
it.ws.send("""{"action": "session_hash_request", "user": "$name", "session_hash": "$hash",
| "client_address": "$address", "backend_public_key":
| "${Base64.getEncoder().encodeToString(backKey.encoded)}"}""".trimMargin())
it.ws.flush()
sent++
}
return sent to viaWebServer.pendingSessionHashes.get(hash)
}
suspend fun connected(ws: WebSocketServerSession) {
val loginState = WebLogin()

View File

@ -4,34 +4,54 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>VIAaaS Authenticator</title>
<style>body {font-family: sans-serif}</style>
<style>
body {
font-family: sans-serif
}
@media (min-width: 700px) {
#browser_accounts {
float: right
}
}
#browser_accounts {
border: 1px solid black;
padding: 10px
}
#connection_status {
background: black;
color: white
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/uuid/8.3.1/uuid.min.js"></script>
</head>
<body>
<div id="browser_accounts">
<p>Browser Minecraft accounts:</p>
<p><label for="cors-proxy">CORS Proxy URL:</label>
<br>
<input type="url" id="cors-proxy" name="cors-proxy" value="" onchange="localStorage.setItem('cors-proxy', this.value);">
</p>
<p><span id="add-account">
<label for="email">Email/Username (legacy):</label>
<br>
<input type="text" id="email" name="email" value="">
<br>
<label for="password">Password:</label><br>
<input type="password" id="password" name="password" value="">
<br><br>
<input type="button" value="Login into Minecraft" onclick="loginMc()">
</span></p>
<span id="accounts"></span>
</div>
<div id="server_content">
<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/Rob--W/cors-anywhere
for setting up one.</p>
<label for="cors-proxy">CORS Proxy URL:</label>
<br>
<input type="url" id="cors-proxy" name="cors-proxy" value="" onchange="localStorage.setItem('cors-proxy', this.value);">
<script></script>
<hr>
<p>Browser Minecraft accounts:</p>
<p><span id="add-account">
<label for="email">Email/Username (legacy):</label>
<br>
<input type="text" id="email" name="email" value="">
<br>
<label for="password">Password:</label><br>
<input type="password" id="password" name="password" value="">
<br><br>
<input type="button" value="Login into Minecraft" onclick="loginMc()">
</span></p>
<span id="accounts"></span>
<hr>
for setting up one. Calling Mojang API from a remote IP address may block your account.</p>
<p>WebSocket connection status: <span id="connection_status">?</span></p>
<hr>
<p><span id="content"></span></p>
</div>
<script>
let urlParams = new URLSearchParams();
window.location.search.split("?").map(it => new URLSearchParams(it).forEach((a, b) => urlParams.append(b, a)));
@ -167,6 +187,10 @@
socket.send(JSON.stringify({"action": "listen_login_requests", "token": token}));
}
function confirmJoin(hash) {
socket.send(JSON.stringify({action: "session_hash_response", session_hash: hash}));
}
function saveToken(token) {
let hTokens = JSON.parse(localStorage.getItem("tokens")) || {};
let tokens = hTokens[wsUrl] || [];
@ -241,7 +265,7 @@
content.appendChild(p);
} else if (parsed.action == "minecraft_id_result") {
if (!parsed.success) {
alert("Server couldn't verify account via Minecraft.ID");
alert("VIAaaS instance couldn't verify account via Minecraft.ID");
} else {
listen(parsed.token);
saveToken(parsed.token);
@ -255,7 +279,7 @@
removeToken(parsed.token);
}
} else if (parsed.action == "session_hash_request") {
if (confirm("auth request sent from server, info: " + event.data + ". Should we authenticate?")) {
if (confirm("Confirm auth request sent from VIAaaS instance? info: " + event.data)) {
let accounts = getMcAccounts().filter(it => it.user.toLowerCase() == parsed.user.toLowerCase());
accounts.forEach(it => {
$.ajax({type: "post",
@ -268,13 +292,15 @@
contentType: "application/json",
dataType: "json"
}).done((data) => {
socket.send(JSON.stringify({"action": "session_hash_response", "session_hash": parsed.session_hash}));
confirmJoin(parsed.session_hash);
}).fail((e) => {
console.log(e);
alert("Failed to authenticate to Minecraft backend server!");
});
});
if (accounts.length == 0) alert("Couldn't find " + parsed.user + " account in browser");
if (accounts.length == 0 && confirm("Couldn't find " + parsed.user + " account in browser. Continue without authentication (works on LAN worlds)?")) {
confirmJoin(parsed.session_hash);
}
}
}
};