1.19-pre1, add "send access token"

closes #200
This commit is contained in:
creeper123123321 2022-05-22 18:57:25 -03:00
parent fb85cfa329
commit 237761a3d3
13 changed files with 233 additions and 108 deletions

View File

@ -57,8 +57,8 @@ dependencies {
implementation(kotlin("stdlib-jdk8")) implementation(kotlin("stdlib-jdk8"))
implementation(kotlin("reflect")) implementation(kotlin("reflect"))
val vvVer = "4.3.0-22w19a-SNAPSHOT" val vvVer = "4.3.0-1.19-pre1-SNAPSHOT"
val vbVer = "4.3.0-22w19a-SNAPSHOT" val vbVer = "4.3.0-1.19-pre1-SNAPSHOT"
val vrVer = "d189537" val vrVer = "d189537"
implementation("com.viaversion:viaversion:$vvVer") { isTransitive = false } implementation("com.viaversion:viaversion:$vvVer") { isTransitive = false }
implementation("com.viaversion:viabackwards:$vbVer") { isTransitive = false } implementation("com.viaversion:viabackwards:$vbVer") { isTransitive = false }

View File

@ -1,5 +1,6 @@
package com.viaversion.aas.codec; package com.viaversion.aas.codec;
import com.viaversion.aas.UtilKt;
import com.viaversion.aas.codec.packet.Packet; import com.viaversion.aas.codec.packet.Packet;
import com.viaversion.aas.codec.packet.PacketRegistry; import com.viaversion.aas.codec.packet.PacketRegistry;
import com.viaversion.aas.handler.MinecraftHandler; import com.viaversion.aas.handler.MinecraftHandler;
@ -51,6 +52,7 @@ public class MinecraftCodec extends MessageToMessageCodec<ByteBuf, Packet> {
handler.getFrontEnd() ? Direction.SERVERBOUND : Direction.CLIENTBOUND handler.getFrontEnd() ? Direction.SERVERBOUND : Direction.CLIENTBOUND
)); ));
if (msg.isReadable()) { if (msg.isReadable()) {
UtilKt.getMcLogger().debug("Remaining bytes in packet {}", out);
throw new StacklessException("Remaining bytes!!!"); throw new StacklessException("Remaining bytes!!!");
} }
} }

View File

