diff --git a/README.md b/README.md
index 1df2030..e35302d 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,10 @@ Default WS URL: wss://localhost:25543/ws
It requires a CORS Proxy for calling Mojang APIs, which may make Mojang see that
as suspicious and reset/block your account password.
+- There are some information about Mojang password resetting:
+ https://github.com/GeyserMC/Geyser/wiki/Common-Issues#mojang-resetting-account-credentials and
+ https://mobile.twitter.com/MojangSupport/status/863697596350517248
+
- VIAaaS may have security vulnerabilities, make sure to block the ports in firewall and take care of browser local storage.
Download: https://github.com/ViaVersion/VIAaaS/actions (needs to be logged into GitHub)
@@ -21,18 +25,25 @@ Usage for offline mode:
- Run the shadow jar or ./gradlew clean run
- Connect to mc.example.com._v1_8.viaaas.localhost
-Usage for online mode (may block your Mojang account):
+Usage for online mode with two accounts (recommended):
- Run the shadow jar or ./gradlew clean run
-- It's recommended to use 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 in your browser. You can use
- https://www.curseforge.com/minecraft/mc-mods/auth-me for reauthenticate the client.)
- You should set up a CORS Proxy (something like https://github.com/Rob--W/cors-anywhere) on local machine.
-- Go to https://localhost:25543/auth.html, configure the CORS Proxy URL (something like http://localhost:8080/) and listen to
- the username A that you're using to connect to the proxy.
-- Add web page the account B you used in _u parameter.
-- Connect to mc.example.com._v1_8.viaaas._u(account B).localhost
+- Go to https://localhost:25543/auth.html, configure the CORS Proxy URL (something like http://localhost:8080/,
+ note the ending slash) and listen to the username A that you're using to connect to the proxy.
+- Add the account B you'll use in _u parameter to browser auth page.
+- Connect to mc.example.com._v1_8._u(account B).viaaas.localhost
- Approve the login
-- There are some information about Mojang password resetting: https://github.com/GeyserMC/Geyser/wiki/Common-Issues#mojang-resetting-account-credentials and https://mobile.twitter.com/MojangSupport/status/863697596350517248
+
+Usage for online mode with one account:
+- Run the shadow jar or ./gradlew clean run
+- You should set up a CORS Proxy (something like https://github.com/Rob--W/cors-anywhere) on local machine.
+- Go to https://localhost:25543/auth.html, configure the CORS Proxy URL (something like http://localhost:8080/,
+ note the ending slash) and listen to the username.
+- Add the account to browser auth page.
+- Connect to mc.example.com._v1_8.viaaas.localhost
+- Approve the login
+- Minecraft client will give Bad Login after you approve the login in your browser. You can use
+ https://www.curseforge.com/minecraft/mc-mods/auth-me for reauthenticate the client.
## WARNING
VIAaaS may trigger anti-cheats, due to block, item, movement and other differences between versions. USE AT OWN RISK
diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/CloudHandler.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/CloudHandler.kt
index 17ed7fc..0d77cd8 100644
--- a/src/main/kotlin/com/github/creeper123123321/viaaas/CloudHandler.kt
+++ b/src/main/kotlin/com/github/creeper123123321/viaaas/CloudHandler.kt
@@ -131,6 +131,10 @@ class HandshakeState : MinecraftConnectionState {
else -> throw IllegalStateException("Invalid next state")
}
+ if (!handler.user.get(CloudData::class.java)!!.hadHostname && VIAaaSConfig.requireHostName) {
+ throw UnsupportedOperationException("This VIAaaS instance requires you to use the hostname")
+ }
+
handler.user.channel!!.setAutoRead(false)
GlobalScope.launch(Dispatchers.IO) {
val frontHandler = handler.user.channel!!.pipeline().get(CloudMinecraftHandler::class.java)
@@ -153,9 +157,9 @@ class HandshakeState : MinecraftConnectionState {
val socketAddr = InetSocketAddress(InetAddress.getByName(srvResolvedAddr), srvResolvedPort)
val addrInfo = socketAddr.address
if (VIAaaSConfig.blockLocalAddress && (addrInfo.isSiteLocalAddress
- || addrInfo.isLoopbackAddress
- || addrInfo.isLinkLocalAddress
- || addrInfo.isAnyLocalAddress)
+ || addrInfo.isLoopbackAddress
+ || addrInfo.isLinkLocalAddress
+ || addrInfo.isAnyLocalAddress)
) throw SecurityException("Local addresses aren't allowed")
val bootstrap = Bootstrap().handler(BackendInit(handler.user))
diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/CloudProtocol.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/CloudProtocol.kt
index fc19c7e..92fd3c5 100644
--- a/src/main/kotlin/com/github/creeper123123321/viaaas/CloudProtocol.kt
+++ b/src/main/kotlin/com/github/creeper123123321/viaaas/CloudProtocol.kt
@@ -35,8 +35,11 @@ object CloudHeadProtocol : SimpleProtocol() {
val receivedPort = wrapper.read(Type.UNSIGNED_SHORT)
val parsed = VIAaaSAddress().parse(addr.substringBefore(0.toChar()), VIAaaSConfig.hostName)
- if (parsed.viaSuffix == null && VIAaaSConfig.requireHostName) throw IllegalStateException("VIAaaS hostname is required")
- val backPort = parsed.port ?: receivedPort
+ val backPort = parsed.port ?: if (VIAaaSConfig.defaultBackendPort == -1) {
+ receivedPort
+ } else {
+ VIAaaSConfig.defaultBackendPort
+ }
val backAddr = parsed.realAddress
val backProto = parsed.protocol ?: 47
@@ -52,7 +55,8 @@ object CloudHeadProtocol : SimpleProtocol() {
userConnection = wrapper.user(),
backendVer = backProto,
frontOnline = parsed.online,
- altName = parsed.altUsername
+ altName = parsed.altUsername,
+ hadHostname = parsed.viaSuffix == null
)
)
@@ -67,5 +71,6 @@ data class CloudData(
val userConnection: UserConnection,
var backendVer: Int,
var frontOnline: Boolean,
- var altName: String?
+ var altName: String?,
+ var hadHostname: Boolean
) : StoredObject(userConnection)
\ No newline at end of file
diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/VIAaaS.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/VIAaaS.kt
index a4df97c..0906296 100644
--- a/src/main/kotlin/com/github/creeper123123321/viaaas/VIAaaS.kt
+++ b/src/main/kotlin/com/github/creeper123123321/viaaas/VIAaaS.kt
@@ -256,6 +256,7 @@ object VIAaaSConfig : Config(File("config/viaaas.yml")) {
val useStrongRandom: Boolean get() = this.getBoolean("use-strong-random", true)
val blockLocalAddress: Boolean get() = this.getBoolean("block-local-address", true)
val requireHostName: Boolean get() = this.getBoolean("require-host-name", true)
+ val defaultBackendPort: Int get() = this.getInt("default-backend-port", 25565)
}
class VIAaaSAddress {
diff --git a/src/main/kotlin/com/github/creeper123123321/viaaas/ViaWeb.kt b/src/main/kotlin/com/github/creeper123123321/viaaas/ViaWeb.kt
index 330d244..079b5b3 100644
--- a/src/main/kotlin/com/github/creeper123123321/viaaas/ViaWeb.kt
+++ b/src/main/kotlin/com/github/creeper123123321/viaaas/ViaWeb.kt
@@ -4,15 +4,20 @@ import com.google.common.base.Preconditions
import com.google.common.cache.CacheBuilder
import com.google.common.cache.CacheLoader
import com.google.gson.Gson
+import com.google.gson.JsonArray
import com.google.gson.JsonObject
import io.ktor.application.*
+import io.ktor.client.features.json.*
import io.ktor.client.request.*
import io.ktor.client.request.forms.*
import io.ktor.features.*
import io.ktor.http.*
import io.ktor.http.cio.websocket.*
import io.ktor.http.content.*
+import io.ktor.request.*
+import io.ktor.response.*
import io.ktor.routing.*
+import io.ktor.util.*
import io.ktor.websocket.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.consumeEach
@@ -67,6 +72,126 @@ class ViaWebApp {
}
}
+ // todo xbox auth
+ /*
+ val redirectUrl = "https://localhost:25543/xbox-auth/ms-callback"
+ val siteKey = "9e95fd56-0f45-42f9-af28-9b803645da22"
+ val secretKey = "redacted"
+ val azureClientId = "a370fff9-7648-4dbf-b96e-2b4f8d539ac2"
+ val azureClientSecret = "redacted"
+
+ get("/xbox-auth/") {
+ call.respondText(contentType = ContentType.parse("text/html")) {
+ """
+
+
+"""
+ }
+ }
+
+ val validTokens = Collections.newSetFromMap(ConcurrentHashMap())
+
+ post("/xbox-auth/ms-login") {
+ val multipart = call.receiveParameters()
+
+ val hcaptchaResponse = httpClient.submitForm(
+ "https://hcaptcha.com/siteverify",
+ parametersOf(
+ "response" to listOf(multipart["h-captcha-response"]!!),
+ "secret" to listOf(secretKey),
+ "siteKey" to listOf(siteKey)
+ )
+ )
+
+ if (!hcaptchaResponse.get("success").asBoolean) {
+ call.respondText(status = HttpStatusCode.Forbidden) { "hcaptcha failed" }
+ return@post
+ }
+
+ call.respondRedirect(permanent = false) {
+ takeFrom(
+ "https://login.live.com/oauth20_authorize.srf" +
+ "?client_id=$azureClientId" +
+ "&response_type=code" +
+ "&redirect_uri=${URLEncoder.encode(redirectUrl, Charsets.UTF_8)}" +
+ "&scope=XboxLive.signin" +
+ "&state=${UUID.randomUUID().also { validTokens.add(it) }}"
+ )
+ }
+ }
+
+ get("/xbox-auth/ms-callback") {
+ val authCode = call.request.queryParameters.getOrFail("code")
+ val state = call.request.queryParameters.getOrFail("state")
+
+ if (!validTokens.remove(UUID.fromString(state))) {
+ call.respondText(status = HttpStatusCode.Forbidden) { "failed state token" }
+ return@get
+ }
+ val authToken = httpClient.submitForm(
+ "https://login.live.com/oauth20_token.srf",
+ parametersOf(
+ "client_id" to listOf(azureClientId),
+ "client_secret" to listOf(azureClientSecret),
+ "code" to listOf(authCode),
+ "grant_type" to listOf("authorization_code"),
+ "redirect_uri" to listOf(redirectUrl)
+ )
+ )
+
+
+ val xboxLiveAuthResult = httpClient.post {
+ url("https://user.auth.xboxlive.com/user/authenticate")
+ body = JsonObject().also {
+ it.add("Properties", JsonObject().also {
+ it.addProperty("AuthMethod", "RPS")
+ it.addProperty("SiteName", "user.auth.xboxlive.com")
+ it.addProperty("RpsTicket", authToken.get("access_token").asString)
+ })
+ it.addProperty("TokenType", "JWT")
+ it.addProperty("RelyingParty", "http://auth.xboxlive.com")
+ }
+ header("content-type", "application/json")
+ header("accept", "application/json")
+ }
+
+ val xstsAuth = httpClient.post {
+ url("https://xsts.auth.xboxlive.com/xsts/authorize")
+ body = JsonObject().also {
+ it.add("Properties", JsonObject().also {
+ it.addProperty("SandboxId", "RETAIL")
+ it.add(
+ "UserTokens",
+ JsonArray().also { it.add(xboxLiveAuthResult.get("Token").asString) })
+ })
+ it.addProperty("TokenType", "JWT")
+ it.addProperty("RelyingParty", "rp://api.minecraftservices.com/")
+ }
+ header("content-type", "application/json")
+ header("accept", "application/json")
+ }
+
+ val mcToken = httpClient.post {
+ url("https://api.minecraftservices.com/authentication/login_with_xbox")
+ body = JsonObject().also {
+ it.addProperty(
+ "identityToken",
+ "XBL3.0 x=${
+ xstsAuth.getAsJsonObject("DisplayClaims").getAsJsonArray("xui")
+ .first { it.asJsonObject.has("uhs") }.asJsonObject.get("uhs").asString
+ };${xstsAuth.get("Token").asString}"
+ )
+ }
+ header("content-type", "application/json")
+ header("accept", "application/json")
+ }
+
+ call.respondText { mcToken.get("access_token").asString }
+ } */
+
static {
defaultResource("index.html", "web")
resources("web")
@@ -79,43 +204,47 @@ class ViaWebApp {
fun fromUndashed(string: String): UUID {
Preconditions.checkArgument(string.length == 32, "Length is incorrect")
return UUID(
- java.lang.Long.parseUnsignedLong(string.substring(0, 16), 16),
- java.lang.Long.parseUnsignedLong(string.substring(16), 16)
+ java.lang.Long.parseUnsignedLong(string.substring(0, 16), 16),
+ java.lang.Long.parseUnsignedLong(string.substring(16), 16)
)
}
class WebDashboardServer {
val clients = ConcurrentHashMap()
val loginTokens = CacheBuilder.newBuilder()
- .expireAfterAccess(10, TimeUnit.DAYS)
- .build()
+ .expireAfterAccess(10, TimeUnit.DAYS)
+ .build()
// Minecraft account -> WebClient
val listeners = ConcurrentHashMap>()
val usernameIdCache = CacheBuilder.newBuilder()
- .expireAfterWrite(1, TimeUnit.HOURS)
- .build(CacheLoader.from { name ->
- runBlocking {
- withContext(Dispatchers.IO) {
- httpClient.get("https://api.mojang.com/users/profiles/minecraft/$name")
- ?.get("id")?.asString?.let { fromUndashed(it) }
- }
+ .expireAfterWrite(1, TimeUnit.HOURS)
+ .build(CacheLoader.from { name ->
+ runBlocking {
+ withContext(Dispatchers.IO) {
+ httpClient.get("https://api.mojang.com/users/profiles/minecraft/$name")
+ ?.get("id")?.asString?.let { fromUndashed(it) }
}
- })
+ }
+ })
val pendingSessionHashes = CacheBuilder.newBuilder()
- .expireAfterWrite(30, TimeUnit.SECONDS)
- .build>(CacheLoader.from { _ -> CompletableFuture() })
+ .expireAfterWrite(30, TimeUnit.SECONDS)
+ .build>(CacheLoader.from { _ -> CompletableFuture() })
- suspend fun requestSessionJoin(id: UUID, name: String, hash: String,
- address: SocketAddress, backKey: PublicKey)
+ suspend fun requestSessionJoin(
+ id: UUID, name: String, hash: String,
+ address: SocketAddress, backKey: PublicKey
+ )
: CompletableFuture {
val future = viaWebServer.pendingSessionHashes.get(hash)
var sent = 0
viaWebServer.listeners[id]?.forEach {
- it.ws.send("""{"action": "session_hash_request", "user": "$name", "session_hash": "$hash",
+ it.ws.send(
+ """{"action": "session_hash_request", "user": "$name", "session_hash": "$hash",
| "client_address": "$address", "backend_public_key":
- | "${Base64.getEncoder().encodeToString(backKey.encoded)}"}""".trimMargin())
+ | "${Base64.getEncoder().encodeToString(backKey.encoded)}"}""".trimMargin()
+ )
it.ws.flush()
sent++
}
@@ -154,10 +283,12 @@ class WebDashboardServer {
}
-data class WebClient(val server: WebDashboardServer,
- val ws: WebSocketServerSession,
- val state: WebState,
- val listenedIds: MutableSet = mutableSetOf())
+data class WebClient(
+ val server: WebDashboardServer,
+ val ws: WebSocketServerSession,
+ val state: WebState,
+ val listenedIds: MutableSet = mutableSetOf()
+)
interface WebState {
suspend fun start(webClient: WebClient)
@@ -181,10 +312,9 @@ class WebLogin : WebState {
val code = obj.getAsJsonPrimitive("code").asString
val check = httpClient.submitForm(
- "https://api.minecraft.id/gateway/verify/${URLEncoder.encode(username, Charsets.UTF_8)}",
- formParameters = parametersOf("code", code),
- encodeInQuery = false) {
- }
+ "https://api.minecraft.id/gateway/verify/${URLEncoder.encode(username, Charsets.UTF_8)}",
+ formParameters = parametersOf("code", code),
+ )
if (check.getAsJsonPrimitive("valid").asBoolean) {
val token = UUID.randomUUID()
@@ -192,13 +322,15 @@ class WebLogin : WebState {
val uuid = webClient.server.usernameIdCache.get(mcIdUser)
webClient.server.loginTokens.put(token, uuid)
- webClient.ws.send("""{"action": "minecraft_id_result", "success": true,
- | "username": "$mcIdUser", "uuid": "$uuid", "token": "$token"}""".trimMargin())
+ webClient.ws.send(
+ """{"action": "minecraft_id_result", "success": true,
+ | "username": "$mcIdUser", "uuid": "$uuid", "token": "$token"}""".trimMargin()
+ )
- webLogger.info("Generated a token for account $mcIdUser $uuid")
+ webLogger.info("${webClient.ws.call.request.local.remoteHost} (O: ${webClient.ws.call.request.origin.remoteHost}) generated a token for account $mcIdUser $uuid")
} else {
webClient.ws.send("""{"action": "minecraft_id_result", "success": false}""")
- webLogger.info("Failed to generated a token for account $username")
+ webLogger.info("${webClient.ws.call.request.local.remoteHost} (O: ${webClient.ws.call.request.origin.remoteHost}) failed to generated a token for account $username")
}
}
"listen_login_requests" -> {
@@ -208,12 +340,12 @@ class WebLogin : WebState {
webClient.ws.send("""{"action": "listen_login_requests_result", "token": "$token", "success": true, "user": "$user"}""")
webClient.listenedIds.add(user)
webClient.server.listeners.computeIfAbsent(user) { Collections.newSetFromMap(ConcurrentHashMap()) }
- .add(webClient)
+ .add(webClient)
- webLogger.info("Listening for logins for $user")
+ webLogger.info("${webClient.ws.call.request.local.remoteHost} (O: ${webClient.ws.call.request.origin.remoteHost}) listening for logins for $user")
} else {
webClient.ws.send("""{"action": "listen_login_requests_result", "token": "$token", "success": false}""")
- webLogger.info("Failed token")
+ webLogger.info("${webClient.ws.call.request.local.remoteHost} (O: ${webClient.ws.call.request.origin.remoteHost}) failed token")
}
}
"session_hash_response" -> {
diff --git a/src/main/resources/viaaas.yml b/src/main/resources/viaaas.yml
index bab8976..0d6d733 100644
--- a/src/main/resources/viaaas.yml
+++ b/src/main/resources/viaaas.yml
@@ -18,4 +18,7 @@ use-strong-random: true
block-local-address: true
# Requires virtual host to contain the value from "host-name"
# A false value could be used for transparent proxying.
-require-host-name: true
\ No newline at end of file
+require-host-name: true
+# Default port to be used to connect to backend server
+# Use -1 to reuse the port sent by client, useful for transparent proxying
+default-backend-port: 25565
\ No newline at end of file