native transport, refresh account only when used, console command completing

closes #8 closes #9
This commit is contained in:
creeper123123321 2020-11-07 11:43:35 -03:00
parent 7d4ee38392
commit bb7778d9a9
5 changed files with 153 additions and 60 deletions

View File

@ -1,6 +1,7 @@
VIAaaS 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 - 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): Usage for online mode (may block your Mojang account):
- Run the shadow jar or ./gradlew clean run - Run the shadow jar or ./gradlew clean run
- You'll need 2 premium accounts for online mode - You'll need 2 premium accounts for online mode (using only one account is possible but, as only one access tokens
- 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)). 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. - 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. - 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 - Connect to mc.example.com._v1_8.viaaas._u(BACKUSERNAME).localhost

View File

@ -12,7 +12,6 @@ import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelOption import io.netty.channel.ChannelOption
import io.netty.channel.SimpleChannelInboundHandler import io.netty.channel.SimpleChannelInboundHandler
import io.netty.channel.socket.SocketChannel import io.netty.channel.socket.SocketChannel
import io.netty.channel.socket.nio.NioSocketChannel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -145,7 +144,7 @@ class HandshakeState : MinecraftConnectionState {
|| addrInfo.isAnyLocalAddress) throw SecurityException("Local addresses aren't allowed") || addrInfo.isAnyLocalAddress) throw SecurityException("Local addresses aren't allowed")
val bootstrap = Bootstrap().handler(BackendInit(handler.user)) val bootstrap = Bootstrap().handler(BackendInit(handler.user))
.channel(NioSocketChannel::class.java) .channelFactory(channelSocketFactory())
.group(handler.user.channel!!.eventLoop()) .group(handler.user.channel!!.eventLoop())
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 15_000) // Half of mc timeout .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 15_000) // Half of mc timeout
.connect(socketAddr) .connect(socketAddr)
@ -403,7 +402,7 @@ fun encryptRsa(publicKey: PublicKey, data: ByteArray) = Cipher.getInstance("RSA"
it.doFinal(data) it.doFinal(data)
} }
fun mcCfb8(key: ByteArray, mode: Int) : Cipher { fun mcCfb8(key: ByteArray, mode: Int): Cipher {
val spec = SecretKeySpec(key, "AES") val spec = SecretKeySpec(key, "AES")
val iv = IvParameterSpec(key) val iv = IvParameterSpec(key)
return Cipher.getInstance("AES/CFB8/NoPadding").let { return Cipher.getInstance("AES/CFB8/NoPadding").let {

View File

@ -10,11 +10,27 @@ import io.ktor.network.tls.certificates.*
import io.ktor.server.engine.* import io.ktor.server.engine.*
import io.ktor.server.netty.* import io.ktor.server.netty.*
import io.netty.bootstrap.ServerBootstrap import io.netty.bootstrap.ServerBootstrap
import io.netty.channel.ChannelFactory
import io.netty.channel.ChannelOption 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.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.NioServerSocketChannel
import io.netty.channel.socket.nio.NioSocketChannel
import io.netty.util.concurrent.Future import io.netty.util.concurrent.Future
import net.minecrell.terminalconsole.SimpleTerminalConsole 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 org.slf4j.LoggerFactory
import us.myles.ViaVersion.ViaManager import us.myles.ViaVersion.ViaManager
import us.myles.ViaVersion.api.Via 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.api.protocol.ProtocolVersion
import us.myles.ViaVersion.util.Config import us.myles.ViaVersion.util.Config
import java.io.File import java.io.File
import java.lang.IllegalArgumentException
import java.net.InetAddress import java.net.InetAddress
import java.security.KeyPairGenerator import java.security.KeyPairGenerator
import java.util.* import java.util.*
@ -49,6 +64,30 @@ val mcCryptoKey = KeyPairGenerator.getInstance("RSA").let {
it.genKeyPair() it.genKeyPair()
} }
fun eventLoopGroup(): EventLoopGroup {
if (VIAaaSConfig.isNativeTransportMc) {
if (Epoll.isAvailable()) return EpollEventLoopGroup()
if (KQueue.isAvailable()) return KQueueEventLoopGroup()
}
return NioEventLoopGroup()
}
fun channelServerSocketFactory(): ChannelFactory<ServerSocketChannel> {
if (VIAaaSConfig.isNativeTransportMc) {
if (Epoll.isAvailable()) return ChannelFactory { EpollServerSocketChannel() }
if (KQueue.isAvailable()) return ChannelFactory { KQueueServerSocketChannel() }
}
return ChannelFactory { NioServerSocketChannel() }
}
fun channelSocketFactory(): ChannelFactory<SocketChannel> {
if (VIAaaSConfig.isNativeTransportMc) {
if (Epoll.isAvailable()) return ChannelFactory { EpollSocketChannel() }
if (KQueue.isAvailable()) return ChannelFactory { KQueueSocketChannel() }
}
return ChannelFactory { NioSocketChannel() }
}
fun main(args: Array<String>) { fun main(args: Array<String>) {
File("config/https.jks").apply { File("config/https.jks").apply {
parentFile.mkdirs() parentFile.mkdirs()
@ -65,10 +104,12 @@ fun main(args: Array<String>) {
CloudRewind.init(ViaRewindConfigImpl(File("config/viarewind.yml"))) CloudRewind.init(ViaRewindConfigImpl(File("config/viarewind.yml")))
CloudBackwards.init(File("config/viabackwards.yml")) CloudBackwards.init(File("config/viabackwards.yml"))
val boss = NioEventLoopGroup() val parent = eventLoopGroup()
val worker = NioEventLoopGroup() val child = eventLoopGroup()
val future = ServerBootstrap().group(boss, worker)
.channel(NioServerSocketChannel::class.java) val future = ServerBootstrap()
.group(parent, child)
.channelFactory(channelServerSocketFactory())
.childHandler(ChannelInit) .childHandler(ChannelInit)
.childOption(ChannelOption.IP_TOS, 0x18) .childOption(ChannelOption.IP_TOS, 0x18)
.childOption(ChannelOption.TCP_NODELAY, true) .childOption(ChannelOption.TCP_NODELAY, true)
@ -91,28 +132,65 @@ fun main(args: Array<String>) {
ktorServer?.stop(1000, 1000) ktorServer?.stop(1000, 1000)
httpClient.close() httpClient.close()
listOf<Future<*>>(future.channel().close(), boss.shutdownGracefully(), worker.shutdownGracefully()) listOf<Future<*>>(future.channel().close(), parent.shutdownGracefully(), child.shutdownGracefully())
.forEach { it.sync() } .forEach { it.sync() }
Via.getManager().destroy() Via.getManager().destroy()
} }
class VIAaaSConsole : SimpleTerminalConsole(), ViaCommandSender { class VIAaaSConsole : SimpleTerminalConsole(), ViaCommandSender {
val commands = hashMapOf<String, (String, Array<String>) -> Unit>() val commands = hashMapOf<String, (MutableList<String>?, String, Array<String>) -> Unit>()
override fun isRunning(): Boolean = runningServer override fun isRunning(): Boolean = runningServer
init { init {
commands["stop"] = { _, _ -> this.shutdown() } commands["stop"] = { suggestion, _, _ -> if (suggestion == null) this.shutdown() }
commands["end"] = commands["stop"]!! commands["end"] = commands["stop"]!!
commands["viaversion"] = { _, args -> commands["viaversion"] = { suggestion, _, args ->
Via.getManager().commandHandler.onCommand(this, args) if (suggestion == null) {
Via.getManager().commandHandler.onCommand(this, args)
} else {
suggestion.addAll(Via.getManager().commandHandler.onTabComplete(this, args))
}
} }
commands["viaver"] = commands["viaversion"]!! commands["viaver"] = commands["viaversion"]!!
commands["vvcloud"] = commands["viaversion"]!! commands["vvcloud"] = commands["viaversion"]!!
commands["help"] = { _, _ -> commands["help"] = { suggestion , _, _ ->
sendMessage(commands.keys.toString()) if (suggestion == null) sendMessage(commands.keys.toString())
} }
commands["?"] = commands["help"]!! 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<String>()
cmd(suggestions, alias, args.toTypedArray())
candidates.addAll(suggestions.map(::Candidate))
}
}
} catch (e: Exception) {
sendMessage("Error completing command: $e")
}
})
} }
override fun runCommand(command: String) { override fun runCommand(command: String) {
@ -124,7 +202,7 @@ class VIAaaSConsole : SimpleTerminalConsole(), ViaCommandSender {
if (runnable == null) { if (runnable == null) {
sendMessage("unknown command, try 'help'") sendMessage("unknown command, try 'help'")
} else { } else {
runnable(alias, args) runnable(null, alias, args)
} }
} catch (e: Exception) { } catch (e: Exception) {
sendMessage("Error running command: $e") sendMessage("Error running command: $e")
@ -160,6 +238,7 @@ object VIAaaSConfig : Config(File("config/viaaas.yml")) {
override fun handleConfig(p0: MutableMap<String, Any>?) { override fun handleConfig(p0: MutableMap<String, Any>?) {
} }
val isNativeTransportMc: Boolean get() = this.getBoolean("native-transport-mc", true)
val port: Int get() = this.getInt("port", 25565) val port: Int get() = this.getInt("port", 25565)
val bindAddress: String get() = this.getString("bind-address", "localhost")!! val bindAddress: String get() = this.getString("bind-address", "localhost")!!
val hostName: String get() = this.getString("host-name", "viaaas.localhost")!! val hostName: String get() = this.getString("host-name", "viaaas.localhost")!!
@ -171,7 +250,7 @@ class VIAaaSAddress {
var realAddress: String? = null var realAddress: String? = null
var port: Int? = null var port: Int? = null
var online = true var online = true
var altUsername : String? = null var altUsername: String? = null
fun parse(address: String, viaHostName: String): VIAaaSAddress { fun parse(address: String, viaHostName: String): VIAaaSAddress {
val parts = address.split('.') val parts = address.split('.')
var foundDomain = false var foundDomain = false

View File

@ -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 used for binding Minecraft port
port: 25565 port: 25565
# Address to bind # Address to bind
bind-address: localhost bind-address: localhost
# Host name of this instance, that will be used in the virtual host # Host name of this instance, that will be used in the virtual host
host-name: viaaas.localhost host-name: viaaas.localhost
# Use netty native transport for Minecraft when available.
native-transport-mc: true

View File

@ -93,9 +93,11 @@
function storeMcAccount(accessToken, clientToken, name, id) { function storeMcAccount(accessToken, clientToken, name, id) {
let accounts = JSON.parse(localStorage.getItem("mc_accounts")) || []; 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)); localStorage.setItem("mc_accounts", JSON.stringify(accounts));
refreshAccountList(); refreshAccountList();
return account;
} }
function removeMcAccount(id) { function removeMcAccount(id) {
@ -149,42 +151,43 @@
getMcAccounts().forEach(it => addMcAccountToList(it.id, it.name)); getMcAccounts().forEach(it => addMcAccountToList(it.id, it.name));
} }
function refreshAccountsIfNeeded() { function refreshAccountIfNeeded(it, doneCallback, failCallback) {
getMcAccounts().forEach(it => { $.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", $.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({ data: JSON.stringify({
accessToken: it.accessToken, accessToken: it.accessToken,
clientToken: it.clientToken clientToken: it.clientToken
}), }),
contentType: "application/json", contentType: "application/json",
dataType: "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(() => { }).fail(() => {
// Needs refresh if (confirm("failed to refresh token! remove account?")) {
$.ajax({type: "post", removeMcAccount(it.id);
url: localStorage.getItem("cors-proxy") + "https://authserver.mojang.com/refresh", }
data: JSON.stringify({ failCallback();
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);
}
});
}); });
}); });
} }
refreshAccountList(); refreshAccountList();
refreshAccountsIfNeeded();
function listen(token) { function listen(token) {
socket.send(JSON.stringify({"action": "listen_login_requests", "token": token})); socket.send(JSON.stringify({"action": "listen_login_requests", "token": token}));
} }
@ -282,22 +285,28 @@
} }
} else if (parsed.action == "session_hash_request") { } else if (parsed.action == "session_hash_request") {
if (confirm("Confirm auth request sent from VIAaaS instance? info: " + event.data)) { 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 => { accounts.forEach(it => {
$.ajax({type: "post", refreshAccountIfNeeded(it, (data) => {
url: localStorage.getItem("cors-proxy") + "https://sessionserver.mojang.com/session/minecraft/join", $.ajax({type: "post",
data: JSON.stringify({ url: localStorage.getItem("cors-proxy") + "https://sessionserver.mojang.com/session/minecraft/join",
accessToken: it.accessToken, data: JSON.stringify({
selectedProfile: it.id, accessToken: data.accessToken,
serverId: parsed.session_hash selectedProfile: data.id,
}), serverId: parsed.session_hash
contentType: "application/json", }),
dataType: "json" contentType: "application/json",
}).done((data) => { dataType: "json"
confirmJoin(parsed.session_hash); }).done((data) => {
}).fail((e) => { confirmJoin(parsed.session_hash);
console.log(e); }).fail((e) => {
alert("Failed to authenticate to Minecraft backend server!"); 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)?")) { if (accounts.length == 0 && confirm("Couldn't find " + parsed.user + " account in browser. Continue without authentication (works on LAN worlds)?")) {