mirror of
https://github.com/ViaVersion/VIAaaS.git
synced 2024-11-21 11:55:15 +01:00
parent
951000627e
commit
9d4dcb65b5
@ -78,6 +78,7 @@ dependencies {
|
||||
implementation("io.ktor:ktor-websockets:$ktorVersion")
|
||||
testImplementation("io.ktor:ktor-server-test-host:$ktorVersion")
|
||||
implementation("io.ipinfo:ipinfo-api:1.1")
|
||||
implementation("com.auth0:java-jwt:3.15.0")
|
||||
}
|
||||
|
||||
val run: JavaExec by tasks
|
||||
|
@ -1,10 +1,11 @@
|
||||
package com.viaversion.aas
|
||||
|
||||
import com.viaversion.aas.config.VIAaaSConfig
|
||||
import com.google.common.base.Preconditions
|
||||
import com.google.common.net.HostAndPort
|
||||
import com.google.common.net.UrlEscapers
|
||||
import com.google.common.primitives.Ints
|
||||
import com.google.gson.JsonObject
|
||||
import com.viaversion.aas.config.VIAaaSConfig
|
||||
import com.viaversion.aas.util.StacklessException
|
||||
import io.ktor.client.request.*
|
||||
import io.netty.buffer.ByteBuf
|
||||
@ -35,20 +36,20 @@ val viaaasLogger = LoggerFactory.getLogger("VIAaaS")
|
||||
|
||||
val secureRandom = if (VIAaaSConfig.useStrongRandom) SecureRandom.getInstanceStrong() else SecureRandom()
|
||||
|
||||
fun resolveSrv(address: String, port: Int): Pair<String, Int> {
|
||||
if (port == 25565) {
|
||||
fun resolveSrv(hostAndPort: HostAndPort): HostAndPort {
|
||||
if (hostAndPort.port == 25565) {
|
||||
try {
|
||||
// https://github.com/GeyserMC/Geyser/blob/99e72f35b308542cf0dbfb5b58816503c3d6a129/connector/src/main/java/org/geysermc/connector/GeyserConnector.java
|
||||
val attr = InitialDirContext()
|
||||
.getAttributes("dns:///_minecraft._tcp.$address", arrayOf("SRV"))["SRV"]
|
||||
.getAttributes("dns:///_minecraft._tcp.${hostAndPort.host}", arrayOf("SRV"))["SRV"]
|
||||
if (attr != null && attr.size() > 0) {
|
||||
val record = (attr.get(0) as String).split(" ")
|
||||
return record[3] to record[2].toInt()
|
||||
return HostAndPort.fromParts(record[3], record[2].toInt())
|
||||
}
|
||||
} catch (ignored: Exception) { // DuckDNS workaround
|
||||
}
|
||||
}
|
||||
return address to port
|
||||
return hostAndPort
|
||||
}
|
||||
|
||||
fun decryptRsa(privateKey: PrivateKey, data: ByteArray) = Cipher.getInstance("RSA").let {
|
||||
|
@ -1,5 +1,7 @@
|
||||
package com.viaversion.aas
|
||||
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParser
|
||||
import com.viaversion.aas.command.VIAaaSConsole
|
||||
import com.viaversion.aas.command.ViaAspirinCommand
|
||||
import com.viaversion.aas.config.VIAaaSConfig
|
||||
@ -9,7 +11,6 @@ import com.viaversion.aas.platform.*
|
||||
import com.viaversion.aas.protocol.registerAspirinProtocols
|
||||
import com.viaversion.aas.web.ViaWebApp
|
||||
import com.viaversion.aas.web.WebDashboardServer
|
||||
import com.google.gson.JsonParser
|
||||
import de.gerrygames.viarewind.api.ViaRewindConfigImpl
|
||||
import io.ktor.application.*
|
||||
import io.ktor.client.*
|
||||
@ -43,12 +44,14 @@ import us.myles.ViaVersion.api.protocol.ProtocolVersion
|
||||
import java.io.File
|
||||
import java.net.InetAddress
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.SecureRandom
|
||||
import java.util.*
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
val viaaasVer = JsonParser.parseString(
|
||||
AspirinPlatform::class.java.classLoader.getResourceAsStream("viaaas_info.json")!!.reader(Charsets.UTF_8).readText()
|
||||
).asJsonObject.get("version").asString
|
||||
val viaWebServer = WebDashboardServer()
|
||||
var viaWebServer = WebDashboardServer()
|
||||
var serverFinishing = CompletableFuture<Unit>()
|
||||
var finishedFuture = CompletableFuture<Unit>()
|
||||
val httpClient = HttpClient {
|
||||
|
@ -2,6 +2,8 @@ package com.viaversion.aas.config
|
||||
|
||||
import us.myles.ViaVersion.util.Config
|
||||
import java.io.File
|
||||
import java.security.SecureRandom
|
||||
import java.util.*
|
||||
|
||||
object VIAaaSConfig : Config(File("config/viaaas.yml")) {
|
||||
init {
|
||||
@ -10,13 +12,22 @@ object VIAaaSConfig : Config(File("config/viaaas.yml")) {
|
||||
|
||||
override fun getUnsupportedOptions() = emptyList<String>().toMutableList()
|
||||
override fun getDefaultConfigURL() = VIAaaSConfig::class.java.classLoader.getResource("viaaas.yml")!!
|
||||
override fun handleConfig(p0: MutableMap<String, Any>?) {
|
||||
override fun handleConfig(map: MutableMap<String, Any>) {
|
||||
if (map["jwt-secret"]?.toString().isNullOrBlank()) {
|
||||
map["jwt-secret"] = Base64.getEncoder().encodeToString(ByteArray(64)
|
||||
.also { SecureRandom().nextBytes(it) })
|
||||
}
|
||||
|
||||
if (map["host-name"] is String) {
|
||||
map["host-name"] = map["host-name"].toString().split(',').map { it.trim() }
|
||||
}
|
||||
}
|
||||
|
||||
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: List<String> get() = this.getString("host-name", "viaaas.localhost")!!.split(",").map { it.trim() }
|
||||
val hostName: List<String>
|
||||
get() = this.get("host-name", List::class.java, listOf("viaaas.localhost"))!!.map { it.toString() }
|
||||
val mcRsaSize: Int get() = this.getInt("mc-rsa-size", 4096)
|
||||
val useStrongRandom: Boolean get() = this.getBoolean("use-strong-random", true)
|
||||
val blockLocalAddress: Boolean get() = this.getBoolean("block-local-address", true)
|
||||
@ -27,13 +38,13 @@ object VIAaaSConfig : Config(File("config/viaaas.yml")) {
|
||||
"blocked-back-addresses",
|
||||
List::class.java,
|
||||
emptyList<String>()
|
||||
)!!.map { it as String }
|
||||
)!!.map { it.toString() }
|
||||
val allowedBackAddresses: List<String>
|
||||
get() = this.get(
|
||||
"allowed-back-addresses",
|
||||
List::class.java,
|
||||
emptyList<String>()
|
||||
)!!.map { it as String }
|
||||
)!!.map { it.toString() }
|
||||
val forceOnlineMode: Boolean get() = this.getBoolean("force-online-mode", false)
|
||||
val showVersionPing: Boolean get() = this.getBoolean("show-version-ping", true)
|
||||
val showBrandInfo: Boolean get() = this.getBoolean("show-brand-info", true)
|
||||
@ -41,6 +52,10 @@ object VIAaaSConfig : Config(File("config/viaaas.yml")) {
|
||||
val rateLimitConnectionMc: Double get() = this.getDouble("rate-limit-connection-mc", 10.0)
|
||||
val listeningWsLimit: Int get() = this.getInt("listening-ws-limit", 16)
|
||||
val backendSocks5ProxyAddress: String?
|
||||
get() = this.getString("backend-socks5-proxy-address", "")!!.let { if (it.isEmpty()) null else it }
|
||||
get() = this.getString("backend-socks5-proxy-address", "")!!.ifEmpty { null }
|
||||
val backendSocks5ProxyPort: Int get() = this.getInt("backend-socks5-proxy-port", 9050)
|
||||
val jwtSecret: String
|
||||
get() = this.getString("jwt-secret", null).let {
|
||||
if (it.isNullOrBlank()) throw IllegalStateException("invalid jwt-secret") else it
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package com.viaversion.aas.handler.state
|
||||
|
||||
import com.google.common.net.HostAndPort
|
||||
import com.viaversion.aas.*
|
||||
import com.viaversion.aas.config.VIAaaSConfig
|
||||
import com.viaversion.aas.handler.BackEndInit
|
||||
@ -93,19 +94,19 @@ fun connectBack(handler: MinecraftHandler, address: String, port: Int, state: St
|
||||
handler.data.frontChannel.setAutoRead(false)
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val srvResolved = resolveSrv(address, port)
|
||||
val srvResolved = resolveSrv(HostAndPort.fromParts(address, port))
|
||||
|
||||
val removedEndDot = srvResolved.first.replace(Regex("\\.$"), "")
|
||||
val removedEndDot = srvResolved.host.replace(Regex("\\.$"), "")
|
||||
|
||||
val iterator =
|
||||
if (!removedEndDot.endsWith(".onion")) {
|
||||
InetAddress.getAllByName(srvResolved.first)
|
||||
InetAddress.getAllByName(srvResolved.host)
|
||||
.groupBy { it is Inet4Address }
|
||||
.toSortedMap() // I'm sorry, IPv4, but my true love is IPv6... We can still be friends though...
|
||||
.map { InetSocketAddress(it.value.random(), srvResolved.second) }
|
||||
.map { InetSocketAddress(it.value.random(), srvResolved.port) }
|
||||
.iterator()
|
||||
} else {
|
||||
listOf(InetSocketAddress.createUnresolved(removedEndDot, srvResolved.second)).iterator()
|
||||
listOf(InetSocketAddress.createUnresolved(removedEndDot, srvResolved.port)).iterator()
|
||||
}
|
||||
|
||||
if (!iterator.hasNext()) throw StacklessException("Hostname has no IP address")
|
||||
|
@ -1,10 +1,14 @@
|
||||
package com.viaversion.aas.web
|
||||
|
||||
import com.auth0.jwt.JWT
|
||||
import com.auth0.jwt.algorithms.Algorithm
|
||||
import com.auth0.jwt.exceptions.JWTVerificationException
|
||||
import com.google.common.cache.CacheBuilder
|
||||
import com.google.common.cache.CacheLoader
|
||||
import com.google.common.collect.MultimapBuilder
|
||||
import com.google.common.collect.Multimaps
|
||||
import com.google.gson.JsonObject
|
||||
import com.viaversion.aas.config.VIAaaSConfig
|
||||
import com.viaversion.aas.httpClient
|
||||
import com.viaversion.aas.parseUndashedId
|
||||
import com.viaversion.aas.util.StacklessException
|
||||
@ -18,6 +22,7 @@ import kotlinx.coroutines.time.delay
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.SocketAddress
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
@ -27,12 +32,31 @@ class WebDashboardServer {
|
||||
// I don't think i'll need more than 1k/day
|
||||
val ipInfo = IPInfo.builder().setToken("").build()
|
||||
val clients = ConcurrentHashMap<WebSocketSession, WebClient>()
|
||||
val loginTokens = CacheBuilder.newBuilder()
|
||||
.expireAfterAccess(10, TimeUnit.DAYS)
|
||||
.build<UUID, UUID>()
|
||||
val jwtAlgorithm = Algorithm.HMAC256(VIAaaSConfig.jwtSecret)
|
||||
|
||||
fun generateToken(account: UUID): UUID {
|
||||
return UUID.randomUUID().also { loginTokens.put(it, account) }
|
||||
fun generateToken(account: UUID): String {
|
||||
return JWT.create()
|
||||
.withExpiresAt(Date.from(Instant.now().plus(Duration.ofDays(30))))
|
||||
.withSubject("mc_account")
|
||||
.withClaim("can_listen", true)
|
||||
.withClaim("mc_uuid", account.toString())
|
||||
.withIssuer("viaaas")
|
||||
.sign(jwtAlgorithm)
|
||||
}
|
||||
|
||||
fun checkToken(token: String): UUID? {
|
||||
try {
|
||||
val verified = JWT.require(jwtAlgorithm)
|
||||
.withSubject("mc_account")
|
||||
.withClaim("can_listen", true)
|
||||
.withClaimPresence("mc_uuid")
|
||||
.build()
|
||||
.verify(token)
|
||||
|
||||
return UUID.fromString(verified.getClaim("mc_uuid").asString())
|
||||
} catch (e: JWTVerificationException) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Minecraft account -> WebClient
|
||||
@ -53,7 +77,7 @@ class WebDashboardServer {
|
||||
}
|
||||
})
|
||||
|
||||
val pendingSessionHashes = CacheBuilder.newBuilder()
|
||||
val sessionHashCallbacks = CacheBuilder.newBuilder()
|
||||
.expireAfterWrite(30, TimeUnit.SECONDS)
|
||||
.build<String, CompletableFuture<Unit>>(CacheLoader.from { _ -> CompletableFuture() })
|
||||
|
||||
@ -61,7 +85,7 @@ class WebDashboardServer {
|
||||
id: UUID, name: String, hash: String,
|
||||
address: SocketAddress, backAddress: SocketAddress
|
||||
): CompletableFuture<Unit> {
|
||||
val future = pendingSessionHashes.get(hash)
|
||||
val future = sessionHashCallbacks.get(hash)
|
||||
if (!listeners.containsKey(id)) {
|
||||
future.completeExceptionally(StacklessException("No browser listening"))
|
||||
} else {
|
||||
@ -103,7 +127,9 @@ class WebDashboardServer {
|
||||
|
||||
suspend fun onMessage(ws: WebSocketServerSession, msg: String) {
|
||||
val client = clients[ws]!!
|
||||
client.rateLimiter.acquire()
|
||||
while (client.rateLimiter.tryAcquire()) {
|
||||
delay(10)
|
||||
}
|
||||
client.state.onMessage(client, msg)
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,13 @@
|
||||
package com.viaversion.aas.web
|
||||
|
||||
import com.viaversion.aas.generateOfflinePlayerUuid
|
||||
import com.viaversion.aas.httpClient
|
||||
import com.viaversion.aas.webLogger
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonObject
|
||||
import com.viaversion.aas.generateOfflinePlayerUuid
|
||||
import com.viaversion.aas.httpClient
|
||||
import com.viaversion.aas.parseUndashedId
|
||||
import com.viaversion.aas.util.StacklessException
|
||||
import com.viaversion.aas.webLogger
|
||||
import io.ktor.client.request.forms.*
|
||||
import io.ktor.features.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.http.cio.websocket.*
|
||||
import java.net.URLEncoder
|
||||
@ -29,10 +29,13 @@ class WebLogin : WebState {
|
||||
val uuid = generateOfflinePlayerUuid(username)
|
||||
|
||||
val token = webClient.server.generateToken(uuid)
|
||||
webClient.ws.send(
|
||||
"""{"action": "login_result", "success": true,
|
||||
| "username": "$username", "uuid": "$uuid", "token": "$token"}""".trimMargin()
|
||||
)
|
||||
webClient.ws.send(JsonObject().also {
|
||||
it.addProperty("login_result", true)
|
||||
it.addProperty("success", true)
|
||||
it.addProperty("username", username)
|
||||
it.addProperty("uuid", uuid.toString())
|
||||
it.addProperty("token", token)
|
||||
}.toString())
|
||||
|
||||
webLogger.info("Token gen: ${webClient.id}: offline $username $uuid")
|
||||
}
|
||||
@ -47,43 +50,51 @@ class WebLogin : WebState {
|
||||
|
||||
if (check.getAsJsonPrimitive("valid").asBoolean) {
|
||||
val mcIdUser = check.get("username").asString
|
||||
val uuid = webClient.server.usernameIdCache.get(mcIdUser)
|
||||
val uuid = check.get("uuid")?.asString?.let { parseUndashedId(it.replace("-", "")) }
|
||||
?: webClient.server.usernameIdCache.get(mcIdUser)
|
||||
|
||||
val token = webClient.server.generateToken(uuid)
|
||||
webClient.ws.send(
|
||||
"""{"action": "login_result", "success": true,
|
||||
| "username": "$mcIdUser", "uuid": "$uuid", "token": "$token"}""".trimMargin()
|
||||
)
|
||||
webClient.ws.send(JsonObject().also {
|
||||
it.addProperty("action", "login_result")
|
||||
it.addProperty("success", true)
|
||||
it.addProperty("username", mcIdUser)
|
||||
it.addProperty("uuid", uuid.toString())
|
||||
it.addProperty("token", token)
|
||||
}.toString())
|
||||
|
||||
webLogger.info("Token gen: ${webClient.id}: $mcIdUser $uuid")
|
||||
} else {
|
||||
webClient.ws.send("""{"action": "login_result", "success": false}""")
|
||||
webClient.ws.send(JsonObject().also {
|
||||
it.addProperty("action", "login_result")
|
||||
it.addProperty("success", false)
|
||||
}.toString())
|
||||
webLogger.info("Token gen fail: ${webClient.id}: $username")
|
||||
}
|
||||
}
|
||||
"listen_login_requests" -> {
|
||||
val token = UUID.fromString(obj.getAsJsonPrimitive("token").asString)
|
||||
val user = webClient.server.loginTokens.getIfPresent(token)
|
||||
val token = obj.getAsJsonPrimitive("token").asString
|
||||
val user = webClient.server.checkToken(token)
|
||||
val response = JsonObject().also {
|
||||
it.addProperty("action", "listen_login_requests_result")
|
||||
it.addProperty("token", token)
|
||||
}
|
||||
if (user != null && webClient.listenId(user)) {
|
||||
webClient.ws.send("""{"action": "listen_login_requests_result", "token": "$token", "success": true, "user": "$user"}""")
|
||||
response.addProperty("success", true)
|
||||
response.addProperty("user", user.toString())
|
||||
webLogger.info("Listen: ${webClient.id}: $user")
|
||||
} else {
|
||||
webClient.server.loginTokens.invalidate(token)
|
||||
webClient.ws.send("""{"action": "listen_login_requests_result", "token": "$token", "success": false}""")
|
||||
response.addProperty("success", false)
|
||||
webLogger.info("Token fail: ${webClient.id}")
|
||||
}
|
||||
webClient.ws.send(response.toString())
|
||||
}
|
||||
"unlisten_login_requests" -> {
|
||||
val uuid = UUID.fromString(obj.getAsJsonPrimitive("uuid").asString)
|
||||
webClient.unlistenId(uuid)
|
||||
}
|
||||
"invalidate_token" -> {
|
||||
val token = UUID.fromString(obj.getAsJsonPrimitive("token").asString)
|
||||
webClient.server.loginTokens.invalidate(token)
|
||||
}
|
||||
"session_hash_response" -> {
|
||||
val hash = obj.get("session_hash").asString
|
||||
webClient.server.pendingSessionHashes.getIfPresent(hash)?.complete(null)
|
||||
webClient.server.sessionHashCallbacks.getIfPresent(hash)?.complete(null)
|
||||
}
|
||||
else -> throw StacklessException("invalid action!")
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ bind-address: localhost
|
||||
# Use Netty native transport for Minecraft connections when available.
|
||||
native-transport-mc: true
|
||||
# Address of SOCKS5 proxy used for connecting to backend servers. Empty to disable.
|
||||
backend-socks5-proxy-address: ""
|
||||
backend-socks5-proxy-address: ''
|
||||
# Port of SOCKS5 proxy used for connecting to backend servers.
|
||||
backend-socks5-proxy-port: 9050
|
||||
#
|
||||
@ -36,7 +36,7 @@ use-strong-random: false
|
||||
require-host-name: true
|
||||
# Host names of this instance, that will be used in the virtual host as a suffix.
|
||||
# Use commas for separating multiple hostnames.
|
||||
host-name: viaaas.localhost,via.localhost,via-127-0-0-1.nip.io
|
||||
host-name: [ viaaas.localhost, via.localhost, via-127-0-0-1.nip.io ]
|
||||
# Requires online mode for front-end connections. May be useful for stopping bots.
|
||||
force-online-mode: false
|
||||
# Default port to be used when connecting to the backend server.
|
||||
@ -49,9 +49,9 @@ default-backend-port: 25565
|
||||
# Blocks backend connection to local addresses (localhost, 0.0.0.0, ::1, 127.(...), 10.(...), etc).
|
||||
block-local-address: true
|
||||
# If some server is in this list, it will be blocked. This has priority over allowed-back-addresses.
|
||||
blocked-back-addresses: ["*.hypixel.net"]
|
||||
blocked-back-addresses: [ "*.hypixel.net" ]
|
||||
# Only allows the backend address if it matches an address in this list.
|
||||
allowed-back-addresses: ["*"]
|
||||
allowed-back-addresses: [ "*" ]
|
||||
#
|
||||
######
|
||||
# Info
|
||||
@ -70,3 +70,9 @@ rate-limit-ws: 1.5
|
||||
rate-limit-connection-mc: 10.0
|
||||
# Limits how many usernames a websocket connection can listen to.
|
||||
listening-ws-limit: 10
|
||||
#
|
||||
#####
|
||||
# SECRETS - DO NOT SHARE
|
||||
#####
|
||||
# Key used to generate Minecraft tokens for listening logins
|
||||
jwt-secret: ''
|
||||
|
Loading…
Reference in New Issue
Block a user