fixed sw cache, better ui?

This commit is contained in:
creeper123123321 2021-09-26 21:09:24 -03:00
parent 171c00a148
commit c25745cdc1
8 changed files with 156 additions and 144 deletions

View File

@ -71,7 +71,7 @@ dependencies {
implementation("io.netty:netty-tcnative-boringssl-static:2.0.43.Final")
implementation("io.netty.incubator:netty-incubator-transport-native-io_uring:0.0.8.Final:linux-x86_64")
implementation("com.google.guava:guava:30.1.1-jre")
implementation("com.google.guava:guava:31.0-jre")
implementation("com.velocitypowered:velocity-native:3.0.1")
implementation("net.coobird:thumbnailator:0.4.14")
implementation("org.powernukkit.fastutil:fastutil-lite:8.1.1")

View File

@ -39,22 +39,26 @@ class WebDashboardServer {
val clients = ConcurrentHashMap<WebSocketSession, WebClient>()
val jwtAlgorithm = Algorithm.HMAC256(VIAaaSConfig.jwtSecret)
fun generateToken(account: UUID): String {
fun generateToken(account: UUID, username: String): String {
return JWT.create()
.withIssuedAt(Date())
.withExpiresAt(Date.from(Instant.now().plus(Duration.ofDays(30))))
.withSubject(account.toString())
.withClaim("name", username)
.withAudience("viaaas_listen")
.withIssuer("viaaas")
.sign(jwtAlgorithm)
}
fun checkToken(token: String): UUID? {
data class UserInfo(val id: UUID, val name: String?)
fun parseToken(token: String): UserInfo? {
return try {
val verified = JWT.require(jwtAlgorithm)
.withAnyOfAudience("viaaas_listen")
.build()
.verify(token)
UUID.fromString(verified.subject)
UserInfo(UUID.fromString(verified.subject), verified.getClaim("name").asString())
} catch (e: JWTVerificationException) {
null
}

View File

@ -19,95 +19,108 @@ class WebLogin : WebState {
webClient.ws.flush()
}
private fun loginSuccessJson(username: String, uuid: UUID, token: String): JsonObject {
return JsonObject().also {
it.addProperty("action", "login_result")
it.addProperty("success", true)
it.addProperty("username", username)
it.addProperty("uuid", uuid.toString())
it.addProperty("token", token)
}
}
private fun loginNotSuccess(): JsonObject {
return JsonObject().also {
it.addProperty("action", "login_result")
it.addProperty("success", false)
}
}
private suspend fun handleOfflineLogin(webClient: WebClient, msg: String, obj: JsonObject) {
if (!sha512Hex(msg.toByteArray(Charsets.UTF_8)).startsWith("00000")) throw StacklessException("PoW failed")
if ((obj.getAsJsonPrimitive("date").asLong - System.currentTimeMillis())
.absoluteValue > Duration.ofSeconds(20).toMillis()
) {
throw StacklessException("Invalid PoW date")
}
val username = obj["username"].asString.trim()
val uuid = generateOfflinePlayerUuid(username)
val token = webClient.server.generateToken(uuid, username)
webClient.ws.send(loginSuccessJson(username, uuid, token).toString())
webLogger.info("Token gen: ${webClient.id}: offline $username $uuid")
}
private suspend fun handleMcIdLogin(webClient: WebClient, msg: String, obj: JsonObject) {
val username = obj["username"].asString
val code = obj["code"].asString
val check = AspirinServer.httpClient.submitForm<JsonObject>(
"https://api.minecraft.id/gateway/verify/${URLEncoder.encode(username, Charsets.UTF_8)}",
formParameters = parametersOf("code", code),
)
if (check.getAsJsonPrimitive("valid").asBoolean) {
val mcIdUser = check["username"].asString
val uuid = check["uuid"]?.asString?.let { parseUndashedId(it.replace("-", "")) }
?: webClient.server.usernameIdCache[mcIdUser].await()
?: throw StacklessException("Failed to get UUID from minecraft.id")
val token = webClient.server.generateToken(uuid, mcIdUser)
webClient.ws.send(loginSuccessJson(mcIdUser, uuid, token).toString())
webLogger.info("Token gen: ${webClient.id}: $mcIdUser $uuid")
} else {
webClient.ws.send(loginNotSuccess().toString())
webLogger.info("Token gen fail: ${webClient.id}: $username")
}
}
private suspend fun handleListenLogins(webClient: WebClient, msg: String, obj: JsonObject) {
val token = obj.getAsJsonPrimitive("token").asString
val user = webClient.server.parseToken(token)
val response = JsonObject().also {
it.addProperty("action", "listen_login_requests_result")
it.addProperty("token", token)
}
if (user != null && webClient.listenId(user.id)) {
response.addProperty("success", true)
response.addProperty("user", user.id.toString())
response.addProperty("username", user.name)
webLogger.info("Listen: ${webClient.id}: $user")
} else {
response.addProperty("success", false)
webLogger.info("Listen fail: ${webClient.id}")
}
webClient.ws.send(response.toString())
}
private suspend fun handleUnlisten(webClient: WebClient, msg: String, obj: JsonObject) {
val uuid = UUID.fromString(obj.getAsJsonPrimitive("uuid").asString)
webLogger.info("Unlisten: ${webClient.id}: $uuid")
val response = JsonObject().also {
it.addProperty("action", "unlisten_login_requests_result")
it.addProperty("uuid", uuid.toString())
it.addProperty("success", webClient.unlistenId(uuid))
}
webClient.ws.send(response.toString())
}
private suspend fun handleSessionResponse(webClient: WebClient, msg: String, obj: JsonObject) {
val hash = obj["session_hash"].asString
webClient.server.sessionHashCallbacks.getIfPresent(hash)?.complete(Unit)
}
override suspend fun onMessage(webClient: WebClient, msg: String) {
val obj = JsonParser.parseString(msg) as JsonObject
when (obj.getAsJsonPrimitive("action").asString) {
"offline_login" -> {
if (!sha512Hex(msg.toByteArray(Charsets.UTF_8)).startsWith("00000")) throw StacklessException("PoW failed")
if ((obj.getAsJsonPrimitive("date").asLong - System.currentTimeMillis())
.absoluteValue > Duration.ofSeconds(20).toMillis()
) {
throw StacklessException("Invalid PoW date")
}
val username = obj["username"].asString.trim()
val uuid = generateOfflinePlayerUuid(username)
val token = webClient.server.generateToken(uuid)
webClient.ws.send(JsonObject().also {
it.addProperty("action", "login_result")
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")
}
"minecraft_id_login" -> {
val username = obj["username"].asString
val code = obj["code"].asString
val check = AspirinServer.httpClient.submitForm<JsonObject>(
"https://api.minecraft.id/gateway/verify/${URLEncoder.encode(username, Charsets.UTF_8)}",
formParameters = parametersOf("code", code),
)
if (check.getAsJsonPrimitive("valid").asBoolean) {
val mcIdUser = check["username"].asString
val uuid = check["uuid"]?.asString?.let { parseUndashedId(it.replace("-", "")) }
?: webClient.server.usernameIdCache[mcIdUser].await()
?: throw StacklessException("Failed to get UUID from minecraft.id")
val token = webClient.server.generateToken(uuid)
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(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 = 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)) {
response.addProperty("success", true)
response.addProperty("user", user.toString())
webLogger.info("Listen: ${webClient.id}: $user")
} else {
response.addProperty("success", false)
webLogger.info("Listen fail: ${webClient.id}")
}
webClient.ws.send(response.toString())
}
"unlisten_login_requests" -> {
val uuid = UUID.fromString(obj.getAsJsonPrimitive("uuid").asString)
webLogger.info("Unlisten: ${webClient.id}: $uuid")
val response = JsonObject().also {
it.addProperty("action", "unlisten_login_requests_result")
it.addProperty("uuid", uuid.toString())
it.addProperty("success", webClient.unlistenId(uuid))
}
webClient.ws.send(response.toString())
}
"session_hash_response" -> {
val hash = obj["session_hash"].asString
webClient.server.sessionHashCallbacks.getIfPresent(hash)?.complete(Unit)
}
"offline_login" -> handleOfflineLogin(webClient, msg, obj)
"minecraft_id_login" -> handleMcIdLogin(webClient, msg, obj)
"listen_login_requests" -> handleListenLogins(webClient, msg, obj)
"unlisten_login_requests" -> handleUnlisten(webClient, msg, obj)
"session_hash_response" -> handleSessionResponse(webClient, msg, obj)
else -> throw StacklessException("invalid action!")
}

View File

@ -24,7 +24,7 @@ script-src 'self' https://*.cloudflare.com/ https://alcdn.msauth.net/ https://*.
<meta content="#0468a1" name="theme-color">
<!-- https://www.srihash.org/ -->
<link class="async-css" rel="preload" as="stylesheet"
<link class="async-css" rel="preload" as="style"
href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.1.1/css/bootstrap.min.css"
integrity="sha512-6KY5s6UI5J7SVYuZB4S/CZMyPylqyyNZco376NM2Z8Sb8OxEdp02e1jkKk/wZxIEmjQ6DRCEBhni+gpr9c4tvA=="
crossorigin="anonymous" referrerpolicy="no-referrer"/>
@ -48,8 +48,6 @@ script-src 'self' https://*.cloudflare.com/ https://alcdn.msauth.net/ https://*.
crossorigin="anonymous"></script>
<script defer src="js/config.js"></script>
<script defer src="js/page.js"></script>
<link rel="preload" href="js/worker.js" as="worker">
<link rel="preload" href="sw.js" as="serviceworker">
</head>
<body>
<nav class="navbar navbar-expand-md navbar-dark bg-dark mb-2">

View File

@ -39,7 +39,8 @@ $(() => {
});
$(() => {
if (navigator.serviceWorker) {
navigator.serviceWorker.register("sw.js").then(() => swCacheFiles());
navigator.serviceWorker.register("sw.js")
.then(() => setTimeout(() => swRefreshFiles(), 1000));
}
})
@ -48,7 +49,6 @@ window.addEventListener('beforeinstallprompt', e => e.preventDefault());
$(() => {
$(".async-css").attr("rel", "stylesheet");
$("form").on("submit", e => e.preventDefault());
$("a[href='javascript:']").on("click", e => e.preventDefault());
cors_proxy_txt.value = getCorsProxy();
ws_url_txt.value = getWsUrl();
@ -70,11 +70,12 @@ $(() => {
connect();
})
function swCacheFiles() {
function swRefreshFiles() {
// https://stackoverflow.com/questions/46830493/is-there-any-way-to-cache-all-files-of-defined-folder-path-in-service-worker
navigator.serviceWorker.ready.then(ready => ready.active.postMessage({
action: "cache",
urls: performance.getEntriesByType("resource").map(it => it.name)
})); // https://stackoverflow.com/questions/46830493/is-there-any-way-to-cache-all-files-of-defined-folder-path-in-service-worker
}));
}
function setWsStatus(txt) {
@ -96,7 +97,7 @@ function addMcAccountToList(account) {
let line = $(`<li class='input-group d-flex'>
<span class='input-group-text'><img loading="lazy" width=24 class='mc-head'/></span>
<span class='form-control mc-user'></span>
<a class='btn btn-danger mc-remove' href='javascript:'>Logout</a>
<button type="button" class='btn btn-danger mc-remove'>Logout</button>
</li>`);
let txt = account.name;
if (account instanceof MicrosoftAccount) txt += " (" + account.msUser + ")";
@ -126,8 +127,7 @@ function refreshAccountList() {
function renderActions() {
actions.innerHTML = "";
if (Notification.permission === "default") {
actions.innerHTML += '<p><a href="javascript:" id="notificate">Enable notifications</a></p>';
$("#notificate").on("click", () => Notification.requestPermission().then(renderActions)); // i'm lazy
addAction("Enable notifications", () => Notification.requestPermission().then(renderActions));
}
if (listenVisible) {
if (mcIdUsername != null && mcauth_code != null) {
@ -141,7 +141,7 @@ function renderActions() {
renderActions();
});
}
addAction("Listen to frontend premium login in VIAaaS instance", () => {
addAction("Listen to frontend premium login", () => {
let user = prompt("Premium username (case-sensitive): ", "");
if (!user) return;
let callbackUrl = new URL(location);
@ -150,7 +150,7 @@ function renderActions() {
location.href = "https://api.minecraft.id/gateway/start/" + encodeURIComponent(user)
+ "?callback=" + encodeURIComponent(callbackUrl);
});
addAction("Listen to frontend offline login in VIAaaS instance", () => {
addAction("Listen to frontend offline login", () => {
let user = prompt("Offline username (case-sensitive):", "");
if (!user) return;
let taskId = Math.random();
@ -171,21 +171,17 @@ function onCompletedPoW(e) {
}
function addAction(text, onClick) {
let p = document.createElement("p");
let link = document.createElement("a");
p.appendChild(link);
link.innerText = text;
link.href = "javascript:";
link.onclick = onClick;
actions.appendChild(p);
let line = $("<button type='button' class='btn btn-primary'></button>")
line.text(text).on("click", onClick);
$(actions).append(line);
}
function addListeningList(user, token) {
let line = $("<p><img loading='lazy' width=24 class='head'/> <span class='username'></span> <a href='javascript:'>Unlisten</a></p>");
line.find(".username").text(user);
line.find("a").on("click", () => {
function addListeningList(user, username, token) {
let line = $("<p><img loading='lazy' width=24 class='head'/> <span class='username'></span> <button class='btn btn-danger' type='button'>Unlisten</button></p>");
line.find(".username").text(username || user);
line.find(".btn").on("click", () => {
removeToken(token);
listening.removeChild(p);
line.remove();
unlisten(user);
});
let head = line.find(".head");
@ -719,7 +715,7 @@ function onSocketMsg(event) {
}
} else if (parsed.action === "listen_login_requests_result") {
if (parsed.success) {
addListeningList(parsed.user, parsed.token);
addListeningList(parsed.user, parsed.username, parsed.token);
} else {
removeToken(parsed.token);
}

View File

@ -37,10 +37,4 @@ function listenPoW(e) {
} while (!sha512(msg).startsWith("00000"));
postMessage({id: e.data.id, action: "completed_pow", msg: msg});
}
/* function sha512(s) {
const shaObj = new jsSHA("SHA-512", "TEXT", { encoding: "UTF8" });
shaObj.update(s);
return shaObj.getHash("HEX");
} */
}

View File

@ -7,7 +7,7 @@
"type": "image/png",
"purpose": "any maskable"
}],
"start_url": "./index.html",
"start_url": "./",
"background_color": "#3367D6",
"display": "standalone",
"theme_color": "#0468a1",

View File

@ -1,49 +1,56 @@
// https://stackoverflow.com/questions/42127148/service-worker-communicate-to-clients
let viac = new BroadcastChannel("viaaas-notification");
self.addEventListener("notificationclick", event => {
event.preventDefault();
event.notification.close();
viac.postMessage({tag: event.notification.tag, action: event.action});
event.preventDefault();
event.notification.close();
viac.postMessage({tag: event.notification.tag, action: event.action});
});
// stolen from https://github.com/mozilla/serviceworker-cookbook/blob/master/strategy-network-or-cache/service-worker.js (MIT license)
var CACHE = "network-or-cache";
let cacheId = "viaaas";
self.addEventListener("install", evt => {
evt.waitUntil(cache(["./index.html"]));
self.addEventListener("install", () => {
});
self.addEventListener("fetch", evt => {
return; // todo fix
if (!shouldCache(evt.request.url)
|| evt.request.method != "GET") return;
evt.respondWith(
fromCache(evt.request).catch(() => fromNetwork(evt.request))
);
if (!shouldCache(evt.request.url)
|| evt.request.method !== "GET") return;
evt.respondWith(
fromCache(evt.request).catch(() => fromNetwork(evt.request))
);
});
addEventListener("message", e => {
if (e.data.action == "cache") {
e.waitUntil(cache(e.data.urls));
}
if (e.data.action === "cache") {
e.waitUntil(cache(e.data.urls));
}
});
function shouldCache(it) {
return it.endsWith(".js") || it.endsWith(".css") || it.endsWith(".png") || it.endsWith(".html")
return [".js", ".css", ".png", ".html", ".webp", "manifest.json"].findIndex(end => it.endsWith(end)) !== -1
|| it === new URL("./", self.location).toString()
}
function cache(urls) {
return; // todo fix
return caches.open(CACHE).then(cache => cache.addAll(urls.filter(shouldCache)));
let filtered = Array.from(new Set(urls.filter(shouldCache)));
return caches.open(cacheId).then(cache => cache.addAll(filtered));
}
function fromNetwork(request) {
return fetch(request);
return fetch(request)
.then(response => {
if (!shouldCache(response.url)) return response;
// Let's cache it when loading for the first time
return caches.open(cacheId)
.then(it => it.add(request))
.then(() => response);
});
}
function fromCache(request) {
return caches.open(CACHE)
.then(cache => cache.match(request))
.then(matching => matching || Promise.reject("no-match"));
return caches.open(cacheId)
.then(cache => cache.match(request))
.then(matching => matching || Promise.reject("no-match"));
}