diff --git a/README.md b/README.md index 7e4abd7..ff7c482 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ 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) +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) - TODO: _o option for disabling online mode only in front end, protocol auto detection @@ -18,8 +19,10 @@ Usage for offline mode: Usage for online mode (may block your Mojang account): - 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)). +- You'll need 2 premium accounts for online mode (using only one account is possible but, as only one access tokens + can be active, your Minecraft client will give Bad Login after you approve the login) +- 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 diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/CloudHandler.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/CloudHandler.kt index 4ff32bd..2f43233 100644 --- a/src/main/kotlin/com/github/creeper123123321/viaaas/CloudHandler.kt +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/CloudHandler.kt @@ -12,7 +12,6 @@ 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 @@ -145,7 +144,7 @@ class HandshakeState : MinecraftConnectionState { || addrInfo.isAnyLocalAddress) throw SecurityException("Local addresses aren't allowed") val bootstrap = Bootstrap().handler(BackendInit(handler.user)) - .channel(NioSocketChannel::class.java) + .channelFactory(channelSocketFactory()) .group(handler.user.channel!!.eventLoop()) .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 15_000) // Half of mc timeout .connect(socketAddr) @@ -403,7 +402,7 @@ fun encryptRsa(publicKey: PublicKey, data: ByteArray) = Cipher.getInstance("RSA" it.doFinal(data) } -fun mcCfb8(key: ByteArray, mode: Int) : Cipher { +fun mcCfb8(key: ByteArray, mode: Int): Cipher { val spec = SecretKeySpec(key, "AES") val iv = IvParameterSpec(key) return Cipher.getInstance("AES/CFB8/NoPadding").let { diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/VIAaaS.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/VIAaaS.kt index b5c5142..04c8395 100644 --- a/src/main/kotlin/com/github/creeper123123321/viaaas/VIAaaS.kt +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/VIAaaS.kt @@ -10,11 +10,27 @@ import io.ktor.network.tls.certificates.* import io.ktor.server.engine.* import io.ktor.server.netty.* import io.netty.bootstrap.ServerBootstrap +import io.netty.channel.ChannelFactory import io.netty.channel.ChannelOption +import io.netty.channel.EventLoopGroup +import io.netty.channel.epoll.Epoll +import io.netty.channel.epoll.EpollEventLoopGroup +import io.netty.channel.epoll.EpollServerSocketChannel +import io.netty.channel.epoll.EpollSocketChannel +import io.netty.channel.kqueue.KQueue +import io.netty.channel.kqueue.KQueueEventLoopGroup +import io.netty.channel.kqueue.KQueueServerSocketChannel +import io.netty.channel.kqueue.KQueueSocketChannel import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.ServerSocketChannel +import io.netty.channel.socket.SocketChannel import io.netty.channel.socket.nio.NioServerSocketChannel +import io.netty.channel.socket.nio.NioSocketChannel import io.netty.util.concurrent.Future import net.minecrell.terminalconsole.SimpleTerminalConsole +import org.jline.reader.Candidate +import org.jline.reader.LineReader +import org.jline.reader.LineReaderBuilder import org.slf4j.LoggerFactory import us.myles.ViaVersion.ViaManager import us.myles.ViaVersion.api.Via @@ -23,7 +39,6 @@ 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.* @@ -49,6 +64,30 @@ val mcCryptoKey = KeyPairGenerator.getInstance("RSA").let { it.genKeyPair() } +fun eventLoopGroup(): EventLoopGroup { + if (VIAaaSConfig.isNativeTransportMc) { + if (Epoll.isAvailable()) return EpollEventLoopGroup() + if (KQueue.isAvailable()) return KQueueEventLoopGroup() + } + return NioEventLoopGroup() +} + +fun channelServerSocketFactory(): ChannelFactory { + if (VIAaaSConfig.isNativeTransportMc) { + if (Epoll.isAvailable()) return ChannelFactory { EpollServerSocketChannel() } + if (KQueue.isAvailable()) return ChannelFactory { KQueueServerSocketChannel() } + } + return ChannelFactory { NioServerSocketChannel() } +} + +fun channelSocketFactory(): ChannelFactory { + if (VIAaaSConfig.isNativeTransportMc) { + if (Epoll.isAvailable()) return ChannelFactory { EpollSocketChannel() } + if (KQueue.isAvailable()) return ChannelFactory { KQueueSocketChannel() } + } + return ChannelFactory { NioSocketChannel() } +} + fun main(args: Array) { File("config/https.jks").apply { parentFile.mkdirs() @@ -65,10 +104,12 @@ fun main(args: Array) { CloudRewind.init(ViaRewindConfigImpl(File("config/viarewind.yml"))) CloudBackwards.init(File("config/viabackwards.yml")) - val boss = NioEventLoopGroup() - val worker = NioEventLoopGroup() - val future = ServerBootstrap().group(boss, worker) - .channel(NioServerSocketChannel::class.java) + val parent = eventLoopGroup() + val child = eventLoopGroup() + + val future = ServerBootstrap() + .group(parent, child) + .channelFactory(channelServerSocketFactory()) .childHandler(ChannelInit) .childOption(ChannelOption.IP_TOS, 0x18) .childOption(ChannelOption.TCP_NODELAY, true) @@ -91,28 +132,65 @@ fun main(args: Array) { ktorServer?.stop(1000, 1000) httpClient.close() - listOf>(future.channel().close(), boss.shutdownGracefully(), worker.shutdownGracefully()) + listOf>(future.channel().close(), parent.shutdownGracefully(), child.shutdownGracefully()) .forEach { it.sync() } Via.getManager().destroy() } class VIAaaSConsole : SimpleTerminalConsole(), ViaCommandSender { - val commands = hashMapOf) -> Unit>() + val commands = hashMapOf?, String, Array) -> Unit>() override fun isRunning(): Boolean = runningServer init { - commands["stop"] = { _, _ -> this.shutdown() } + commands["stop"] = { suggestion, _, _ -> if (suggestion == null) this.shutdown() } commands["end"] = commands["stop"]!! - commands["viaversion"] = { _, args -> - Via.getManager().commandHandler.onCommand(this, args) + commands["viaversion"] = { suggestion, _, args -> + if (suggestion == null) { + Via.getManager().commandHandler.onCommand(this, args) + } else { + suggestion.addAll(Via.getManager().commandHandler.onTabComplete(this, args)) + } } commands["viaver"] = commands["viaversion"]!! commands["vvcloud"] = commands["viaversion"]!! - commands["help"] = { _, _ -> - sendMessage(commands.keys.toString()) + commands["help"] = { suggestion , _, _ -> + if (suggestion == null) sendMessage(commands.keys.toString()) } commands["?"] = commands["help"]!! + commands["list"] = { suggestion, _, _ -> + if (suggestion == null) { + Via.getPlatform().connectionManager.connections.forEach { + sendMessage("${it.channel?.remoteAddress()} (${it.protocolInfo?.protocolVersion}) -> " + + "(${it.protocolInfo?.serverProtocolVersion}) " + + "${it.channel?.pipeline()?.get(CloudMinecraftHandler::class.java)?.other?.remoteAddress()}") + } + } + } + } + + override fun buildReader(builder: LineReaderBuilder): LineReader { + // Stolen from Velocity + return super.buildReader(builder.appName("VIAaaS").completer { _, line, candidates -> + try { + val cmdArgs = line.line().substring(0, line.cursor()).split(" ") + val alias = cmdArgs[0] + val args = cmdArgs.filterIndexed { i, _ -> i > 0 } + if (cmdArgs.size == 1) { + candidates.addAll(commands.keys.filter { it.startsWith(alias, ignoreCase = true) } + .map { Candidate(it) }) + } else { + val cmd = commands[alias.toLowerCase()] + if (cmd != null) { + val suggestions = mutableListOf() + cmd(suggestions, alias, args.toTypedArray()) + candidates.addAll(suggestions.map(::Candidate)) + } + } + } catch (e: Exception) { + sendMessage("Error completing command: $e") + } + }) } override fun runCommand(command: String) { @@ -124,7 +202,7 @@ class VIAaaSConsole : SimpleTerminalConsole(), ViaCommandSender { if (runnable == null) { sendMessage("unknown command, try 'help'") } else { - runnable(alias, args) + runnable(null, alias, args) } } catch (e: Exception) { sendMessage("Error running command: $e") @@ -160,6 +238,7 @@ object VIAaaSConfig : Config(File("config/viaaas.yml")) { override fun handleConfig(p0: MutableMap?) { } + val isNativeTransportMc: Boolean get() = this.getBoolean("native-transport-mc", true) 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")!! @@ -171,7 +250,7 @@ class VIAaaSAddress { var realAddress: String? = null var port: Int? = null var online = true - var altUsername : String? = null + var altUsername: String? = null fun parse(address: String, viaHostName: String): VIAaaSAddress { val parts = address.split('.') var foundDomain = false diff --git a/src/main/resources/viaaas.yml b/src/main/resources/viaaas.yml index cf36a3f..e5a97f9 100644 --- a/src/main/resources/viaaas.yml +++ b/src/main/resources/viaaas.yml @@ -1,7 +1,10 @@ -# See application.conf in resources for https interface options +## CHANGING THIS CONFIG AT RUNTIME ISN'T SUPPORTED +## See application.conf in resources for https interface options # Port used for binding Minecraft port port: 25565 # Address to bind bind-address: localhost # Host name of this instance, that will be used in the virtual host -host-name: viaaas.localhost \ No newline at end of file +host-name: viaaas.localhost +# Use netty native transport for Minecraft when available. +native-transport-mc: true \ No newline at end of file diff --git a/src/main/resources/web/auth.html b/src/main/resources/web/auth.html index 4bcab2b..e5b919d 100644 --- a/src/main/resources/web/auth.html +++ b/src/main/resources/web/auth.html @@ -93,9 +93,11 @@ function storeMcAccount(accessToken, clientToken, name, id) { let accounts = JSON.parse(localStorage.getItem("mc_accounts")) || []; - accounts.push({accessToken: accessToken, clientToken: clientToken, name: name, id: id}); + let account = {accessToken: accessToken, clientToken: clientToken, name: name, id: id}; + accounts.push(account); localStorage.setItem("mc_accounts", JSON.stringify(accounts)); refreshAccountList(); + return account; } function removeMcAccount(id) { @@ -149,42 +151,43 @@ getMcAccounts().forEach(it => addMcAccountToList(it.id, it.name)); } - function refreshAccountsIfNeeded() { - getMcAccounts().forEach(it => { + function refreshAccountIfNeeded(it, doneCallback, failCallback) { + $.ajax({type: "post", + url: localStorage.getItem("cors-proxy") + "https://authserver.mojang.com/validate", + data: JSON.stringify({ + accessToken: it.accessToken, + clientToken: it.clientToken + }), + contentType: "application/json", + dataType: "json" + }) + .done(() => doneCallback(it)) + .fail(() => { + // Needs refresh + console.log("refreshing " + it.id); $.ajax({type: "post", - url: localStorage.getItem("cors-proxy") + "https://authserver.mojang.com/validate", + url: localStorage.getItem("cors-proxy") + "https://authserver.mojang.com/refresh", data: JSON.stringify({ accessToken: it.accessToken, clientToken: it.clientToken }), contentType: "application/json", dataType: "json" + }).done((data) => { + console.log("refreshed " + data.selectedProfile.id); + removeMcAccount(data.selectedProfile.id); + doneCallback(storeMcAccount(data.accessToken, data.clientToken, data.selectedProfile.name, data.selectedProfile.id)); }).fail(() => { - // Needs refresh - $.ajax({type: "post", - url: localStorage.getItem("cors-proxy") + "https://authserver.mojang.com/refresh", - data: JSON.stringify({ - accessToken: it.accessToken, - clientToken: it.clientToken - }), - contentType: "application/json", - dataType: "json" - }).done((data) => { - removeMcAccount(data.selectedProfile.id); - storeMcAccount(data.accessToken, data.clientToken, data.selectedProfile.name, data.selectedProfile.id); - }).fail(() => { - if (confirm("failed to refresh token! remove account?")) { - removeMcAccount(it.id); - } - }); + if (confirm("failed to refresh token! remove account?")) { + removeMcAccount(it.id); + } + failCallback(); }); }); } refreshAccountList(); - refreshAccountsIfNeeded(); - function listen(token) { socket.send(JSON.stringify({"action": "listen_login_requests", "token": token})); } @@ -282,22 +285,28 @@ } } else if (parsed.action == "session_hash_request") { if (confirm("Confirm auth request sent from VIAaaS instance? info: " + event.data)) { - let accounts = getMcAccounts().filter(it => it.user.toLowerCase() == parsed.user.toLowerCase()); + let accounts = getMcAccounts().filter(it => it.name.toLowerCase() == parsed.user.toLowerCase()); accounts.forEach(it => { - $.ajax({type: "post", - url: localStorage.getItem("cors-proxy") + "https://sessionserver.mojang.com/session/minecraft/join", - data: JSON.stringify({ - accessToken: it.accessToken, - selectedProfile: it.id, - serverId: parsed.session_hash - }), - contentType: "application/json", - dataType: "json" - }).done((data) => { - confirmJoin(parsed.session_hash); - }).fail((e) => { - console.log(e); - alert("Failed to authenticate to Minecraft backend server!"); + refreshAccountIfNeeded(it, (data) => { + $.ajax({type: "post", + url: localStorage.getItem("cors-proxy") + "https://sessionserver.mojang.com/session/minecraft/join", + data: JSON.stringify({ + accessToken: data.accessToken, + selectedProfile: data.id, + serverId: parsed.session_hash + }), + contentType: "application/json", + dataType: "json" + }).done((data) => { + confirmJoin(parsed.session_hash); + }).fail((e) => { + console.log(e); + alert("Failed to authenticate to Minecraft backend server!"); + }); + }, () => { + if (confirm("Couldn't refresh " + parsed.user + " account in browser. Continue without authentication (works on LAN worlds)?")) { + confirmJoin(parsed.session_hash); + } }); }); if (accounts.length == 0 && confirm("Couldn't find " + parsed.user + " account in browser. Continue without authentication (works on LAN worlds)?")) {