diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameDownloadTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameDownloadTask.java index b923b708d..e1402d55e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameDownloadTask.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameDownloadTask.java @@ -20,6 +20,7 @@ package org.jackhuang.hmcl.game; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Settings; import org.jackhuang.hmcl.task.FileDownloadTask; +import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.FileUtils; import org.jackhuang.hmcl.util.Logging; @@ -70,7 +71,7 @@ public class HMCLGameDownloadTask extends Task { NetworkUtils.toURL(profile.getDependency().getDownloadProvider().injectURL(version.getDownloadInfo().getUrl())), cache, profile.getDependency().getProxy(), - version.getDownloadInfo().getSha1() + new IntegrityCheck("SHA-1", version.getDownloadInfo().getSha1()) ).then(Task.of(v -> FileUtils.copyFile(cache, jar)))); } 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/HMCL/src/main/java/org/jackhuang/hmcl/ui/AddAccountPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/AddAccountPane.java index cb6c19f33..732efb929 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/AddAccountPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/AddAccountPane.java @@ -82,6 +82,7 @@ public class AddAccountPane extends StackPane { cboServers.setCellFactory(jfxListCellFactory(server -> new TwoLineListItem(server.getName(), server.getUrl()))); cboServers.setConverter(stringConverter(AuthlibInjectorServer::getName)); cboServers.setItems(Settings.SETTINGS.authlibInjectorServers); + cboServers.setPromptText(Launcher.i18n("general.prompt.empty")); // workaround: otherwise the combox will be black if (!cboServers.getItems().isEmpty()) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java index 27a2e9aa0..7d80a6217 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java @@ -46,7 +46,6 @@ import javafx.util.Duration; import javafx.util.StringConverter; import org.jackhuang.hmcl.Launcher; -import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer; import org.jackhuang.hmcl.util.*; import java.io.File; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/AppDataUpgrader.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/AppDataUpgrader.java index 3c0955d33..03c4d5686 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/AppDataUpgrader.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/AppDataUpgrader.java @@ -23,6 +23,7 @@ import com.jfoenix.concurrency.JFXUtilities; import javafx.scene.layout.Region; import org.jackhuang.hmcl.Launcher; import org.jackhuang.hmcl.task.FileDownloadTask; +import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.TaskExecutor; import org.jackhuang.hmcl.ui.Controllers; @@ -182,7 +183,7 @@ public class AppDataUpgrader extends IUpgrader { @Override public Collection getDependents() { - return Collections.singleton(new FileDownloadTask(downloadLink, tempFile, Proxy.NO_PROXY, hash)); + return Collections.singleton(new FileDownloadTask(downloadLink, tempFile, Proxy.NO_PROXY, new IntegrityCheck("SHA-1", hash))); } @Override @@ -232,7 +233,7 @@ public class AppDataUpgrader extends IUpgrader { @Override public Collection getDependents() { - return Collections.singleton(new FileDownloadTask(downloadLink, tempFile, Proxy.NO_PROXY, hash)); + return Collections.singleton(new FileDownloadTask(downloadLink, tempFile, Proxy.NO_PROXY, new IntegrityCheck("SHA-1", hash))); } @Override diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 14d4c27a2..013c0a68b 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -99,6 +99,8 @@ folder.resourcepacks=Resourcepacks folder.saves=Saves folder.screenshots=Screenshots +general.prompt.empty=(None) + input.email=The username must be an e-mail. input.number=Must be a number. input.not_empty=Required field diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index d2f0aeb43..3871c9cb3 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -99,6 +99,8 @@ folder.resourcepacks=资源包文件夹 folder.saves=存档文件夹 folder.screenshots=截图文件夹 +general.prompt.empty=(无) + input.email=用户名必须是邮箱 input.number=必须是数字 input.not_empty=必填项 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..94b7587c0 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 @@ -1,3 +1,20 @@ +/* + * 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 org.jackhuang.hmcl.auth.AccountFactory; @@ -7,6 +24,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 +33,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 +54,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; + } + +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorProvider.java index 80d83aa1e..6276a705b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorProvider.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/authlibinjector/AuthlibInjectorProvider.java @@ -1,3 +1,20 @@ +/* + * 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 org.jackhuang.hmcl.auth.yggdrasil.YggdrasilProvider; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameAssetDownloadTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameAssetDownloadTask.java index 43539e780..8f95de725 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameAssetDownloadTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameAssetDownloadTask.java @@ -21,6 +21,7 @@ import org.jackhuang.hmcl.download.AbstractDependencyManager; import org.jackhuang.hmcl.game.AssetObject; import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.task.FileDownloadTask; +import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.FileUtils; import org.jackhuang.hmcl.util.Logging; @@ -106,7 +107,7 @@ public final class GameAssetDownloadTask extends Task { flag = !file.exists(); } if (flag) { - FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(url), file, dependencyManager.getProxy(), assetObject.getHash()); + FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(url), file, dependencyManager.getProxy(), new IntegrityCheck("SHA-1", assetObject.getHash())); task.setName(assetObject.getHash()); dependencies.add(task); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameDownloadTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameDownloadTask.java index 336c675b7..9400f771f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameDownloadTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameDownloadTask.java @@ -20,6 +20,7 @@ package org.jackhuang.hmcl.download.game; import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.task.FileDownloadTask; +import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.NetworkUtils; @@ -56,7 +57,7 @@ public final class GameDownloadTask extends Task { NetworkUtils.toURL(dependencyManager.getDownloadProvider().injectURL(version.getDownloadInfo().getUrl())), jar, dependencyManager.getProxy(), - version.getDownloadInfo().getSha1() + new IntegrityCheck("SHA-1", version.getDownloadInfo().getSha1()) )); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/LibraryDownloadTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/LibraryDownloadTask.java index e5bac6171..fcc569197 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/LibraryDownloadTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/LibraryDownloadTask.java @@ -4,6 +4,7 @@ import org.apache.commons.compress.compressors.xz.XZCompressorInputStream; import org.jackhuang.hmcl.download.AbstractDependencyManager; import org.jackhuang.hmcl.game.Library; import org.jackhuang.hmcl.task.FileDownloadTask; +import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.FileUtils; import org.jackhuang.hmcl.util.IOUtils; @@ -48,7 +49,7 @@ public final class LibraryDownloadTask extends Task { setSignificance(TaskSignificance.MODERATE); task = new FileDownloadTask(NetworkUtils.toURL(url), - file, dependencyManager.getProxy(), library.getDownload().getSha1()); + file, dependencyManager.getProxy(), new IntegrityCheck("SHA-1", library.getDownload().getSha1())); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/FileDownloadTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/FileDownloadTask.java index 8e19ecb24..37f6aefc7 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/FileDownloadTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/FileDownloadTask.java @@ -19,6 +19,7 @@ package org.jackhuang.hmcl.task; import org.jackhuang.hmcl.event.EventManager; import org.jackhuang.hmcl.event.FailedEvent; +import org.jackhuang.hmcl.util.ChecksumMismatchException; import org.jackhuang.hmcl.util.FileUtils; import org.jackhuang.hmcl.util.IOUtils; import org.jackhuang.hmcl.util.Logging; @@ -35,6 +36,7 @@ import java.net.URL; import java.security.MessageDigest; import java.util.logging.Level; +import static java.util.Objects.requireNonNull; import static org.jackhuang.hmcl.util.DigestUtils.getDigest; /** @@ -44,9 +46,38 @@ import static org.jackhuang.hmcl.util.DigestUtils.getDigest; */ public class FileDownloadTask extends Task { + public static class IntegrityCheck { + private String algorithm; + private String checksum; + + public IntegrityCheck(String algorithm, String checksum) { + this.algorithm = requireNonNull(algorithm); + this.checksum = requireNonNull(checksum); + } + + public String getAlgorithm() { + return algorithm; + } + + public String getChecksum() { + return checksum; + } + + public MessageDigest createDigest() { + return getDigest(algorithm); + } + + public void performCheck(MessageDigest digest) throws ChecksumMismatchException { + String actualChecksum = String.format("%1$040x", new BigInteger(1, digest.digest())); + if (!checksum.equalsIgnoreCase(actualChecksum)) { + throw new ChecksumMismatchException(algorithm, checksum, actualChecksum); + } + } + } + private final URL url; private final File file; - private final String hash; + private final IntegrityCheck integrityCheck; private final int retry; private final Proxy proxy; private final EventManager> onFailed = new EventManager<>(); @@ -74,23 +105,23 @@ public class FileDownloadTask extends Task { * @param url the URL of remote file. * @param file the location that download to. * @param proxy the proxy. - * @param hash the SHA-1 hash code of remote file, null if the hash is unknown or it is no need to check the hash code. + * @param integrityCheck the integrity check to perform, null if no integrity check is to be performed */ - public FileDownloadTask(URL url, File file, Proxy proxy, String hash) { - this(url, file, proxy, hash, 5); + public FileDownloadTask(URL url, File file, Proxy proxy, IntegrityCheck integrityCheck) { + this(url, file, proxy, integrityCheck, 5); } /** * @param url the URL of remote file. * @param file the location that download to. - * @param hash the SHA-1 hash code of remote file, null if the hash is unknown or it is no need to check the hash code. + * @param integrityCheck the integrity check to perform, null if no integrity check is to be performed * @param retry the times for retrying if downloading fails. * @param proxy the proxy. */ - public FileDownloadTask(URL url, File file, Proxy proxy, String hash, int retry) { + public FileDownloadTask(URL url, File file, Proxy proxy, IntegrityCheck integrityCheck, int retry) { this.url = url; this.file = file; - this.hash = hash; + this.integrityCheck = integrityCheck; this.retry = retry; this.proxy = proxy; @@ -159,7 +190,7 @@ public class FileDownloadTask extends Task { temp = FileUtils.createTempFile(); rFile = new RandomAccessFile(temp, "rw"); - MessageDigest digest = getDigest("SHA-1"); + MessageDigest digest = integrityCheck == null ? null : integrityCheck.createDigest(); stream = con.getInputStream(); int lastDownloaded = 0, downloaded = 0; @@ -175,8 +206,9 @@ public class FileDownloadTask extends Task { if (read == -1) break; - if (hash != null) + if (digest != null) { digest.update(buffer, 0, read); + } // Write buffer to file. rFile.write(buffer, 0, read); @@ -214,11 +246,9 @@ public class FileDownloadTask extends Task { if (downloaded != contentLength) throw new IllegalStateException("Unexpected file size: " + downloaded + ", expected: " + contentLength); - // Check hash code - if (hash != null) { - String hashCode = String.format("%1$040x", new BigInteger(1, digest.digest())); - if (!hash.equalsIgnoreCase(hashCode)) - throw new IllegalStateException("Unexpected hash code: " + hashCode + ", expected: " + hash); + // Integrity check + if (integrityCheck != null) { + integrityCheck.performCheck(digest); } return; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/ChecksumMismatchException.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/ChecksumMismatchException.java new file mode 100644 index 000000000..419ec9ee0 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/ChecksumMismatchException.java @@ -0,0 +1,46 @@ +/* + * 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.util; + +import java.io.IOException; + +public class ChecksumMismatchException extends IOException { + + private String algorithm; + private String expectedChecksum; + private String actualChecksum; + + public ChecksumMismatchException(String algorithm, String expectedChecksum, String actualChecksum) { + super("Incorrect checksum (" + algorithm + "), expected: " + expectedChecksum + ", actual: " + actualChecksum); + this.algorithm = algorithm; + this.expectedChecksum = expectedChecksum; + this.actualChecksum = actualChecksum; + } + + public String getAlgorithm() { + return algorithm; + } + + public String getExpectedChecksum() { + return expectedChecksum; + } + + public String getActualChecksum() { + return actualChecksum; + } +}