Merge pull request #358 from yushijinhun/authlib-injector

重构 authlib-injector 下载,并切换到新 API
This commit is contained in:
huanghongxun
2018-07-01 11:54:55 +08:00
committed by GitHub
18 changed files with 400 additions and 129 deletions

View File

@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Settings; import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.FileUtils; import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.Logging; import org.jackhuang.hmcl.util.Logging;
@@ -70,7 +71,7 @@ public class HMCLGameDownloadTask extends Task {
NetworkUtils.toURL(profile.getDependency().getDownloadProvider().injectURL(version.getDownloadInfo().getUrl())), NetworkUtils.toURL(profile.getDependency().getDownloadProvider().injectURL(version.getDownloadInfo().getUrl())),
cache, cache,
profile.getDependency().getProxy(), profile.getDependency().getProxy(),
version.getDownloadInfo().getSha1() new IntegrityCheck("SHA-1", version.getDownloadInfo().getSha1())
).then(Task.of(v -> FileUtils.copyFile(cache, jar)))); ).then(Task.of(v -> FileUtils.copyFile(cache, jar))));
} }

View File

@@ -22,19 +22,14 @@ import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AccountFactory; import org.jackhuang.hmcl.auth.AccountFactory;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccountFactory; 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.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.offline.OfflineAccount; import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory; import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
import org.jackhuang.hmcl.auth.yggdrasil.MojangYggdrasilProvider; import org.jackhuang.hmcl.auth.yggdrasil.MojangYggdrasilProvider;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory; 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.io.IOException;
import java.net.URL;
import java.util.Map; import java.util.Map;
import java.util.logging.Level; import java.util.logging.Level;
@@ -55,7 +50,9 @@ public final class Accounts {
public static final Map<String, AccountFactory<?>> ACCOUNT_FACTORY = mapOf( public static final Map<String, AccountFactory<?>> ACCOUNT_FACTORY = mapOf(
pair(OFFLINE_ACCOUNT_KEY, OfflineAccountFactory.INSTANCE), pair(OFFLINE_ACCOUNT_KEY, OfflineAccountFactory.INSTANCE),
pair(YGGDRASIL_ACCOUNT_KEY, new YggdrasilAccountFactory(MojangYggdrasilProvider.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) { public static String getAccountType(Account account) {
@@ -73,22 +70,6 @@ public final class Accounts {
return username + ":" + character; 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) { private static AuthlibInjectorServer getOrCreateAuthlibInjectorServer(String url) {
return Settings.SETTINGS.authlibInjectorServers.stream() return Settings.SETTINGS.authlibInjectorServers.stream()
.filter(server -> url.equals(server.getUrl())) .filter(server -> url.equals(server.getUrl()))

View File

@@ -82,6 +82,7 @@ public class AddAccountPane extends StackPane {
cboServers.setCellFactory(jfxListCellFactory(server -> new TwoLineListItem(server.getName(), server.getUrl()))); cboServers.setCellFactory(jfxListCellFactory(server -> new TwoLineListItem(server.getName(), server.getUrl())));
cboServers.setConverter(stringConverter(AuthlibInjectorServer::getName)); cboServers.setConverter(stringConverter(AuthlibInjectorServer::getName));
cboServers.setItems(Settings.SETTINGS.authlibInjectorServers); cboServers.setItems(Settings.SETTINGS.authlibInjectorServers);
cboServers.setPromptText(Launcher.i18n("general.prompt.empty"));
// workaround: otherwise the combox will be black // workaround: otherwise the combox will be black
if (!cboServers.getItems().isEmpty()) if (!cboServers.getItems().isEmpty())

View File

@@ -46,7 +46,6 @@ import javafx.util.Duration;
import javafx.util.StringConverter; import javafx.util.StringConverter;
import org.jackhuang.hmcl.Launcher; import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.util.*; import org.jackhuang.hmcl.util.*;
import java.io.File; import java.io.File;

View File

@@ -23,6 +23,7 @@ import com.jfoenix.concurrency.JFXUtilities;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import org.jackhuang.hmcl.Launcher; import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor; import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.Controllers;
@@ -182,7 +183,7 @@ public class AppDataUpgrader extends IUpgrader {
@Override @Override
public Collection<Task> getDependents() { public Collection<Task> 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 @Override
@@ -232,7 +233,7 @@ public class AppDataUpgrader extends IUpgrader {
@Override @Override
public Collection<Task> getDependents() { public Collection<Task> 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 @Override

View File

@@ -99,6 +99,8 @@ folder.resourcepacks=Resourcepacks
folder.saves=Saves folder.saves=Saves
folder.screenshots=Screenshots folder.screenshots=Screenshots
general.prompt.empty=(None)
input.email=The username must be an e-mail. input.email=The username must be an e-mail.
input.number=Must be a number. input.number=Must be a number.
input.not_empty=Required field input.not_empty=Required field

View File

@@ -99,6 +99,8 @@ folder.resourcepacks=资源包文件夹
folder.saves=存档文件夹 folder.saves=存档文件夹
folder.screenshots=截图文件夹 folder.screenshots=截图文件夹
general.prompt.empty=(无)
input.email=用户名必须是邮箱 input.email=用户名必须是邮箱
input.number=必须是数字 input.number=必须是数字
input.not_empty=必填项 input.not_empty=必填项

View File

@@ -31,19 +31,23 @@ import org.jackhuang.hmcl.util.NetworkUtils;
import java.util.Base64; import java.util.Base64;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.UUID; 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 java.nio.charset.StandardCharsets.UTF_8;
import static org.jackhuang.hmcl.util.Logging.LOG;
import java.io.IOException;
public class AuthlibInjectorAccount extends YggdrasilAccount { public class AuthlibInjectorAccount extends YggdrasilAccount {
private final AuthlibInjectorServer server; private AuthlibInjectorServer server;
private final ExceptionalSupplier<String, ?> injectorJarPath; private ExceptionalSupplier<AuthlibInjectorArtifactInfo, ? extends IOException> authlibInjectorDownloader;
protected AuthlibInjectorAccount(YggdrasilService service, AuthlibInjectorServer server, ExceptionalSupplier<String, ?> injectorJarPath, String username, UUID characterUUID, YggdrasilSession session) { protected AuthlibInjectorAccount(YggdrasilService service, AuthlibInjectorServer server, ExceptionalSupplier<AuthlibInjectorArtifactInfo, ? extends IOException> authlibInjectorDownloader, String username, UUID characterUUID, YggdrasilSession session) {
super(service, username, characterUUID, session); super(service, username, characterUUID, session);
this.injectorJarPath = injectorJarPath; this.authlibInjectorDownloader = authlibInjectorDownloader;
this.server = server; this.server = server;
} }
@@ -57,25 +61,42 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
return inject(() -> super.logInWithPassword(password, selector)); return inject(() -> super.logInWithPassword(password, selector));
} }
private AuthInfo inject(ExceptionalSupplier<AuthInfo, AuthenticationException> supplier) throws AuthenticationException { private AuthInfo inject(ExceptionalSupplier<AuthInfo, AuthenticationException> loginAction) throws AuthenticationException {
// Authlib Injector recommends launchers to pre-fetch the server basic information before launched the game to save time. // Pre-fetch metadata
GetTask getTask = new GetTask(NetworkUtils.toURL(server.getUrl())); GetTask metadataFetchTask = new GetTask(NetworkUtils.toURL(server.getUrl()));
AtomicBoolean flag = new AtomicBoolean(true); Thread metadataFetchThread = Lang.thread(() -> {
Thread thread = Lang.thread(() -> flag.set(getTask.test())); 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 { try {
thread.join(); artifact = authlibInjectorDownloader.get();
} catch (IOException e) {
Arguments arguments = new Arguments().addJVMArguments("-javaagent:" + injectorJarPath.get() + "=" + server.getUrl()); throw new AuthenticationException("Failed to download authlib-injector", e);
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);
} }
// 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<String> 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 @Override

View File

@@ -1,3 +1,20 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* 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; package org.jackhuang.hmcl.auth.authlibinjector;
import org.jackhuang.hmcl.auth.AccountFactory; 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.auth.yggdrasil.YggdrasilSession;
import org.jackhuang.hmcl.util.ExceptionalSupplier; import org.jackhuang.hmcl.util.ExceptionalSupplier;
import java.io.IOException;
import java.net.Proxy; import java.net.Proxy;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
@@ -15,14 +33,14 @@ import java.util.function.Function;
import static org.jackhuang.hmcl.util.Lang.tryCast; import static org.jackhuang.hmcl.util.Lang.tryCast;
public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjectorAccount> { public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjectorAccount> {
private final ExceptionalSupplier<String, ?> injectorJarPathSupplier; private ExceptionalSupplier<AuthlibInjectorArtifactInfo, ? extends IOException> authlibInjectorDownloader;
private Function<String, AuthlibInjectorServer> serverLookup; private Function<String, AuthlibInjectorServer> serverLookup;
/** /**
* @param serverLookup a function that looks up {@link AuthlibInjectorServer} by url * @param serverLookup a function that looks up {@link AuthlibInjectorServer} by url
*/ */
public AuthlibInjectorAccountFactory(ExceptionalSupplier<String, ?> injectorJarPathSupplier, Function<String, AuthlibInjectorServer> serverLookup) { public AuthlibInjectorAccountFactory(ExceptionalSupplier<AuthlibInjectorArtifactInfo, ? extends IOException> authlibInjectorDownloader, Function<String, AuthlibInjectorServer> serverLookup) {
this.injectorJarPathSupplier = injectorJarPathSupplier; this.authlibInjectorDownloader = authlibInjectorDownloader;
this.serverLookup = serverLookup; this.serverLookup = serverLookup;
} }
@@ -36,7 +54,7 @@ public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjecto
AuthlibInjectorServer server = (AuthlibInjectorServer) additionalData; AuthlibInjectorServer server = (AuthlibInjectorServer) additionalData;
AuthlibInjectorAccount account = new AuthlibInjectorAccount(new YggdrasilService(new AuthlibInjectorProvider(server.getUrl()), proxy), AuthlibInjectorAccount account = new AuthlibInjectorAccount(new YggdrasilService(new AuthlibInjectorProvider(server.getUrl()), proxy),
server, injectorJarPathSupplier, username, null, null); server, authlibInjectorDownloader, username, null, null);
account.logInWithPassword(password, selector); account.logInWithPassword(password, selector);
return account; return account;
} }
@@ -56,6 +74,6 @@ public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjecto
AuthlibInjectorServer server = serverLookup.apply(apiRoot); AuthlibInjectorServer server = serverLookup.apply(apiRoot);
return new AuthlibInjectorAccount(new YggdrasilService(new AuthlibInjectorProvider(server.getUrl()), proxy), return new AuthlibInjectorAccount(new YggdrasilService(new AuthlibInjectorProvider(server.getUrl()), proxy),
server, injectorJarPathSupplier, username, session.getSelectedProfile().getId(), session); server, authlibInjectorDownloader, username, session.getSelectedProfile().getId(), session);
} }
} }

View File

@@ -0,0 +1,50 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* 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 + "]";
}
}

View File

@@ -1,59 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* 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";
}

View File

@@ -0,0 +1,158 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* 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> downloadProvider;
/**
* @param artifactsDirectory where to save authlib-injector artifacts
*/
public AuthlibInjectorDownloader(Path artifactsDirectory, Supplier<DownloadProvider> downloadProvider) {
this.artifactLocation = artifactsDirectory.resolve("authlib-injector.jar");
this.downloadProvider = downloadProvider;
}
public AuthlibInjectorArtifactInfo getArtifactInfo() throws IOException {
synchronized (artifactLocation) {
Optional<AuthlibInjectorArtifactInfo> 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<AuthlibInjectorArtifactInfo> 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<AuthlibInjectorArtifactInfo> 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<String, String> checksums;
}
}

View File

@@ -1,3 +1,20 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* 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; package org.jackhuang.hmcl.auth.authlibinjector;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilProvider; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilProvider;

View File

@@ -21,6 +21,7 @@ import org.jackhuang.hmcl.download.AbstractDependencyManager;
import org.jackhuang.hmcl.game.AssetObject; import org.jackhuang.hmcl.game.AssetObject;
import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.FileUtils; import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.Logging; import org.jackhuang.hmcl.util.Logging;
@@ -106,7 +107,7 @@ public final class GameAssetDownloadTask extends Task {
flag = !file.exists(); flag = !file.exists();
} }
if (flag) { 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()); task.setName(assetObject.getHash());
dependencies.add(task); dependencies.add(task);
} }

View File

@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.download.game;
import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.NetworkUtils; import org.jackhuang.hmcl.util.NetworkUtils;
@@ -56,7 +57,7 @@ public final class GameDownloadTask extends Task {
NetworkUtils.toURL(dependencyManager.getDownloadProvider().injectURL(version.getDownloadInfo().getUrl())), NetworkUtils.toURL(dependencyManager.getDownloadProvider().injectURL(version.getDownloadInfo().getUrl())),
jar, jar,
dependencyManager.getProxy(), dependencyManager.getProxy(),
version.getDownloadInfo().getSha1() new IntegrityCheck("SHA-1", version.getDownloadInfo().getSha1())
)); ));
} }

View File

@@ -4,6 +4,7 @@ import org.apache.commons.compress.compressors.xz.XZCompressorInputStream;
import org.jackhuang.hmcl.download.AbstractDependencyManager; import org.jackhuang.hmcl.download.AbstractDependencyManager;
import org.jackhuang.hmcl.game.Library; import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.FileUtils; import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.IOUtils; import org.jackhuang.hmcl.util.IOUtils;
@@ -48,7 +49,7 @@ public final class LibraryDownloadTask extends Task {
setSignificance(TaskSignificance.MODERATE); setSignificance(TaskSignificance.MODERATE);
task = new FileDownloadTask(NetworkUtils.toURL(url), task = new FileDownloadTask(NetworkUtils.toURL(url),
file, dependencyManager.getProxy(), library.getDownload().getSha1()); file, dependencyManager.getProxy(), new IntegrityCheck("SHA-1", library.getDownload().getSha1()));
} }
@Override @Override

