Merge pull request #468 from yushijinhun/authlib-injector-ali

支持 authlib-injector API 地址指示
This commit is contained in:
huanghongxun 2018-10-06 12:38:04 +08:00 committed by GitHub
commit f0d1c9b361
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 194 additions and 115 deletions

View File

@ -31,6 +31,7 @@ import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloadException;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
@ -278,6 +279,8 @@ public class AddAccountPane extends StackPane {
return i18n("account.failed.invalid_password");
}
return exception.getMessage();
} else if (exception instanceof AuthlibInjectorDownloadException) {
return i18n("account.failed.injector_download_failure");
} else {
return exception.getClass().getName() + ": " + exception.getLocalizedMessage();
}

View File

@ -21,7 +21,6 @@ import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.binding.Bindings;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
@ -39,7 +38,6 @@ import java.io.IOException;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.loadFXML;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class AddAuthlibInjectorServerPane extends StackPane implements DialogAware {
@ -70,24 +68,13 @@ public class AddAuthlibInjectorServerPane extends StackPane implements DialogAwa
transitionHandler = new TransitionHandler(addServerContainer);
transitionHandler.setContent(addServerPane, ContainerAnimations.NONE.getAnimationProducer());
btnAddNext.disableProperty().bind(
Bindings.createBooleanBinding(txtServerUrl::validate, txtServerUrl.textProperty()).not());
btnAddNext.disableProperty().bind(txtServerUrl.textProperty().isEmpty());
nextPane.hideSpinner();
txtServerUrl.setText("https://");
}
@Override
public void onDialogShown() {
txtServerUrl.requestFocus();
txtServerUrl.selectEnd();
}
private String fixInputUrl(String url) {
if (!url.endsWith("/")) {
url += "/";
}
return url;
}
private String resolveFetchExceptionMessage(Throwable exception) {
@ -110,13 +97,13 @@ public class AddAuthlibInjectorServerPane extends StackPane implements DialogAwa
lblCreationWarning.setText("");
String url = fixInputUrl(txtServerUrl.getText());
String url = txtServerUrl.getText();
nextPane.showSpinner();
addServerPane.setDisable(true);
Task.of(() -> {
serverBeingAdded = AuthlibInjectorServer.fetchServerInfo(url);
serverBeingAdded = AuthlibInjectorServer.locateServer(url);
}).finalized(Schedulers.javafx(), (variables, isDependentsSucceeded) -> {
addServerPane.setDisable(false);
nextPane.hideSpinner();

View File

@ -1,37 +0,0 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.validation.base.ValidatorBase;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.TextInputControl;
import java.net.MalformedURLException;
import java.net.URL;
public class URLValidator extends ValidatorBase {
private final ObservableList<String> protocols = FXCollections.observableArrayList();
public URLValidator() {
super();
}
public ObservableList<String> getProtocols() {
return protocols;
}
@Override
protected void eval() {
if (srcControl.get() instanceof TextInputControl) {
try {
URL url = new URL(((TextInputControl) srcControl.get()).getText());
if (protocols.isEmpty())
hasErrors.set(false);
else
hasErrors.set(!protocols.contains(url.getProtocol()));
} catch (MalformedURLException e) {
hasErrors.set(true);
}
}
}
}

View File

@ -4,7 +4,6 @@
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import com.jfoenix.controls.*?>
<?import org.jackhuang.hmcl.ui.construct.URLValidator?>
<?import org.jackhuang.hmcl.ui.construct.SpinnerPane?>
<fx:root xmlns="http://javafx.com/javafx"
@ -16,16 +15,7 @@
<Label text="%account.injector.add" />
</heading>
<body>
<JFXTextField fx:id="txtServerUrl" promptText="%account.injector.server_url" onAction="#onAddNext">
<validators>
<URLValidator message="%input.url">
<protocols>
<String fx:value="http" />
<String fx:value="https" />
</protocols>
</URLValidator>
</validators>
</JFXTextField>
<JFXTextField fx:id="txtServerUrl" promptText="%account.injector.server_url" onAction="#onAddNext" />
</body>
<actions>
<Label fx:id="lblCreationWarning" />

View File

@ -36,6 +36,7 @@ account.create=Create a new account
account.email=Email
account.failed.connect_authentication_server=Cannot connect to the authentication server. Check your network.
account.failed.connect_injector_server=Cannot connect to the authentication server. Check your network and ensure the URL is correct.
account.failed.injector_download_failure=Failed to download authlib-injector. Check your network and try switching to another download source.
account.failed.invalid_credentials=Incorrect password, or you are forbidden to login temporarily.
account.failed.invalid_password=Invalid password
account.failed.invalid_token=Please log out and re-input your password to login.

View File

@ -36,6 +36,7 @@ account.create=建立帳戶
account.email=電子信箱
account.failed.connect_authentication_server=無法連接認證伺服器,可能是網路問題
account.failed.connect_injector_server=無法連接認證伺服器,可能是網路故障或 URL 輸入錯誤
account.failed.injector_download_failure=無法下載 authlib-injector請檢查網路或嘗試切換下載源
account.failed.invalid_credentials=您的使用者名稱或密碼錯誤,或者登入次數過多被暫時禁止登入,請稍後再試
account.failed.invalid_password=無效的密碼
account.failed.invalid_token=請嘗試登出並重新輸入密碼登入

View File

@ -36,6 +36,7 @@ account.create=新建账户
account.email=邮箱
account.failed.connect_authentication_server=无法连接认证服务器,可能是网络问题
account.failed.connect_injector_server=无法连接认证服务器,可能是网络故障或 URL 输入错误
account.failed.injector_download_failure=无法下载 authlib-injector请检查网络或尝试切换下载源
account.failed.invalid_credentials=您的用户名或密码错误,或者登录次数过多被暂时禁止登录,请稍后再试
account.failed.invalid_password=无效的密码
account.failed.invalid_token=请尝试登出并重新输入密码登录

View File

@ -20,21 +20,20 @@ package org.jackhuang.hmcl.auth.authlibinjector;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.CharacterSelector;
import org.jackhuang.hmcl.auth.ServerDisconnectException;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilSession;
import org.jackhuang.hmcl.game.Arguments;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.function.ExceptionalSupplier;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.*;
import java.util.logging.Level;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.jackhuang.hmcl.util.Logging.LOG;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import static org.jackhuang.hmcl.util.io.IOUtils.readFullyWithoutClosing;
public class AuthlibInjectorAccount extends YggdrasilAccount {
private AuthlibInjectorServer server;
@ -58,41 +57,44 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
}
private AuthInfo inject(ExceptionalSupplier<AuthInfo, AuthenticationException> 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);
CompletableFuture<byte[]> prefetchedMetaTask = CompletableFuture.supplyAsync(() -> {
try (InputStream in = new URL(server.getUrl()).openStream()) {
return readFullyWithoutClosing(in);
} catch (IOException e) {
throw new CompletionException(new ServerDisconnectException(e));
}
}, "Yggdrasil metadata fetch thread");
});
// Update authlib-injector
CompletableFuture<AuthlibInjectorArtifactInfo> artifactTask = CompletableFuture.supplyAsync(() -> {
try {
return authlibInjectorDownloader.get();
} catch (IOException e) {
throw new CompletionException(new AuthlibInjectorDownloadException(e));
}
});
AuthInfo auth = loginAction.get();
byte[] prefetchedMeta;
AuthlibInjectorArtifactInfo artifact;
try {
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();
prefetchedMeta = prefetchedMetaTask.get();
artifact = artifactTask.get();
} 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)));
throw new AuthenticationException(e);
} catch (ExecutionException e) {
if (e.getCause() instanceof AuthenticationException) {
throw (AuthenticationException) e.getCause();
} else {
throw new AuthenticationException(e.getCause());
}
}
return info.withArguments(arguments);
return auth.withArguments(new Arguments().addJVMArguments(
"-javaagent:" + artifact.getLocation().toString() + "=" + server.getUrl(),
"-Dauthlibinjector.side=client",
"-Dorg.to2mbn.authlibinjector.config.prefetched=" + Base64.getEncoder().encodeToString(prefetchedMeta)));
}
@Override

