close #124, jwt with symemtric secret, close #125

This commit is contained in:
creeper123123321 2021-04-09 21:04:48 -03:00
parent 951000627e
commit 9d4dcb65b5
8 changed files with 118 additions and 54 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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")

View File

@ -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)
}

View File

@ -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!")
}

View File

@ -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: ''