View File

@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.task;
import org.jackhuang.hmcl.event.EventManager; import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.event.FailedEvent; import org.jackhuang.hmcl.event.FailedEvent;
import org.jackhuang.hmcl.util.ChecksumMismatchException;
import org.jackhuang.hmcl.util.FileUtils; import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.IOUtils; import org.jackhuang.hmcl.util.IOUtils;
import org.jackhuang.hmcl.util.Logging; import org.jackhuang.hmcl.util.Logging;
@@ -35,6 +36,7 @@ import java.net.URL;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.util.logging.Level; import java.util.logging.Level;
import static java.util.Objects.requireNonNull;
import static org.jackhuang.hmcl.util.DigestUtils.getDigest; 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 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 URL url;
private final File file; private final File file;
private final String hash; private final IntegrityCheck integrityCheck;
private final int retry; private final int retry;
private final Proxy proxy; private final Proxy proxy;
private final EventManager<FailedEvent<URL>> onFailed = new EventManager<>(); private final EventManager<FailedEvent<URL>> onFailed = new EventManager<>();
@@ -74,23 +105,23 @@ public class FileDownloadTask extends Task {
* @param url the URL of remote file. * @param url the URL of remote file.
* @param file the location that download to. * @param file the location that download to.
* @param proxy the proxy. * @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) { public FileDownloadTask(URL url, File file, Proxy proxy, IntegrityCheck integrityCheck) {
this(url, file, proxy, hash, 5); this(url, file, proxy, integrityCheck, 5);
} }
/** /**
* @param url the URL of remote file. * @param url the URL of remote file.
* @param file the location that download to. * @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 retry the times for retrying if downloading fails.
* @param proxy the proxy. * @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.url = url;
this.file = file; this.file = file;
this.hash = hash; this.integrityCheck = integrityCheck;
this.retry = retry; this.retry = retry;
this.proxy = proxy; this.proxy = proxy;
@@ -159,7 +190,7 @@ public class FileDownloadTask extends Task {
temp = FileUtils.createTempFile(); temp = FileUtils.createTempFile();
rFile = new RandomAccessFile(temp, "rw"); rFile = new RandomAccessFile(temp, "rw");
MessageDigest digest = getDigest("SHA-1"); MessageDigest digest = integrityCheck == null ? null : integrityCheck.createDigest();
stream = con.getInputStream(); stream = con.getInputStream();
int lastDownloaded = 0, downloaded = 0; int lastDownloaded = 0, downloaded = 0;
@@ -175,8 +206,9 @@ public class FileDownloadTask extends Task {
if (read == -1) if (read == -1)
break; break;
if (hash != null) if (digest != null) {
digest.update(buffer, 0, read); digest.update(buffer, 0, read);
}
// Write buffer to file. // Write buffer to file.
rFile.write(buffer, 0, read); rFile.write(buffer, 0, read);
@@ -214,11 +246,9 @@ public class FileDownloadTask extends Task {
if (downloaded != contentLength) if (downloaded != contentLength)
throw new IllegalStateException("Unexpected file size: " + downloaded + ", expected: " + contentLength); throw new IllegalStateException("Unexpected file size: " + downloaded + ", expected: " + contentLength);
// Check hash code // Integrity check
if (hash != null) { if (integrityCheck != null) {
String hashCode = String.format("%1$040x", new BigInteger(1, digest.digest())); integrityCheck.performCheck(digest);
if (!hash.equalsIgnoreCase(hashCode))
throw new IllegalStateException("Unexpected hash code: " + hashCode + ", expected: " + hash);
} }
return; return;

View File

@@ -0,0 +1,46 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* 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;
}
}