View File

@ -0,0 +1,41 @@
/*
* 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 org.jackhuang.hmcl.auth.AuthenticationException;
/**
* @author yushijinhun
*/
public class AuthlibInjectorDownloadException extends AuthenticationException {
public AuthlibInjectorDownloadException() {
}
public AuthlibInjectorDownloadException(String message, Throwable cause) {
super(message, cause);
}
public AuthlibInjectorDownloadException(String message) {
super(message);
}
public AuthlibInjectorDownloadException(Throwable cause) {
super(cause);
}
}

View File

@ -45,6 +45,11 @@ public class AuthlibInjectorDownloader {
private Path artifactLocation;
private Supplier<DownloadProvider> downloadProvider;
/**
* The flag will be reset after application restart.
*/
private boolean updateChecked = false;
/**
* @param artifactsDirectory where to save authlib-injector artifacts
*/
@ -57,14 +62,17 @@ public class AuthlibInjectorDownloader {
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;
if (!local.isPresent() || !updateChecked) {
try {
update(local);
updateChecked = true;
} 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());
}
LOG.warning("Fallback to use cached artifact: " + local.get());
}
return getLocalArtifact().orElseThrow(() -> new IOException("The updated authlib-inejector cannot be recognized"));
@ -72,6 +80,7 @@ public class AuthlibInjectorDownloader {
}
private void update(Optional<AuthlibInjectorArtifactInfo> local) throws IOException {
LOG.info("Checking update of authlib-injector");
AuthlibInjectorVersionInfo latest = getLatestArtifactInfo();
if (local.isPresent() && local.get().getBuildNumber() >= latest.buildNumber) {

View File

@ -17,23 +17,104 @@
*/
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.readFullyWithoutClosing;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Optional;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException;
import java.util.Optional;
import static org.jackhuang.hmcl.util.Lang.tryCast;
public class AuthlibInjectorServer {
public static AuthlibInjectorServer fetchServerInfo(String url) throws IOException {
private static final int MAX_REDIRECTS = 5;
public static AuthlibInjectorServer locateServer(String url) throws IOException {
url = parseInputUrl(url);
HttpURLConnection conn;
int redirectCount = 0;
for (;;) {
conn = (HttpURLConnection) new URL(url).openConnection();
Optional<String> ali = getApiLocationIndication(conn);
if (ali.isPresent()) {
conn.disconnect();
url = ali.get();
if (redirectCount >= MAX_REDIRECTS) {
throw new IOException("Exceeded maximum number of redirects (" + MAX_REDIRECTS + ")");
}
redirectCount++;
LOG.info("Redirect API root to: " + url);
} else {
break;
}
}
try {
JsonObject response = JsonUtils.fromNonNullJson(NetworkUtils.doGet(NetworkUtils.toURL(url)), JsonObject.class);
return parseResponse(url, readFullyWithoutClosing(conn.getInputStream()));
} finally {
conn.disconnect();
}
}
private static Optional<String> getApiLocationIndication(URLConnection conn) {
return Optional.ofNullable(conn.getHeaderFields().get("X-Authlib-Injector-API-Location"))
.flatMap(list -> list.isEmpty() ? Optional.empty() : Optional.of(list.get(0)))
.flatMap(indication -> {
String currentUrl = appendSuffixSlash(conn.getURL().toString());
String newUrl;
try {
newUrl = appendSuffixSlash(new URL(conn.getURL(), indication).toString());
} catch (MalformedURLException e) {
LOG.warning("Failed to resolve absolute ALI, the header is [" + indication + "]. Ignore it.");
return Optional.empty();
}
if (newUrl.equals(currentUrl)) {
return Optional.empty();
} else {
return Optional.of(newUrl);
}
});
}
private static String parseInputUrl(String url) {
String lowercased = url.toLowerCase();
if (!lowercased.startsWith("http://") && !lowercased.startsWith("https://")) {
url = "https://" + url;
}
url = appendSuffixSlash(url);
return url;
}
private static String appendSuffixSlash(String url) {
if (!url.endsWith("/")) {
return url + "/";
} else {
return url;
}
}
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) {