diff --git a/auth.html b/auth.html deleted file mode 100644 index ff1b20a..0000000 --- a/auth.html +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/build.gradle.kts b/build.gradle.kts index ac2a780..4b0c4f0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -23,12 +23,21 @@ repositories { } dependencies { - implementation("us.myles:viaversion:3.1.1-SNAPSHOT") - implementation("nl.matsv:viabackwards-all:3.1.1-SNAPSHOT") + implementation("us.myles:viaversion:3.1.1") + implementation("nl.matsv:viabackwards-all:3.1.1") implementation("de.gerrygames:viarewind-all:1.5.1") + implementation("net.md-5:bungeecord-chat:1.16-R0.3") implementation("io.netty:netty-all:4.1.51.Final") implementation(kotlin("stdlib-jdk8")) + + val ktorVersion = "1.4.0" + + implementation("io.ktor:ktor-network-tls-certificates:$ktorVersion") + implementation("io.ktor:ktor-server-netty:$ktorVersion") + implementation("io.ktor:ktor-websockets:$ktorVersion") + implementation("ch.qos.logback:logback-classic:1.2.3") + testCompile("io.ktor:ktor-server-test-host:$ktorVersion") } val run: JavaExec by tasks -run.standardInput = System.`in` +run.standardInput = System.`in` \ No newline at end of file diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/CloudCodec.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/CloudCodec.kt index 62c5347..5d1961d 100644 --- a/src/main/kotlin/com/github/creeper123123321/viaaas/CloudCodec.kt +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/CloudCodec.kt @@ -24,25 +24,25 @@ object ChannelInit : ChannelInitializer() { override fun initChannel(ch: Channel) { val user = UserConnection(ch) CloudPipeline(user) - ch.pipeline().addLast("frame-encoder", FrameEncoder) + ch.pipeline().addLast("timeout", ReadTimeoutHandler(30, TimeUnit.SECONDS)) + .addLast("frame-encoder", FrameEncoder) .addLast("frame-decoder", FrameDecoder()) .addLast("compress", CloudCompressor()) .addLast("decompress", CloudDecompressor()) + .addLast("flow-handler", FlowControlHandler()) .addLast("via-encoder", CloudEncodeHandler(user)) .addLast("via-decoder", CloudDecodeHandler(user)) - .addLast("flow-handler", FlowControlHandler()) - .addLast("timeout", ReadTimeoutHandler(30, TimeUnit.SECONDS)) .addLast("handler", CloudSideForwarder(user, null)) } } class BackendInit(val user: UserConnection) : ChannelInitializer() { override fun initChannel(ch: Channel) { - ch.pipeline().addLast("frame-encoder", FrameEncoder) + ch.pipeline().addLast("timeout", ReadTimeoutHandler(30, TimeUnit.SECONDS)) + .addLast("frame-encoder", FrameEncoder) .addLast("frame-decoder", FrameDecoder()) .addLast("compress", CloudCompressor()) .addLast("decompress", CloudDecompressor()) - .addLast("timeout", ReadTimeoutHandler(30, TimeUnit.SECONDS)) .addLast("handler", CloudSideForwarder(user, null)) } } @@ -57,7 +57,7 @@ class CloudDecompressor(var threshold: Int = -1) : MessageToMessageDecoder() { +class CloudSideForwarder(val userConnection: UserConnection, var other: Channel?) : SimpleChannelInboundHandler() { override fun channelRead0(ctx: ChannelHandlerContext, msg: ByteBuf) { - other?.write(msg.retain()) + if (!userConnection.isPendingDisconnect) { + other!!.write(msg.retain()) + } } override fun channelInactive(ctx: ChannelHandlerContext) { @@ -39,17 +41,17 @@ class CloudSideForwarder(val userConnection: UserConnection, var other: Channel? cause.printStackTrace() } + fun disconnect(s: String) { if (userConnection.channel?.isActive != true) return - val msg = "[VIAaaS] $s"; logger.info("Disconnecting " + userConnection.channel!!.remoteAddress() + ": " + s) when (userConnection.protocolInfo!!.state) { State.LOGIN -> { val packet = ByteBufAllocator.DEFAULT.buffer() try { packet.writeByte(0) // id 0 disconnect - Type.STRING.write(packet, Gson().toJson(msg)) + Type.STRING.write(packet, Gson().toJson("[VIAaaS] §c$s")) userConnection.sendRawPacketFuture(packet.retain()).addListener { userConnection.channel?.close() } } finally { packet.release() @@ -60,8 +62,7 @@ class CloudSideForwarder(val userConnection: UserConnection, var other: Channel? 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(msg)}}}""") +"players": {"max": 0,"online": 0,"sample": []},"description": {"text": ${Gson().toJson("§c$s")}}}""") userConnection.sendRawPacketFuture(packet.retain()).addListener { userConnection.channel?.close() } } finally { packet.release() diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/CloudProtocol.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/CloudProtocol.kt index 259ebee..66ec9dc 100644 --- a/src/main/kotlin/com/github/creeper123123321/viaaas/CloudProtocol.kt +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/CloudProtocol.kt @@ -3,18 +3,25 @@ package com.github.creeper123123321.viaaas import io.netty.bootstrap.Bootstrap import io.netty.buffer.ByteBufAllocator import io.netty.channel.Channel +import io.netty.channel.ChannelOption import io.netty.channel.socket.SocketChannel import io.netty.channel.socket.nio.NioSocketChannel import us.myles.ViaVersion.api.PacketWrapper import us.myles.ViaVersion.api.Via import us.myles.ViaVersion.api.data.UserConnection -import us.myles.ViaVersion.api.protocol.* +import us.myles.ViaVersion.api.protocol.Protocol +import us.myles.ViaVersion.api.protocol.ProtocolPipeline +import us.myles.ViaVersion.api.protocol.ProtocolRegistry +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.net.InetAddress import java.net.InetSocketAddress import java.util.logging.Logger +import javax.naming.NameNotFoundException +import javax.naming.directory.InitialDirContext + class CloudPipeline(userConnection: UserConnection) : ProtocolPipeline(userConnection) { override fun registerPackets() { @@ -35,63 +42,42 @@ object CloudHandlerProtocol : SimpleProtocol() { this.registerIncoming(State.HANDSHAKE, 0, 0, object : PacketRemapper() { override fun registerMap() { handler { wrapper: PacketWrapper -> + wrapper.cancel() val playerVer = wrapper.passthrough(Type.VAR_INT) val addr = wrapper.passthrough(Type.STRING) // Server Address wrapper.passthrough(Type.UNSIGNED_SHORT) val nextState = wrapper.passthrough(Type.VAR_INT) - val addrParts = addr.split(0.toChar())[0].split(".") - var foundDomain = false - var foundOptions = false - var port = 25565 - var online = true // todo implement this between proxy and player - var backProtocol = 47 // todo auto protocol - var backAddr = "" - addrParts.reversed().forEach { - if (foundDomain) { - if (!foundOptions) { - if (it.startsWith("_")) { - val arg = it.substring(2) - when { - it.startsWith("_p", ignoreCase = true) -> port = arg.toInt() - it.startsWith("_o", ignoreCase = true) -> online = arg.toBoolean() - it.startsWith("_v", ignoreCase = true) -> { - try { - backProtocol = Integer.parseInt(arg) - } catch (e: NumberFormatException) { - val closest = ProtocolVersion.getClosest(arg.replace("_", ".")) - if (closest != null) { - backProtocol = closest.id - } - } - } - } - } else { - foundOptions = true - } - } - if (foundOptions) { - backAddr = "$it.$backAddr" - } - } else if (it.equals("viaaas", ignoreCase = true)) { - foundDomain = true - } - } - backAddr = backAddr.replace(Regex("\\.$"), "") + val parsed = ViaaaSAddress().parse(addr) - logger.info("connecting ${wrapper.user().channel!!.remoteAddress()} ($playerVer) to $backAddr:$port ($backProtocol)") + logger.info("connecting ${wrapper.user().channel!!.remoteAddress()} ($playerVer) to ${parsed.realAddress}:${parsed.port} (${parsed.protocol})") wrapper.user().channel!!.setAutoRead(false) wrapper.user().put(CloudData( - backendVer = backProtocol, + backendVer = parsed.protocol, userConnection = wrapper.user(), - frontOnline = online + frontOnline = parsed.online )) Via.getPlatform().runAsync { val frontForwarder = wrapper.user().channel!!.pipeline().get(CloudSideForwarder::class.java) try { - val socketAddr = InetSocketAddress(InetAddress.getByName(backAddr), port) + var srvResolvedAddr = parsed.realAddress + var srvResolvedPort = parsed.port + if (srvResolvedPort == 25565) { + try { + // https://github.com/GeyserMC/Geyser/blob/99e72f35b308542cf0dbfb5b58816503c3d6a129/connector/src/main/java/org/geysermc/connector/GeyserConnector.java + val ctx = InitialDirContext() + val attr = ctx.getAttributes("dns:///_minecraft._tcp.${parsed.realAddress}", arrayOf("SRV"))["SRV"] + if (attr != null && attr.size() > 0) { + val record = (attr.get(0) as String).split(" ").toTypedArray() + srvResolvedAddr = record[3] + srvResolvedPort = record[2].toInt() + } + } catch (ignored: NameNotFoundException) { + } + } + val socketAddr = InetSocketAddress(InetAddress.getByName(srvResolvedAddr), srvResolvedPort) val addrInfo = socketAddr.address if (addrInfo.isSiteLocalAddress || addrInfo.isLoopbackAddress @@ -100,6 +86,7 @@ object CloudHandlerProtocol : SimpleProtocol() { val bootstrap = Bootstrap().handler(BackendInit(wrapper.user())) .channel(NioSocketChannel::class.java) .group(wrapper.user().channel!!.eventLoop()) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 15_000) // Half of mc timeout .connect(socketAddr) bootstrap.addListener { @@ -110,28 +97,23 @@ object CloudHandlerProtocol : SimpleProtocol() { frontForwarder.other = chann val backHandshake = ByteBufAllocator.DEFAULT.buffer() try { + val nullParts = addr.split(0.toChar()) backHandshake.writeByte(0) // Packet 0 handshake - val connProto = - if (ProtocolRegistry.getProtocolPath(playerVer, backProtocol) != null) { - backProtocol - } else playerVer + val connProto = if (ProtocolRegistry.getProtocolPath(playerVer, parsed.protocol) != null) parsed.protocol else playerVer Type.VAR_INT.writePrimitive(backHandshake, connProto) - val nullPos = addr.indexOf(0.toChar()) - Type.STRING.write(backHandshake, backAddr - + (if (nullPos != -1) addr.substring(nullPos) else "")) // Server Address - backHandshake.writeShort(port) + Type.STRING.write(backHandshake, srvResolvedAddr + (if (nullParts.size == 2) 0.toChar() + nullParts[1] else "")) // Server Address + backHandshake.writeShort(srvResolvedPort) Type.VAR_INT.writePrimitive(backHandshake, nextState) chann.writeAndFlush(backHandshake.retain()) } finally { backHandshake.release() } + wrapper.user().channel!!.setAutoRead(true) } else { wrapper.user().channel!!.eventLoop().submit { frontForwarder.disconnect("Couldn't connect: " + it.cause().toString()) } } - - wrapper.user().channel!!.setAutoRead(true) } } catch (e: Exception) { wrapper.user().channel!!.eventLoop().submit { @@ -156,9 +138,9 @@ object CloudHandlerProtocol : SimpleProtocol() { pipe.get(CloudCompressor::class.java).threshold = threshold pipe.get(CloudDecompressor::class.java).threshold = threshold - val backPipe = pipe.get(CloudSideForwarder::class.java).other?.pipeline() - backPipe?.get(CloudCompressor::class.java)?.threshold = threshold - backPipe?.get(CloudDecompressor::class.java)?.threshold = threshold + val backPipe = pipe.get(CloudSideForwarder::class.java).other!!.pipeline() + backPipe.get(CloudCompressor::class.java)?.threshold = threshold + backPipe.get(CloudDecompressor::class.java)?.threshold = threshold } } }) diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/ViaaaS.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/ViaaaS.kt index d618986..42829f7 100644 --- a/src/main/kotlin/com/github/creeper123123321/viaaas/ViaaaS.kt +++ b/src/main/kotlin/com/github/creeper123123321/viaaas/ViaaaS.kt @@ -1,16 +1,31 @@ package com.github.creeper123123321.viaaas import de.gerrygames.viarewind.api.ViaRewindConfigImpl +import io.ktor.application.* +import io.ktor.features.* +import io.ktor.http.cio.websocket.* +import io.ktor.http.content.* +import io.ktor.network.tls.certificates.* +import io.ktor.routing.* +import io.ktor.server.netty.* +import io.ktor.websocket.* import io.netty.bootstrap.ServerBootstrap import io.netty.channel.nio.NioEventLoopGroup import io.netty.channel.socket.nio.NioServerSocketChannel +import kotlinx.coroutines.channels.consumeEach import us.myles.ViaVersion.ViaManager import us.myles.ViaVersion.api.Via import us.myles.ViaVersion.api.data.MappingDataLoader +import us.myles.ViaVersion.api.protocol.ProtocolVersion import java.io.File +import java.net.InetAddress +import java.time.Duration +import java.util.concurrent.ConcurrentHashMap import kotlin.system.exitProcess -fun main() { + +fun main(args: Array) { + val args = args.mapIndexed { i, content -> i to content }.toMap() Via.init(ViaManager.builder() .injector(CloudInjector) .loader(CloudLoader) @@ -30,9 +45,11 @@ fun main() { val future = ServerBootstrap().group(boss, worker) .channel(NioServerSocketChannel::class.java) .childHandler(ChannelInit) - .bind(25565) - .addListener { println(it) } + .bind(InetAddress.getByName(args[0] ?: "::"), args[1]?.toIntOrNull() ?: 25565) + println("Binded minecraft into " + future.sync().channel().localAddress()) + + Thread { EngineMain.main(arrayOf()) }.start() loop@ while (true) { try { @@ -55,3 +72,155 @@ fun main() { exitProcess(0) // todo what's stucking? } +class ViaaaSAddress { + var protocol = 0 + var viaSuffix: String? = null + var realAddress: String? = null + var port: Int = 25565 + var online: Boolean = false + fun parse(address: String): ViaaaSAddress { + val parts = address.split('.') + var foundDomain = false + var foundOptions = false + val ourParts = StringBuilder() + val realAddrBuilder = StringBuilder() + for (i in parts.indices.reversed()) { + val part = parts[i] + var realAddrPart = false + if (foundDomain) { + if (!foundOptions) { + if (part.startsWith("_")) { + val arg = part.substring(2) + when { + part.startsWith("_p", ignoreCase = true) -> port = arg.toInt() + part.startsWith("_o", ignoreCase = true) -> online = arg.toBoolean() + part.startsWith("_v", ignoreCase = true) -> { + try { + protocol = arg.toInt() + } catch (e: NumberFormatException) { + val closest = ProtocolVersion.getClosest(arg.replace("_", ".")) + if (closest != null) { + protocol = closest.id + } + } + } + } + } else { + foundOptions = true + } + } + if (foundOptions) { + realAddrPart = true + } + } else if (part.equals("viaaas", ignoreCase = true)) { + foundDomain = true + } + if (realAddrPart) { + realAddrBuilder.insert(0, "$part.") + } else { + ourParts.insert(0, "$part.") + } + } + val realAddr = realAddrBuilder.toString().replace("\\.$".toRegex(), "") + val suffix = ourParts.toString().replace("\\.$".toRegex(), "") + if (realAddr.isEmpty()) { + realAddress = address + } else { + realAddress = realAddr + viaSuffix = suffix + } + return this + } +} + +fun Application.mainWeb() { + ViaWebApp().apply { main() } +} + +class ViaWebApp { + data class WebSession(val id: String) + + val server = WebDashboardServer() + + fun Application.main() { + install(DefaultHeaders) + install(CallLogging) + install(WebSockets) { + pingPeriod = Duration.ofMinutes(1) + } + + routing { + webSocket("/ws") { + server.connected(this) + + try { + incoming.consumeEach { frame -> + if (frame is Frame.Text) { + server.onMessage(this, frame.readText()) + } + } + } finally { + server.disconnected(this) + } + } + + static { + defaultResource("auth.html", "web") + resources("web") + } + + } + } +} + +class WebDashboardServer { + val clients = ConcurrentHashMap() + suspend fun connected(ws: WebSocketSession) { + clients[ws] = WebClient(ws, WebLogin()) + } + + suspend fun onMessage(ws: WebSocketSession, msg: String) { + val client = clients[ws]!! + client.state.onMessage(client, msg) + } + + suspend fun disconnected(ws: WebSocketSession) { + val client = clients[ws]!! + client.state.disconnected(client) + clients.remove(ws) + } +} + + +data class WebClient(val ws: WebSocketSession, val state: WebState) { +} + + +interface WebState { + fun onMessage(webClient: WebClient, msg: String) + fun disconnected(webClient: WebClient) +} + +class WebLogin : WebState { + override fun onMessage(webClient: WebClient, msg: String) { + TODO("Not yet implemented") + } + + override fun disconnected(webClient: WebClient) { + TODO("Not yet implemented") + } +} + + +object CertificateGenerator { + @JvmStatic + fun main(args: Array) { + val jksFile = File("build/temporary.jks").apply { + parentFile.mkdirs() + } + + if (!jksFile.exists()) { + generateCertificate(jksFile) // Generates the certificate + } + } +} diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf new file mode 100644 index 0000000..032ecf5 --- /dev/null +++ b/src/main/resources/application.conf @@ -0,0 +1,19 @@ +# You can read more about this file: https://ktor.io/servers/configuration.html#hocon-file +ktor { + deployment { + sslPort = 8443 + } + + application { + modules = [ com.github.creeper123123321.viaaas.ViaaaSKt.mainWeb ] + } + + security { + ssl { + keyStore = build/temporary.jks + keyAlias = mykey + keyStorePassword = changeit + privateKeyPassword = changeit + } + } +} diff --git a/src/main/resources/web/auth.html b/src/main/resources/web/auth.html new file mode 100644 index 0000000..b376c10 --- /dev/null +++ b/src/main/resources/web/auth.html @@ -0,0 +1,43 @@ + + + + + + + + + \ No newline at end of file