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