@ -4,13 +4,14 @@ import com.viaversion.aas.codec.packet.Packet;
import com.viaversion.viaversion.api.protocol.version.ProtocolVersion; import com.viaversion.viaversion.api.protocol.version.ProtocolVersion;
import com.viaversion.viaversion.api.type.Type; import com.viaversion.viaversion.api.type.Type;
import com.viaversion.viaversion.api.type.types.StringType; import com.viaversion.viaversion.api.type.types.StringType;
import com.viaversion.viaversion.libs.opennbt.tag.builtin.CompoundTag;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
public class LoginStart implements Packet { public class LoginStart implements Packet {
private String username; private String username;
private CompoundTag publicKey; private long timestamp;
private byte[] key;
private byte[] signature;
public String getUsername() { public String getUsername() {
return username; return username;
@ -25,7 +26,9 @@ public class LoginStart implements Packet {
username = new StringType(16).read(byteBuf); username = new StringType(16).read(byteBuf);
if (protocolVersion >= ProtocolVersion.v1_19.getVersion()) { if (protocolVersion >= ProtocolVersion.v1_19.getVersion()) {
if (byteBuf.readBoolean()) { if (byteBuf.readBoolean()) {
publicKey = Type.NBT.read(byteBuf); timestamp = byteBuf.readLong();
key = Type.BYTE_ARRAY_PRIMITIVE.read(byteBuf);
signature = Type.BYTE_ARRAY_PRIMITIVE.read(byteBuf);
} }
} }
} }
@ -34,11 +37,13 @@ public class LoginStart implements Packet {
public void encode(@NotNull ByteBuf byteBuf, int protocolVersion) throws Exception { public void encode(@NotNull ByteBuf byteBuf, int protocolVersion) throws Exception {
Type.STRING.write(byteBuf, username); Type.STRING.write(byteBuf, username);
if (protocolVersion >= ProtocolVersion.v1_19.getVersion()) { if (protocolVersion >= ProtocolVersion.v1_19.getVersion()) {
if (publicKey == null) { if (key == null) {
byteBuf.writeBoolean(false); byteBuf.writeBoolean(false);
} else { } else {
byteBuf.writeBoolean(true); byteBuf.writeBoolean(true);
Type.NBT.write(byteBuf, publicKey); byteBuf.writeLong(timestamp);
Type.BYTE_ARRAY_PRIMITIVE.write(byteBuf, key);
Type.BYTE_ARRAY_PRIMITIVE.write(byteBuf, signature);
} }
} }
} }

View File

@ -10,7 +10,7 @@ import com.viaversion.viaversion.api.protocol.version.ProtocolVersion
import de.gerrygames.viarewind.api.ViaRewindConfigImpl import de.gerrygames.viarewind.api.ViaRewindConfigImpl
import io.ktor.server.application.* import io.ktor.server.application.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.apache.logging.log4j.Level import org.apache.logging.log4j.Level
import org.apache.logging.log4j.io.IoBuilder import org.apache.logging.log4j.io.IoBuilder
@ -21,7 +21,7 @@ fun main(args: Array<String>) {
try { try {
setupSystem() setupSystem()
printSplash() printSplash()
CoroutineScope(Dispatchers.IO).launch { viaaasLogger.info(AspirinServer.updaterCheckMessage()) } CoroutineScope(Job()).launch { viaaasLogger.info(AspirinServer.updaterCheckMessage()) }
AspirinServer.generateCert() AspirinServer.generateCert()
initVia() initVia()
AspirinServer.listenPorts(args) AspirinServer.listenPorts(args)

View File

@ -4,7 +4,7 @@ import com.viaversion.aas.AspirinServer
import com.viaversion.viaversion.api.command.ViaCommandSender import com.viaversion.viaversion.api.command.ViaCommandSender
import com.viaversion.viaversion.api.command.ViaSubCommand import com.viaversion.viaversion.api.command.ViaSubCommand
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
object VIAaaSSubCommand : ViaSubCommand() { object VIAaaSSubCommand : ViaSubCommand() {
@ -12,7 +12,7 @@ object VIAaaSSubCommand : ViaSubCommand() {
override fun description(): String = "Info about VIAaaS" override fun description(): String = "Info about VIAaaS"
override fun execute(p0: ViaCommandSender, p1: Array<out String>): Boolean { override fun execute(p0: ViaCommandSender, p1: Array<out String>): Boolean {
p0.sendMessage("VIAaaS version ${AspirinServer.version}") p0.sendMessage("VIAaaS version ${AspirinServer.version}")
CoroutineScope(Dispatchers.IO).launch { p0.sendMessage(AspirinServer.updaterCheckMessage()) } CoroutineScope(Job()).launch { p0.sendMessage(AspirinServer.updaterCheckMessage()) }
return true return true
} }
} }

View File

@ -10,7 +10,7 @@ import io.netty.channel.SimpleChannelInboundHandler
import io.netty.handler.proxy.ProxyConnectException import io.netty.handler.proxy.ProxyConnectException
import io.netty.handler.proxy.ProxyHandler import io.netty.handler.proxy.ProxyHandler
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import java.net.SocketAddress import java.net.SocketAddress
import java.nio.channels.ClosedChannelException import java.nio.channels.ClosedChannelException
@ -22,7 +22,7 @@ class MinecraftHandler(
lateinit var endRemoteAddress: SocketAddress lateinit var endRemoteAddress: SocketAddress
val other: Channel? get() = if (frontEnd) data.backChannel else data.frontChannel val other: Channel? get() = if (frontEnd) data.backChannel else data.frontChannel
var loggedDc = false var loggedDc = false
val coroutineScope = CoroutineScope(Dispatchers.Default) val coroutineScope = CoroutineScope(Job())
override fun channelRead0(ctx: ChannelHandlerContext, packet: Packet) { override fun channelRead0(ctx: ChannelHandlerContext, packet: Packet) {
if (!ctx.channel().isActive) return if (!ctx.channel().isActive) return

View File

@ -24,7 +24,7 @@ import io.netty.channel.ChannelOption
import io.netty.handler.timeout.ReadTimeoutHandler import io.netty.handler.timeout.ReadTimeoutHandler
import io.netty.resolver.NoopAddressResolverGroup import io.netty.resolver.NoopAddressResolverGroup
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
@ -33,7 +33,7 @@ import java.util.concurrent.TimeUnit
object ProtocolDetector { object ProtocolDetector {
private val loader = CacheLoader.from<InetSocketAddress, CompletableFuture<ProtocolVersion>> { address -> private val loader = CacheLoader.from<InetSocketAddress, CompletableFuture<ProtocolVersion>> { address ->
val future = CompletableFuture<ProtocolVersion>() val future = CompletableFuture<ProtocolVersion>()
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Job()).launch {
try { try {
val proxyUri = VIAaaSConfig.backendProxy val proxyUri = VIAaaSConfig.backendProxy
val proxySocket = if (proxyUri == null) null else { val proxySocket = if (proxyUri == null) null else {

View File

@ -0,0 +1,10 @@
package com.viaversion.aas.web
import com.google.common.net.HostAndPort
data class AddressInfo(
val backVersion: Int,
val backHostAndPort: HostAndPort,
var frontOnline: Boolean? = null,
var backName: String? = null
)

View File

@ -0,0 +1,5 @@
package com.viaversion.aas.web
import java.util.*
data class UserInfo(val id: UUID, val name: String?, val expiration: Date)

View File

@ -8,12 +8,14 @@ import com.viaversion.aas.*
import com.viaversion.aas.util.StacklessException import com.viaversion.aas.util.StacklessException
import com.viaversion.viaversion.api.protocol.version.ProtocolVersion import com.viaversion.viaversion.api.protocol.version.ProtocolVersion
import io.ktor.client.call.* import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.client.request.forms.* import io.ktor.client.request.forms.*
import io.ktor.http.* import io.ktor.http.*
import io.ktor.server.websocket.* import io.ktor.server.websocket.*
import kotlinx.coroutines.future.await import kotlinx.coroutines.future.await
import java.net.URLEncoder import java.net.URLEncoder
import java.time.Duration import java.time.Duration
import java.time.Instant
import java.util.* import java.util.*
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
@ -35,6 +37,7 @@ class WebLogin : WebState {
"unlisten_login_requests" -> handleUnlisten(webClient, obj) "unlisten_login_requests" -> handleUnlisten(webClient, obj)
"session_hash_response" -> handleSessionResponse(webClient, obj) "session_hash_response" -> handleSessionResponse(webClient, obj)
"parameters_response" -> handleParametersResponse(webClient, obj) "parameters_response" -> handleParametersResponse(webClient, obj)
"save_access_token" -> handleSaveAccessToken(webClient, obj)
else -> throw StacklessException("invalid action!") else -> throw StacklessException("invalid action!")
} }
@ -144,7 +147,7 @@ class WebLogin : WebState {
private fun handleParametersResponse(webClient: WebClient, obj: JsonObject) { private fun handleParametersResponse(webClient: WebClient, obj: JsonObject) {
val callback = UUID.fromString(obj["callback"].asString) val callback = UUID.fromString(obj["callback"].asString)
webClient.server.addressCallbacks[callback].complete( webClient.server.addressCallbacks[callback].complete(
WebServer.AddressInfo( AddressInfo(
backVersion = obj["version"].asString.let { backVersion = obj["version"].asString.let {
var protocol = Ints.tryParse(it) var protocol = Ints.tryParse(it)
if (protocol == null) { if (protocol == null) {
@ -159,4 +162,13 @@ class WebLogin : WebState {
) )
) )
} }
private suspend fun handleSaveAccessToken(webClient: WebClient, obj: JsonObject) {
val accessToken = obj["mc_access_token"].asString
val profile = AspirinServer.httpClient.get("https://api.minecraftservices.com/minecraft/profile") {
header("Authorization", "Bearer $accessToken")
}.body<JsonObject>()
val uuid = parseUndashedId(profile["id"].asString)
webClient.server.minecraftAccessTokens.put(uuid, accessToken)
}
} }

View File

@ -7,13 +7,14 @@ import com.google.common.cache.CacheBuilder
import com.google.common.cache.CacheLoader import com.google.common.cache.CacheLoader
import com.google.common.collect.MultimapBuilder import com.google.common.collect.MultimapBuilder
import com.google.common.collect.Multimaps import com.google.common.collect.Multimaps
import com.google.common.net.HostAndPort
import com.google.gson.JsonObject import com.google.gson.JsonObject
import com.viaversion.aas.* import com.viaversion.aas.*
import com.viaversion.aas.config.VIAaaSConfig import com.viaversion.aas.config.VIAaaSConfig
import com.viaversion.aas.util.StacklessException import com.viaversion.aas.util.StacklessException
import io.ktor.client.call.body import io.ktor.client.call.body
import io.ktor.client.request.* import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.server.netty.* import io.ktor.server.netty.*
import io.ktor.server.websocket.* import io.ktor.server.websocket.*
import io.ktor.websocket.* import io.ktor.websocket.*
@ -32,12 +33,48 @@ import java.util.*
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.coroutines.coroutineContext
class WebServer { class WebServer {
// I don't think i'll need more than 1k/day
val clients = ConcurrentHashMap<WebSocketSession, WebClient>() val clients = ConcurrentHashMap<WebSocketSession, WebClient>()
val jwtAlgorithm = Algorithm.HMAC256(VIAaaSConfig.jwtSecret) val jwtAlgorithm = Algorithm.HMAC256(VIAaaSConfig.jwtSecret)
val coroutineScope = CoroutineScope(Job())
// Minecraft account -> WebClient
val listeners = Multimaps.synchronizedSetMultimap(
MultimapBuilder.SetMultimapBuilder
.hashKeys()
.hashSetValues()
.build<UUID, WebClient>()
)
val usernameToIdCache = CacheBuilder.newBuilder()
.expireAfterWrite(1, TimeUnit.HOURS)
.build<String, CompletableFuture<UUID?>>(CacheLoader.from { name ->
coroutineScope.async {
AspirinServer.httpClient
.get("https://api.mojang.com/users/profiles/minecraft/$name")
.body<JsonObject?>()?.get("id")?.asString?.let { parseUndashedId(it) }
}.asCompletableFuture()
})
val idToProfileCache = CacheBuilder.newBuilder()
.expireAfterWrite(1, TimeUnit.HOURS)
.build<UUID, CompletableFuture<JsonObject?>>(CacheLoader.from { id ->
coroutineScope.async {
AspirinServer.httpClient
.get("https://sessionserver.mojang.com/session/minecraft/profile/$id")
.body<JsonObject?>()
}.asCompletableFuture()
})
val sessionHashCallbacks = CacheBuilder.newBuilder()
.expireAfterWrite(30, TimeUnit.SECONDS)
.build<String, CompletableFuture<Unit>>(CacheLoader.from { _ -> CompletableFuture() })
val addressCallbacks = CacheBuilder.newBuilder()
.expireAfterWrite(30, TimeUnit.SECONDS)
.build<UUID, CompletableFuture<AddressInfo>>(CacheLoader.from { _ -> CompletableFuture() })
val minecraftAccessTokens = CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.build<UUID, String>()
fun generateToken(account: UUID, username: String): String { fun generateToken(account: UUID, username: String): String {
return JWT.create() return JWT.create()
@ -61,8 +98,6 @@ class WebServer {
return generateToken(info.id, name) return generateToken(info.id, name)
} }
data class UserInfo(val id: UUID, val name: String?, val expiration: Date)
fun parseToken(token: String): UserInfo? { fun parseToken(token: String): UserInfo? {
return try { return try {
val verified = JWT.require(jwtAlgorithm) val verified = JWT.require(jwtAlgorithm)
@ -79,47 +114,6 @@ class WebServer {
} }
} }
// Minecraft account -> WebClient
val listeners = Multimaps.synchronizedSetMultimap(
MultimapBuilder.SetMultimapBuilder
.hashKeys()
.hashSetValues()
.build<UUID, WebClient>()
)
val usernameToIdCache = CacheBuilder.newBuilder()
.expireAfterWrite(1, TimeUnit.HOURS)
.build<String, CompletableFuture<UUID?>>(CacheLoader.from { name ->
CoroutineScope(Dispatchers.IO).async {
AspirinServer.httpClient
.get("https://api.mojang.com/users/profiles/minecraft/$name")
.body<JsonObject?>()?.get("id")?.asString?.let { parseUndashedId(it) }
}.asCompletableFuture()
})
val idToProfileCache = CacheBuilder.newBuilder()
.expireAfterWrite(1, TimeUnit.HOURS)
.build<UUID, CompletableFuture<JsonObject?>>(CacheLoader.from { id ->
CoroutineScope(Dispatchers.IO).async {
AspirinServer.httpClient
.get("https://sessionserver.mojang.com/session/minecraft/profile/$id")
.body<JsonObject?>()
}.asCompletableFuture()
})
val sessionHashCallbacks = CacheBuilder.newBuilder()
.expireAfterWrite(30, TimeUnit.SECONDS)
.build<String, CompletableFuture<Unit>>(CacheLoader.from { _ -> CompletableFuture() })
val addressCallbacks = CacheBuilder.newBuilder()
.expireAfterWrite(30, TimeUnit.SECONDS)
.build<UUID, CompletableFuture<AddressInfo>>(CacheLoader.from { _ -> CompletableFuture() })
data class AddressInfo(
val backVersion: Int,
val backHostAndPort: HostAndPort,
var frontOnline: Boolean? = null,
var backName: String? = null
)
suspend fun requestAddressInfo(frontName: String): CompletableFuture<AddressInfo> { suspend fun requestAddressInfo(frontName: String): CompletableFuture<AddressInfo> {
var onlineId: UUID? = null var onlineId: UUID? = null
try { try {
@ -131,7 +125,7 @@ class WebServer {
val callbackId = UUID.randomUUID() val callbackId = UUID.randomUUID()
val future = addressCallbacks.get(callbackId) val future = addressCallbacks.get(callbackId)
CoroutineScope(coroutineContext).apply { coroutineScope.apply {
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
run sending@{ run sending@{
onlineId?.let { onlineId?.let {
@ -164,16 +158,31 @@ class WebServer {
return sent return sent
} }
suspend fun joinWithCachedToken(playerId: UUID, hash: String): Boolean {
val accessToken = minecraftAccessTokens.getIfPresent(playerId) ?: return false
return AspirinServer.httpClient.post("https://sessionserver.mojang.com/session/minecraft/join") {
setBody(JsonObject().also {
it.addProperty("accessToken", accessToken)
it.addProperty("selectedProfile", playerId.toString().replace("-", ""))
it.addProperty("serverId", hash)
})
contentType(ContentType.Application.Json)
}.bodyAsText().isEmpty()
}
suspend fun requestSessionJoin( suspend fun requestSessionJoin(
frontName: String, frontName: String,
id: UUID, name: String, hash: String, id: UUID, name: String, hash: String,
address: SocketAddress, backAddress: SocketAddress address: SocketAddress, backAddress: SocketAddress
): CompletableFuture<Unit> { ): CompletableFuture<Unit> {
if (frontName.equals(name, ignoreCase = true) && joinWithCachedToken(id, hash)) {
return CompletableFuture.completedFuture(Unit)
}
val future = sessionHashCallbacks[hash] val future = sessionHashCallbacks[hash]
if (!listeners.containsKey(id)) { if (!listeners.containsKey(id)) {
future.completeExceptionally(StacklessException("UUID $id ($frontName) isn't listened. Go to web auth.")) future.completeExceptionally(StacklessException("UUID $id ($frontName) isn't listened. Go to web auth."))
} else { } else {
CoroutineScope(coroutineContext).apply { coroutineScope.apply {
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
var info: JsonObject? = null var info: JsonObject? = null
var ptr: String? = null var ptr: String? = null

View File

@ -92,19 +92,24 @@ frame-src 'self' https://login.microsoftonline.com/ https://login.live.com/"
<p>WebSocket connection status: <span class="text-white bg-dark" id="connection_status">?</span></p> <p>WebSocket connection status: <span class="text-white bg-dark" id="connection_status">?</span></p>
<p>CORS Proxy status: <span class="text-white bg-dark" id="cors_status">?</span></p> <p>CORS Proxy status: <span class="text-white bg-dark" id="cors_status">?</span></p>
<hr> <hr>
<p>Listening to frontend logins from:</p> <p>Listening to front-end logins from:</p>
<div id="listening"></div> <div id="listening"></div>
<div id="actions"> <div id="actions">
<button id="listen_continue" type="button" class="btn btn-primary">Listen to <span id="mcIdUsername"></span> <button id="listen_continue" type="button" class="btn btn-primary">Listen to <span id="mcIdUsername"></span>
</button> </button>
<button id="listen_premium" type="button" class="btn btn-primary">Listen to premium login</button> <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#listenModal"
<button id="listen_offline" type="button" class="btn btn-primary">Listen to offline mode login</button> id="listen_open">Listen to logins
</button>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#sendTokenModal"
id="send_token_open">Send Access Token
</button>
</div> </div>
<hr> <hr>
<p>Connecting to backend server:</p> <p>Connecting to backend server:</p>
<div id="connect"> <div id="connect">
<form class="input-group"> <form class="input-group mb-3">
<input id="connect_address" type="text" class="form-control" placeholder="Address:Port" aria-label="Address and Port"> <input id="connect_address" type="text" class="form-control" placeholder="Address:Port"
aria-label="Address and Port">
<input id="connect_version" type="text" class="form-control" placeholder="Version" aria-label="Version"> <input id="connect_version" type="text" class="form-control" placeholder="Version" aria-label="Version">
<select class="form-select" id="connect_online" aria-label="Online Mode"> <select class="form-select" id="connect_online" aria-label="Online Mode">
<option value="null" selected>Front Online Mode...</option> <option value="null" selected>Front Online Mode...</option>
@ -112,11 +117,12 @@ frame-src 'self' https://login.microsoftonline.com/ https://login.live.com/"
<option value="true">Force online</option> <option value="true">Force online</option>
<option value="false">Force offline (recommended for Geyser)</option> <option value="false">Force offline (recommended for Geyser)</option>
</select> </select>
<input id="connect_user" type="text" class="form-control" placeholder="Back Username" aria-label="Backend Username"> <input id="connect_user" type="text" class="form-control" placeholder="Back Username"
aria-label="Backend Username">
</form> </form>
<p>You can also use <a href="https://jo0001.github.io/ViaSetup/aspirin" id="viasetup_address">the address
generator</a> to specify the server</p>
</div> </div>
<p>You can also use <a href="https://jo0001.github.io/ViaSetup/aspirin" id="viasetup_address">address
generator</a></p>
</div> </div>
<div aria-labelledby="settings-tab" class="tab-pane fade" id="settings"> <div aria-labelledby="settings-tab" class="tab-pane fade" id="settings">
@ -256,6 +262,67 @@ frame-src 'self' https://login.microsoftonline.com/ https://login.live.com/"
</div> </div>
</div> </div>
<div aria-hidden="true" aria-labelledby="listenModalLabel" class="modal fade" id="listenModal" tabindex="-1">
<div class="modal-dialog modal-dialog-scrollable">
<form id="form_listen">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="listenModalLabel">Listen to logins</h5>
<button aria-label="Close" class="btn-close" data-bs-dismiss="modal" type="button"></button>
</div>
<div class="modal-body">
<p>The instance will send impersonation requests to this page. Please note
that they're verified by UUID, so offline and online accounts
are considered different.</p>
<p>VIAaaS will also request the address parameters when they're not specified.
They're verified by username. Online-mode listeners have priority.</p>
<div class="mb-3">
<label for="listen_username" class="form-label">Username</label>
<input type="text" class="form-control" id="listen_username">
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="listen_online" checked>
<label for="listen_online" class="form-check-label">Online Mode</label>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary" data-bs-dismiss="modal">Listen</button>
</div>
</div>
</form>
</div>
</div>
<div aria-hidden="true" aria-labelledby="listenModalLabel" class="modal fade" id="sendTokenModal" tabindex="-1">
<div class="modal-dialog modal-dialog-scrollable">
<form id="form_send_token">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="sendTokenModalLabel">Save Access Token in Instance</h5>
<button aria-label="Close" class="btn-close" data-bs-dismiss="modal" type="button"></button>
</div>
<div class="modal-body">
<p>The instance will cache your Minecraft access token, allowing you to connect without keeping
this page open.</p>
<p>Note that the access token expires in ~1 day and a compromised instance may access your account
with malicious intents.</p>
<p>You'll need to connect as online-mode in the front-end. It's not possible to use the cached
token when changing the backend username</p>
<div class="input-group mb-3">
<label class="input-group-text" for="send_token_user">Account</label>
<select class="form-select" id="send_token_user">
<option selected>Choose...</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary" data-bs-dismiss="modal">Send</button>
</div>
</div>
</form>
</div>
</div>
<div class="toast-container position-absolute top-0 end-0 p-3" id="toasts"></div> <div class="toast-container position-absolute top-0 end-0 p-3" id="toasts"></div>
</body> </body>
</html> </html>

View File

@ -2,7 +2,7 @@
let urlParams = new URLSearchParams(); let urlParams = new URLSearchParams();
window.location.hash.substring(1).split("?") window.location.hash.substring(1).split("?")
.map(it => new URLSearchParams(it) .map(it => new URLSearchParams(it)
.forEach((a, b) => urlParams.append(b, a))); .forEach((a, b) => urlParams.append(b, a)));
let mcIdUsername = urlParams.get("username"); let mcIdUsername = urlParams.get("username");
let mcauth_code = urlParams.get("mcauth_code"); let mcauth_code = urlParams.get("mcauth_code");
let mcauth_success = urlParams.get("mcauth_success"); let mcauth_success = urlParams.get("mcauth_success");
@ -53,6 +53,10 @@ $(() => {
$("#form_add_ms").on("submit", () => loginMs()); $("#form_add_ms").on("submit", () => loginMs());
$("#form_ws_url").on("submit", () => setWsUrl($("#ws-url").val())); $("#form_ws_url").on("submit", () => setWsUrl($("#ws-url").val()));
$("#form_cors_proxy").on("submit", () => setCorsProxy($("#cors-proxy").val())); $("#form_cors_proxy").on("submit", () => setCorsProxy($("#cors-proxy").val()));
$("#form_listen").on("submit", () => submittedListen());
$("#form_send_token").on("submit", () => submittedSendToken());
$("#en_notific").on("click", () => Notification.requestPermission().then(renderActions));
$("#listen_continue").on("click", () => clickedListenContinue());
ohNo(); ohNo();
@ -101,40 +105,54 @@ function addMcAccountToList(account) {
$(accounts).append(line); $(accounts).append(line);
} }
function addListSendToken(username) {
let line = $("<option class='mc_username'></option>");
line.text(username);
$("#send_token_user").append(line);
}
function refreshAccountList() { function refreshAccountList() {
accounts.innerHTML = ""; accounts.innerHTML = "";
$("#send_token_user .mc_username").remove();
getActiveAccounts() getActiveAccounts()
.filter(it => it instanceof MojangAccount)
.sort((a, b) => a.name.localeCompare(b.name)) .sort((a, b) => a.name.localeCompare(b.name))
.forEach(it => addMcAccountToList(it)); .forEach(it => {
getMicrosoftUsers() addMcAccountToList(it)
.sort((a, b) => a.localeCompare(b)) addListSendToken(it.name)
.forEach(username => {
let mcAcc = findAccountByMs(username);
if (!mcAcc) return;
addMcAccountToList(mcAcc);
}); });
} }
$("#en_notific").on("click", () => Notification.requestPermission().then(renderActions));
$("#listen_premium").on("click", () => {
let user = prompt("Premium username (case-sensitive): ", "");
if (!user) return;
let callbackUrl = new URL(location);
callbackUrl.search = "";
callbackUrl.hash = "#username=" + encodeURIComponent(user);
location.href = "https://api.minecraft.id/gateway/start/" + encodeURIComponent(user)
+ "?callback=" + encodeURIComponent(callbackUrl.toString());
});
$("#listen_offline").on("click", () => {
let user = prompt("Offline username (case-sensitive): ", "");
if (!user) return;
let taskId = Math.random();
workers.forEach(it => it.postMessage({action: "listen_pow", user: user, id: taskId, deltaTime: deltaTime}));
addToast("Offline username", "Please wait a minute...");
});
$("#mcIdUsername").text(mcIdUsername); $("#mcIdUsername").text(mcIdUsername);
$("#listen_continue").on("click", () => {
function submittedListen() {
let user = $("#listen_username").val();
if (!user) return;
if ($("#listen_online")[0].checked) {
let callbackUrl = new URL(location);
callbackUrl.search = "";
callbackUrl.hash = "#username=" + encodeURIComponent(user);
location.href = "https://api.minecraft.id/gateway/start/" + encodeURIComponent(user)
+ "?callback=" + encodeURIComponent(callbackUrl.toString());
} else {
let taskId = Math.random();
workers.forEach(it => it.postMessage({action: "listen_pow", user: user, id: taskId, deltaTime: deltaTime}));
addToast("Offline username", "Please wait a minute...");
}
}
function submittedSendToken() {
findAccountByMcName($("#send_token_user").val())
.acquireActiveToken()
.then(acc => {
sendSocket(JSON.stringify({
"action": "save_access_token",
"mc_access_token": acc.accessToken
}))
})
.catch(e => addToast("Failed to send access token", e));
}
function clickedListenContinue() {
sendSocket(JSON.stringify({ sendSocket(JSON.stringify({
"action": "minecraft_id_login", "action": "minecraft_id_login",
"username": mcIdUsername, "username": mcIdUsername,
@ -142,13 +160,13 @@ $("#listen_continue").on("click", () => {
})); }));
mcauth_code = null; mcauth_code = null;
renderActions(); renderActions();
}); }
function renderActions() { function renderActions() {
$("#en_notific").hide(); $("#en_notific").hide();
$("#listen_continue").hide(); $("#listen_continue").hide();
$("#listen_premium").hide(); $("#listen_open").hide();
$("#listen_offline").hide(); $("#send_token_open").hide();
if (Notification.permission === "default") { if (Notification.permission === "default") {
$("#en_notific").show(); $("#en_notific").show();
@ -157,8 +175,8 @@ function renderActions() {
if (mcIdUsername != null && mcauth_code != null) { if (mcIdUsername != null && mcauth_code != null) {
$("#listen_continue").show(); $("#listen_continue").show();
} }
$("#listen_premium").show(); $("#listen_open").show();
$("#listen_offline").show(); $("#send_token_open").show();
} }
} }
@ -358,10 +376,6 @@ function getActiveAccounts() {
return activeAccounts; return activeAccounts;
} }
function getMicrosoftUsers() {
return (myMSALObj.getAllAccounts() || []).map(it => it.username);
}
class McAccount { class McAccount {
constructor(id, username, accessToken) { constructor(id, username, accessToken) {
this.id = id; this.id = id;
@ -589,6 +603,7 @@ function loginMc(user, pass) {
function getLoginRequest() { function getLoginRequest() {
return {scopes: ["XboxLive.signin"]}; return {scopes: ["XboxLive.signin"]};
} }
let redirectUrl = "https://viaversion.github.io/VIAaaS/src/main/resources/web/"; let redirectUrl = "https://viaversion.github.io/VIAaaS/src/main/resources/web/";
if (location.hostname === "localhost" || whitelistedOrigin.includes(location.origin)) { if (location.hostname === "localhost" || whitelistedOrigin.includes(location.origin)) {
redirectUrl = location.origin + location.pathname; redirectUrl = location.origin + location.pathname;