From dcd0a4270526e5e8f46e7526e319d89382c954aa Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Fri, 23 Nov 2018 21:34:54 +0800 Subject: [PATCH] Refactor AuthlibInjectorServer --- .../org/jackhuang/hmcl/setting/Accounts.java | 12 +- .../org/jackhuang/hmcl/setting/Config.java | 2 +- .../AuthlibInjectorAccount.java | 14 +- .../AuthlibInjectorServer.java | 147 ++++++++++++++---- 4 files changed, 125 insertions(+), 50 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java index 9a87c3542..b0a81bbcc 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java @@ -38,7 +38,6 @@ import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory; import org.jackhuang.hmcl.task.Schedulers; -import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.Optional; @@ -235,16 +234,7 @@ public final class Accounts { .filter(server -> url.equals(server.getUrl())) .findFirst() .orElseGet(() -> { - // this usually happens when migrating data from an older version - AuthlibInjectorServer server; - try { - server = AuthlibInjectorServer.fetchServerInfo(url); - LOG.info("Migrated authlib injector server " + server); - } catch (IOException e) { - server = new AuthlibInjectorServer(url, url); - LOG.log(Level.WARNING, "Failed to migrate authlib injector server " + url, e); - } - + AuthlibInjectorServer server = new AuthlibInjectorServer(url); config().getAuthlibInjectorServers().add(server); return server; }); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java index 3d41274da..4c42faec4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java @@ -137,7 +137,7 @@ public final class Config implements Cloneable, Observable { private IntegerProperty logLines = new SimpleIntegerProperty(100); @SerializedName("authlibInjectorServers") - private ObservableList authlibInjectorServers = FXCollections.observableArrayList(); + private ObservableList authlibInjectorServers = FXCollections.observableArrayList(server -> new Observable[] { server }); @SerializedName("updateChannel") private ObjectProperty updateChannel = new SimpleObjectProperty<>(UpdateChannel.STABLE); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorAccount.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorAccount.java index 9e2168d13..f8fff41de 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorAccount.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorAccount.java @@ -27,14 +27,12 @@ import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilSession; import org.jackhuang.hmcl.game.Arguments; import org.jackhuang.hmcl.util.function.ExceptionalSupplier; import java.io.IOException; -import java.io.InputStream; -import java.net.URL; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutionException; -import static org.jackhuang.hmcl.util.io.IOUtils.readFullyWithoutClosing; +import static java.nio.charset.StandardCharsets.UTF_8; public class AuthlibInjectorAccount extends YggdrasilAccount { private AuthlibInjectorServer server; @@ -58,9 +56,9 @@ public class AuthlibInjectorAccount extends YggdrasilAccount { } private AuthInfo inject(ExceptionalSupplier loginAction) throws AuthenticationException { - CompletableFuture prefetchedMetaTask = CompletableFuture.supplyAsync(() -> { - try (InputStream in = new URL(server.getUrl()).openStream()) { - return readFullyWithoutClosing(in); + CompletableFuture prefetchedMetaTask = CompletableFuture.supplyAsync(() -> { + try { + return server.fetchMetadataResponse(); } catch (IOException e) { throw new CompletionException(new ServerDisconnectException(e)); } @@ -75,7 +73,7 @@ public class AuthlibInjectorAccount extends YggdrasilAccount { }); AuthInfo auth = loginAction.get(); - byte[] prefetchedMeta; + String prefetchedMeta; AuthlibInjectorArtifactInfo artifact; try { @@ -95,7 +93,7 @@ public class AuthlibInjectorAccount extends YggdrasilAccount { return auth.withArguments(new Arguments().addJVMArguments( "-javaagent:" + artifact.getLocation().toString() + "=" + server.getUrl(), "-Dauthlibinjector.side=client", - "-Dorg.to2mbn.authlibinjector.config.prefetched=" + Base64.getEncoder().encodeToString(prefetchedMeta))); + "-Dorg.to2mbn.authlibinjector.config.prefetched=" + Base64.getEncoder().encodeToString(prefetchedMeta.getBytes(UTF_8)))); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorServer.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorServer.java index 0c4decdcc..5777d25b4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorServer.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorServer.java @@ -20,25 +20,40 @@ package org.jackhuang.hmcl.auth.authlibinjector; import static java.nio.charset.StandardCharsets.UTF_8; import static org.jackhuang.hmcl.util.Lang.tryCast; import static org.jackhuang.hmcl.util.Logging.LOG; +import static org.jackhuang.hmcl.util.io.IOUtils.readFullyAsByteArray; import static org.jackhuang.hmcl.util.io.IOUtils.readFullyWithoutClosing; import java.io.IOException; -import java.io.InputStream; +import java.lang.reflect.Type; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.util.Optional; +import java.util.logging.Level; -import org.jackhuang.hmcl.util.gson.JsonUtils; +import org.jackhuang.hmcl.util.javafx.ObservableHelper; +import org.jetbrains.annotations.Nullable; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import com.google.gson.JsonPrimitive; +import com.google.gson.annotations.JsonAdapter; -public class AuthlibInjectorServer { +import javafx.application.Platform; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; + +@JsonAdapter(AuthlibInjectorServer.Deserializer.class) +public class AuthlibInjectorServer implements Observable { private static final int MAX_REDIRECTS = 5; + private static final Gson GSON = new GsonBuilder().create(); public static AuthlibInjectorServer locateServer(String url) throws IOException { url = parseInputUrl(url); @@ -60,8 +75,11 @@ public class AuthlibInjectorServer { } } + try { - return parseResponse(url, readFullyWithoutClosing(conn.getInputStream())); + AuthlibInjectorServer server = new AuthlibInjectorServer(url); + server.refreshMetadata(readFullyWithoutClosing(conn.getInputStream())); + return server; } finally { conn.disconnect(); } @@ -107,45 +125,82 @@ public class AuthlibInjectorServer { } public static AuthlibInjectorServer fetchServerInfo(String url) throws IOException { - try (InputStream in = new URL(url).openStream()) { - return parseResponse(url, readFullyWithoutClosing(in)); - } - } - - private static AuthlibInjectorServer parseResponse(String url, byte[] rawResponse) throws IOException { - try { - JsonObject response = JsonUtils.fromNonNullJson(new String(rawResponse, UTF_8), JsonObject.class); - String name = extractServerName(response).orElse(url); - return new AuthlibInjectorServer(url, name); - } catch (JsonParseException e) { - throw new IOException("Malformed response", e); - } - } - - private static Optional extractServerName(JsonObject response){ - return tryCast(response.get("meta"), JsonObject.class) - .flatMap(meta -> tryCast(meta.get("serverName"), JsonPrimitive.class).map(JsonPrimitive::getAsString)); + AuthlibInjectorServer server = new AuthlibInjectorServer(url); + server.refreshMetadata(); + return server; } private String url; - private String name; + @Nullable + private String metadataResponse; + private long metadataTimestamp; - public AuthlibInjectorServer(String url, String name) { + @Nullable + private transient String name; + + private transient boolean metadataRefreshed; + private transient ObservableHelper helper = new ObservableHelper(this); + + public AuthlibInjectorServer(String url) { this.url = url; - this.name = name; } public String getUrl() { return url; } - public String getName() { - return name; + public Optional getMetadataResponse() { + return Optional.ofNullable(metadataResponse); } - @Override - public String toString() { - return url + " (" + name + ")"; + public long getMetadataTimestamp() { + return metadataTimestamp; + } + + public String getName() { + return Optional.ofNullable(name) + .orElse(url); + } + + public String fetchMetadataResponse() throws IOException { + if (metadataResponse == null || !metadataRefreshed) { + refreshMetadata(); + } + return getMetadataResponse().get(); + } + + public void refreshMetadata() throws IOException { + refreshMetadata(readFullyAsByteArray(new URL(url).openStream())); + } + + private void refreshMetadata(byte[] rawResponse) throws IOException { + long timestamp = System.currentTimeMillis(); + try { + setMetadataResponse(new String(rawResponse, UTF_8), timestamp); + } catch (JsonParseException e) { + throw new IOException("Malformed response", e); + } + + metadataRefreshed = true; + LOG.info("authlib-injector server metadata refreshed: " + url); + Platform.runLater(helper::invalidate); + } + + private void setMetadataResponse(String metadataResponse, long metadataTimestamp) throws JsonParseException { + JsonObject response = GSON.fromJson(metadataResponse, JsonObject.class); + if (response == null) { + throw new JsonParseException("Metadata response is empty"); + } + + synchronized (this) { + this.metadataResponse = metadataResponse; + this.metadataTimestamp = metadataTimestamp; + + Optional metaObject = tryCast(response.get("meta"), JsonObject.class); + + this.name = metaObject.flatMap(meta -> tryCast(meta.get("serverName"), JsonPrimitive.class).map(JsonPrimitive::getAsString)) + .orElse(null); + } } @Override @@ -162,4 +217,36 @@ public class AuthlibInjectorServer { AuthlibInjectorServer another = (AuthlibInjectorServer) obj; return this.url.equals(another.url); } + + @Override + public void addListener(InvalidationListener listener) { + helper.addListener(listener); + } + + @Override + public void removeListener(InvalidationListener listener) { + helper.removeListener(listener); + } + + public static class Deserializer implements JsonDeserializer { + @Override + public AuthlibInjectorServer deserialize(JsonElement json, Type type, JsonDeserializationContext ctx) throws JsonParseException { + JsonObject jsonObj = json.getAsJsonObject(); + AuthlibInjectorServer instance = new AuthlibInjectorServer(jsonObj.get("url").getAsString()); + + if (jsonObj.has("name")) { + instance.name = jsonObj.get("name").getAsString(); + } + + if (jsonObj.has("metadataResponse")) { + try { + instance.setMetadataResponse(jsonObj.get("metadataResponse").getAsString(), jsonObj.get("metadataTimestamp").getAsLong()); + } catch (JsonParseException e) { + LOG.log(Level.WARNING, "Ignoring malformed metadata response cache: " + jsonObj.get("metadataResponse"), e); + } + } + return instance; + } + + } }