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
---
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

View File

@ -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 {

View File

@ -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<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>) {
File("config/https.jks").apply {
parentFile.mkdirs()
@ -65,10 +104,12 @@ fun main(args: Array<String>) {
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<String>) {
ktorServer?.stop(1000, 1000)
httpClient.close()
listOf<Future<*>>(future.channel().close(), boss.shutdownGracefully(), worker.shutdownGracefully())
listOf<Future<*>>(future.channel().close(), parent.shutdownGracefully(), child.shutdownGracefully())
.forEach { it.sync() }
Via.getManager().destroy()
}
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
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<String>()
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<String, Any>?) {
}
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

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: 25565
# Address to bind
bind-address: localhost
# 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) {
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)?")) {