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 22bb1cfd0..2c441b699 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Accounts.java @@ -22,19 +22,14 @@ import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.AccountFactory; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccountFactory; -import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorBuildInfo; +import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloader; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer; import org.jackhuang.hmcl.auth.offline.OfflineAccount; import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory; import org.jackhuang.hmcl.auth.yggdrasil.MojangYggdrasilProvider; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory; -import org.jackhuang.hmcl.task.FileDownloadTask; -import org.jackhuang.hmcl.util.FileUtils; - -import java.io.File; import java.io.IOException; -import java.net.URL; import java.util.Map; import java.util.logging.Level; @@ -55,7 +50,9 @@ public final class Accounts { public static final Map> ACCOUNT_FACTORY = mapOf( pair(OFFLINE_ACCOUNT_KEY, OfflineAccountFactory.INSTANCE), pair(YGGDRASIL_ACCOUNT_KEY, new YggdrasilAccountFactory(MojangYggdrasilProvider.INSTANCE)), - pair(AUTHLIB_INJECTOR_ACCOUNT_KEY, new AuthlibInjectorAccountFactory(Accounts::downloadAuthlibInjector, Accounts::getOrCreateAuthlibInjectorServer)) + pair(AUTHLIB_INJECTOR_ACCOUNT_KEY, new AuthlibInjectorAccountFactory( + new AuthlibInjectorDownloader(Launcher.HMCL_DIRECTORY.toPath(), () -> Settings.INSTANCE.getDownloadProvider())::getArtifactInfo, + Accounts::getOrCreateAuthlibInjectorServer)) ); public static String getAccountType(Account account) { @@ -73,22 +70,6 @@ public final class Accounts { return username + ":" + character; } - private static String downloadAuthlibInjector() throws Exception { - AuthlibInjectorBuildInfo buildInfo = AuthlibInjectorBuildInfo.requestBuildInfo(); - File jar = new File(Launcher.HMCL_DIRECTORY, "authlib-injector.jar"); - File local = new File(Launcher.HMCL_DIRECTORY, "authlib-injector.txt"); - int buildNumber = 0; - try { - buildNumber = Integer.parseInt(FileUtils.readText(local)); - } catch (IOException | NumberFormatException ignore) { - } - if (buildNumber < buildInfo.getBuildNumber()) { - new FileDownloadTask(new URL(buildInfo.getUrl()), jar).run(); - FileUtils.writeText(local, String.valueOf(buildInfo.getBuildNumber())); - } - return jar.getAbsolutePath(); - } - private static AuthlibInjectorServer getOrCreateAuthlibInjectorServer(String url) { return Settings.SETTINGS.authlibInjectorServers.stream() .filter(server -> url.equals(server.getUrl())) 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 68a26596b..6ab45f496 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 @@ -31,19 +31,23 @@ import org.jackhuang.hmcl.util.NetworkUtils; import java.util.Base64; import java.util.Map; +import java.util.Optional; import java.util.UUID; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; import static java.nio.charset.StandardCharsets.UTF_8; +import static org.jackhuang.hmcl.util.Logging.LOG; + +import java.io.IOException; public class AuthlibInjectorAccount extends YggdrasilAccount { - private final AuthlibInjectorServer server; - private final ExceptionalSupplier injectorJarPath; + private AuthlibInjectorServer server; + private ExceptionalSupplier authlibInjectorDownloader; - protected AuthlibInjectorAccount(YggdrasilService service, AuthlibInjectorServer server, ExceptionalSupplier injectorJarPath, String username, UUID characterUUID, YggdrasilSession session) { + protected AuthlibInjectorAccount(YggdrasilService service, AuthlibInjectorServer server, ExceptionalSupplier authlibInjectorDownloader, String username, UUID characterUUID, YggdrasilSession session) { super(service, username, characterUUID, session); - this.injectorJarPath = injectorJarPath; + this.authlibInjectorDownloader = authlibInjectorDownloader; this.server = server; } @@ -57,25 +61,42 @@ public class AuthlibInjectorAccount extends YggdrasilAccount { return inject(() -> super.logInWithPassword(password, selector)); } - private AuthInfo inject(ExceptionalSupplier supplier) throws AuthenticationException { - // Authlib Injector recommends launchers to pre-fetch the server basic information before launched the game to save time. - GetTask getTask = new GetTask(NetworkUtils.toURL(server.getUrl())); - AtomicBoolean flag = new AtomicBoolean(true); - Thread thread = Lang.thread(() -> flag.set(getTask.test())); + private AuthInfo inject(ExceptionalSupplier loginAction) throws AuthenticationException { + // Pre-fetch metadata + GetTask metadataFetchTask = new GetTask(NetworkUtils.toURL(server.getUrl())); + Thread metadataFetchThread = Lang.thread(() -> { + try { + metadataFetchTask.run(); + } catch (Exception e) { + LOG.log(Level.WARNING, "Failed to pre-fetch Yggdrasil metadata", e); + } + }, "Yggdrasil metadata fetch thread"); - AuthInfo info = supplier.get(); + // Update authlib-injector + AuthlibInjectorArtifactInfo artifact; try { - thread.join(); - - Arguments arguments = new Arguments().addJVMArguments("-javaagent:" + injectorJarPath.get() + "=" + server.getUrl()); - - if (flag.get()) - arguments = arguments.addJVMArguments("-Dorg.to2mbn.authlibinjector.config.prefetched=" + new String(Base64.getEncoder().encode(getTask.getResult().getBytes()), UTF_8)); - - return info.withArguments(arguments); - } catch (Exception e) { - throw new AuthenticationException("Unable to get authlib injector jar path", e); + artifact = authlibInjectorDownloader.get(); + } catch (IOException e) { + throw new AuthenticationException("Failed to download authlib-injector", e); } + + // Perform authentication + AuthInfo info = loginAction.get(); + Arguments arguments = new Arguments().addJVMArguments("-javaagent:" + artifact.getLocation().toString() + "=" + server.getUrl()); + + // Wait for metadata to be fetched + try { + metadataFetchThread.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + Optional metadata = Optional.ofNullable(metadataFetchTask.getResult()); + if (metadata.isPresent()) { + arguments = arguments.addJVMArguments( + "-Dorg.to2mbn.authlibinjector.config.prefetched=" + Base64.getEncoder().encodeToString(metadata.get().getBytes(UTF_8))); + } + + return info.withArguments(arguments); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorAccountFactory.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorAccountFactory.java index 1cecb7fc6..e1af5d982 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorAccountFactory.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorAccountFactory.java @@ -7,6 +7,7 @@ import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilSession; import org.jackhuang.hmcl.util.ExceptionalSupplier; +import java.io.IOException; import java.net.Proxy; import java.util.Map; import java.util.Objects; @@ -15,14 +16,14 @@ import java.util.function.Function; import static org.jackhuang.hmcl.util.Lang.tryCast; public class AuthlibInjectorAccountFactory extends AccountFactory { - private final ExceptionalSupplier injectorJarPathSupplier; + private ExceptionalSupplier authlibInjectorDownloader; private Function serverLookup; /** * @param serverLookup a function that looks up {@link AuthlibInjectorServer} by url */ - public AuthlibInjectorAccountFactory(ExceptionalSupplier injectorJarPathSupplier, Function serverLookup) { - this.injectorJarPathSupplier = injectorJarPathSupplier; + public AuthlibInjectorAccountFactory(ExceptionalSupplier authlibInjectorDownloader, Function serverLookup) { + this.authlibInjectorDownloader = authlibInjectorDownloader; this.serverLookup = serverLookup; } @@ -36,7 +37,7 @@ public class AuthlibInjectorAccountFactory extends AccountFactory + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.auth.authlibinjector; + +import java.nio.file.Path; + +public class AuthlibInjectorArtifactInfo { + + private int buildNumber; + private String version; + private Path location; + + public AuthlibInjectorArtifactInfo(int buildNumber, String version, Path location) { + this.buildNumber = buildNumber; + this.version = version; + this.location = location; + } + + public int getBuildNumber() { + return buildNumber; + } + + public String getVersion() { + return version; + } + + public Path getLocation() { + return location; + } + + @Override + public String toString() { + return "authlib-injector [buildNumber=" + buildNumber + ", version=" + version + "]"; + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorBuildInfo.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorBuildInfo.java deleted file mode 100644 index 2fdac8fcf..000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorBuildInfo.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Hello Minecraft! Launcher. - * Copyright (C) 2018 huangyuhui - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see {http://www.gnu.org/licenses/}. - */ -package org.jackhuang.hmcl.auth.authlibinjector; - -import com.google.gson.JsonParseException; -import org.jackhuang.hmcl.util.Immutable; -import org.jackhuang.hmcl.util.JsonUtils; -import org.jackhuang.hmcl.util.NetworkUtils; - -import java.io.IOException; - -@Immutable -public final class AuthlibInjectorBuildInfo { - - private final int buildNumber; - private final String url; - - public AuthlibInjectorBuildInfo() { - this(0, ""); - } - - public AuthlibInjectorBuildInfo(int buildNumber, String url) { - this.buildNumber = buildNumber; - this.url = url; - } - - public int getBuildNumber() { - return buildNumber; - } - - public String getUrl() { - return url; - } - - public static AuthlibInjectorBuildInfo requestBuildInfo() throws IOException, JsonParseException { - return requestBuildInfo(UPDATE_URL); - } - - public static AuthlibInjectorBuildInfo requestBuildInfo(String updateUrl) throws IOException, JsonParseException { - return JsonUtils.fromNonNullJson(NetworkUtils.doGet(NetworkUtils.toURL(updateUrl)), AuthlibInjectorBuildInfo.class); - } - - public static final String UPDATE_URL = "https://authlib-injector.to2mbn.org/api/buildInfo"; -} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorDownloader.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorDownloader.java new file mode 100644 index 000000000..e7a657b62 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorDownloader.java @@ -0,0 +1,158 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2018 huangyuhui + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.auth.authlibinjector; + +import static org.jackhuang.hmcl.util.Logging.LOG; + +import java.io.IOException; +import java.net.Proxy; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.Optional; +import java.util.function.Supplier; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.logging.Level; + +import org.jackhuang.hmcl.download.DownloadProvider; +import org.jackhuang.hmcl.task.FileDownloadTask; +import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck; +import org.jackhuang.hmcl.util.JsonUtils; +import org.jackhuang.hmcl.util.NetworkUtils; + +import com.google.gson.JsonParseException; +import com.google.gson.annotations.SerializedName; + +public class AuthlibInjectorDownloader { + + private static final String LATEST_BUILD_URL = "https://authlib-injector.yushi.moe/artifact/latest.json"; + + private Path artifactLocation; + private Supplier downloadProvider; + + /** + * @param artifactsDirectory where to save authlib-injector artifacts + */ + public AuthlibInjectorDownloader(Path artifactsDirectory, Supplier downloadProvider) { + this.artifactLocation = artifactsDirectory.resolve("authlib-injector.jar"); + this.downloadProvider = downloadProvider; + } + + public AuthlibInjectorArtifactInfo getArtifactInfo() throws IOException { + synchronized (artifactLocation) { + Optional local = getLocalArtifact(); + + try { + update(local); + } catch (IOException e) { + LOG.log(Level.WARNING, "Failed to download authlib-injector", e); + if (!local.isPresent()) { + throw e; + } + LOG.warning("Fallback to use cached artifact: " + local.get()); + } + + return getLocalArtifact().orElseThrow(() -> new IOException("The updated authlib-inejector cannot be recognized")); + } + } + + private void update(Optional local) throws IOException { + AuthlibInjectorVersionInfo latest = getLatestArtifactInfo(); + + if (local.isPresent() && local.get().getBuildNumber() >= latest.buildNumber) { + return; + } + + try { + new FileDownloadTask(new URL(downloadProvider.get().injectURL(latest.downloadUrl)), artifactLocation.toFile(), Proxy.NO_PROXY, + Optional.ofNullable(latest.checksums.get("sha256")) + .map(checksum -> new IntegrityCheck("SHA-256", checksum)) + .orElse(null)) + .run(); + } catch (Exception e) { + throw new IOException("Failed to download authlib-injector", e); + } + + LOG.info("Updated authlib-injector to " + latest.version); + } + + private AuthlibInjectorVersionInfo getLatestArtifactInfo() throws IOException { + try { + return JsonUtils.fromNonNullJson( + NetworkUtils.doGet( + new URL(downloadProvider.get().injectURL(LATEST_BUILD_URL))), + AuthlibInjectorVersionInfo.class); + } catch (JsonParseException e) { + throw new IOException("Malformed response", e); + } + } + + private Optional getLocalArtifact() { + if (!Files.isRegularFile(artifactLocation)) { + return Optional.empty(); + } + try { + return Optional.of(readArtifactInfo(artifactLocation)); + } catch (IOException e) { + LOG.log(Level.WARNING, "Bad authlib-injector artifact", e); + return Optional.empty(); + } + } + + private static AuthlibInjectorArtifactInfo readArtifactInfo(Path location) throws IOException { + try (JarFile jarFile = new JarFile(location.toFile())) { + Attributes attributes = jarFile.getManifest().getMainAttributes(); + + String title = Optional.ofNullable(attributes.getValue("Implementation-Title")) + .orElseThrow(() -> new IOException("Missing Implementation-Title")); + if (!"authlib-injector".equals(title)) { + throw new IOException("Bad Implementation-Title"); + } + + String version = Optional.ofNullable(attributes.getValue("Implementation-Version")) + .orElseThrow(() -> new IOException("Missing Implementation-Version")); + + int buildNumber; + try { + buildNumber = Optional.ofNullable(attributes.getValue("Build-Number")) + .map(Integer::parseInt) + .orElseThrow(() -> new IOException("Missing Build-Number")); + } catch (NumberFormatException e) { + throw new IOException("Bad Build-Number", e); + } + return new AuthlibInjectorArtifactInfo(buildNumber, version, location.toAbsolutePath()); + } + } + + private class AuthlibInjectorVersionInfo { + @SerializedName("build_number") + public int buildNumber; + + @SerializedName("version") + public String version; + + @SerializedName("download_url") + public String downloadUrl; + + @SerializedName("checksums") + public Map checksums; + } + +}