mirror of
https://github.com/ViaVersion/VIAaaS.git
synced 2025-01-23 21:52:35 +01:00
parent
fb85cfa329
commit
237761a3d3
@ -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 }
|
||||||
|
@ -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!!!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
|
@ -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 {
|
||||||
|
10
src/main/kotlin/com/viaversion/aas/web/AddressInfo.kt
Normal file
10
src/main/kotlin/com/viaversion/aas/web/AddressInfo.kt
Normal 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
|
||||||
|
)
|
5
src/main/kotlin/com/viaversion/aas/web/UserInfo.kt
Normal file
5
src/main/kotlin/com/viaversion/aas/web/UserInfo.kt
Normal 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)
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user