Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e57a41c2f3 | ||
|
|
d63b0bb088 | ||
|
|
8e9ddfb248 | ||
|
|
1edd36819c | ||
|
|
a5f6028b3a | ||
|
|
52f57c783e |
No files matched your search
@@ -28,7 +28,6 @@ val versionType = System.getenv("VERSION_TYPE") ?: if (isOfficial) "nightly" els
|
|||||||
val versionRoot = System.getenv("VERSION_ROOT") ?: projectConfig.getProperty("versionRoot") ?: "3"
|
val versionRoot = System.getenv("VERSION_ROOT") ?: projectConfig.getProperty("versionRoot") ?: "3"
|
||||||
|
|
||||||
val microsoftAuthId = System.getenv("MICROSOFT_AUTH_ID") ?: ""
|
val microsoftAuthId = System.getenv("MICROSOFT_AUTH_ID") ?: ""
|
||||||
val microsoftAuthSecret = System.getenv("MICROSOFT_AUTH_SECRET") ?: ""
|
|
||||||
val curseForgeApiKey = System.getenv("CURSEFORGE_API_KEY") ?: ""
|
val curseForgeApiKey = System.getenv("CURSEFORGE_API_KEY") ?: ""
|
||||||
|
|
||||||
val launcherExe = System.getenv("HMCL_LAUNCHER_EXE") ?: ""
|
val launcherExe = System.getenv("HMCL_LAUNCHER_EXE") ?: ""
|
||||||
@@ -153,7 +152,6 @@ val hmclProperties = buildList {
|
|||||||
}
|
}
|
||||||
add("hmcl.version.type" to versionType)
|
add("hmcl.version.type" to versionType)
|
||||||
add("hmcl.microsoft.auth.id" to microsoftAuthId)
|
add("hmcl.microsoft.auth.id" to microsoftAuthId)
|
||||||
add("hmcl.microsoft.auth.secret" to microsoftAuthSecret)
|
|
||||||
add("hmcl.curseforge.apikey" to curseForgeApiKey)
|
add("hmcl.curseforge.apikey" to curseForgeApiKey)
|
||||||
add("hmcl.authlib-injector.version" to libs.authlib.injector.get().version!!)
|
add("hmcl.authlib-injector.version" to libs.authlib.injector.get().version!!)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,9 +29,8 @@ import org.jackhuang.hmcl.util.io.JarUtils;
|
|||||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.HashMap;
|
import java.security.SecureRandom;
|
||||||
import java.util.Locale;
|
import java.util.*;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
@@ -43,6 +42,8 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
|||||||
public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
||||||
private final int port;
|
private final int port;
|
||||||
private final CompletableFuture<String> future = new CompletableFuture<>();
|
private final CompletableFuture<String> future = new CompletableFuture<>();
|
||||||
|
private final String codeVerifier;
|
||||||
|
private final String state;
|
||||||
|
|
||||||
public static String lastlyOpenedURL;
|
public static String lastlyOpenedURL;
|
||||||
|
|
||||||
@@ -52,6 +53,34 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
|||||||
super(port);
|
super(port);
|
||||||
|
|
||||||
this.port = port;
|
this.port = port;
|
||||||
|
|
||||||
|
var encoder = Base64.getUrlEncoder().withoutPadding();
|
||||||
|
var random = new SecureRandom();
|
||||||
|
|
||||||
|
{
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc6749#section-10.12
|
||||||
|
byte[] bytes = new byte[32];
|
||||||
|
random.nextBytes(bytes);
|
||||||
|
this.state = encoder.encodeToString(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc7636#section-4.1
|
||||||
|
byte[] bytes = new byte[64];
|
||||||
|
random.nextBytes(bytes);
|
||||||
|
this.codeVerifier = encoder.encodeToString(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCodeVerifier() {
|
||||||
|
return codeVerifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getState() {
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -93,12 +122,22 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
|||||||
String parameters = session.getQueryParameterString();
|
String parameters = session.getQueryParameterString();
|
||||||
|
|
||||||
Map<String, String> query = mapOf(NetworkUtils.parseQuery(parameters));
|
Map<String, String> query = mapOf(NetworkUtils.parseQuery(parameters));
|
||||||
if (query.containsKey("code")) {
|
|
||||||
idToken = query.get("id_token");
|
String code = query.get("code");
|
||||||
future.complete(query.get("code"));
|
if (code != null) {
|
||||||
|
if (this.state.equals(query.get("state"))) {
|
||||||
|
idToken = query.get("id_token");
|
||||||
|
future.complete(code);
|
||||||
|
} else if (query.containsKey("state")) {
|
||||||
|
LOG.warning("Failed to authenticate: invalid state in parameters");
|
||||||
|
future.completeExceptionally(new AuthenticationException("Failed to authenticate: invalid state"));
|
||||||
|
} else {
|
||||||
|
LOG.warning("Failed to authenticate: missing state in parameters");
|
||||||
|
future.completeExceptionally(new AuthenticationException("Failed to authenticate: missing state"));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
LOG.warning("Error: " + parameters);
|
LOG.warning("Failed to authenticate: missing authorization code in parameters");
|
||||||
future.completeExceptionally(new AuthenticationException("failed to authenticate"));
|
future.completeExceptionally(new AuthenticationException("Failed to authenticate: missing authorization code"));
|
||||||
}
|
}
|
||||||
|
|
||||||
String html;
|
String html;
|
||||||
@@ -168,17 +207,6 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
|||||||
return System.getProperty("hmcl.microsoft.auth.id",
|
return System.getProperty("hmcl.microsoft.auth.id",
|
||||||
JarUtils.getAttribute("hmcl.microsoft.auth.id", ""));
|
JarUtils.getAttribute("hmcl.microsoft.auth.id", ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getClientSecret() {
|
|
||||||
return System.getProperty("hmcl.microsoft.auth.secret",
|
|
||||||
JarUtils.getAttribute("hmcl.microsoft.auth.secret", ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isPublicClient() {
|
|
||||||
return true; // We have turned on the device auth flow.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class GrantDeviceCodeEvent extends Event {
|
public static class GrantDeviceCodeEvent extends Event {
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ public class MicrosoftAccountLoginPane extends JFXDialogLayout implements Dialog
|
|||||||
hintPane.setText(i18n("account.methods.microsoft.hint"));
|
hintPane.setText(i18n("account.methods.microsoft.hint"));
|
||||||
rootContainer.getChildren().add(hintPane);
|
rootContainer.getChildren().add(hintPane);
|
||||||
} else if (currentStep instanceof Step.StartAuthorizationCodeLogin) {
|
} else if (currentStep instanceof Step.StartAuthorizationCodeLogin) {
|
||||||
loginButtonSpinner.setLoading(true);
|
loginButtonSpinner.setLoading(false);
|
||||||
cancelAllTasks();
|
cancelAllTasks();
|
||||||
|
|
||||||
rootContainer.getChildren().add(new JFXSpinner());
|
rootContainer.getChildren().add(new JFXSpinner());
|
||||||
@@ -175,16 +175,20 @@ public class MicrosoftAccountLoginPane extends JFXDialogLayout implements Dialog
|
|||||||
} else if (currentStep instanceof Step.WaitForScanQrCode wait) {
|
} else if (currentStep instanceof Step.WaitForScanQrCode wait) {
|
||||||
loginButtonSpinner.setLoading(true);
|
loginButtonSpinner.setLoading(true);
|
||||||
|
|
||||||
|
String scanUri = "https://www.microsoft.com/link".equals(wait.verificationUri())
|
||||||
|
? "https://www.microsoft.com/link?otc=" + wait.userCode()
|
||||||
|
: wait.verificationUri();
|
||||||
|
|
||||||
var deviceHint = new HintPane(MessageDialogPane.MessageType.INFO);
|
var deviceHint = new HintPane(MessageDialogPane.MessageType.INFO);
|
||||||
deviceHint.setSegment(i18n("account.methods.microsoft.methods.device.hint",
|
deviceHint.setSegment(i18n("account.methods.microsoft.methods.device.hint",
|
||||||
StringUtils.escapeXmlAttribute(wait.verificationUri()),
|
StringUtils.escapeXmlAttribute(scanUri),
|
||||||
wait.verificationUri(),
|
wait.verificationUri(),
|
||||||
wait.userCode()
|
wait.userCode()
|
||||||
));
|
));
|
||||||
|
|
||||||
var qrCode = new SVGPath();
|
var qrCode = new SVGPath();
|
||||||
qrCode.fillProperty().bind(Themes.colorSchemeProperty().getPrimary());
|
qrCode.fillProperty().bind(Themes.colorSchemeProperty().getPrimary());
|
||||||
qrCode.setContent(QrCodeUtils.toSVGPath(QrCode.encodeText(wait.verificationUri() + "?otc=" + wait.userCode(), QrCode.Ecc.MEDIUM)));
|
qrCode.setContent(QrCodeUtils.toSVGPath(QrCode.encodeText(scanUri, QrCode.Ecc.MEDIUM)));
|
||||||
qrCode.setScaleX(3);
|
qrCode.setScaleX(3);
|
||||||
qrCode.setScaleY(3);
|
qrCode.setScaleY(3);
|
||||||
|
|
||||||
@@ -205,7 +209,7 @@ public class MicrosoftAccountLoginPane extends JFXDialogLayout implements Dialog
|
|||||||
cancelAllTasks();
|
cancelAllTasks();
|
||||||
|
|
||||||
HintPane errHintPane = new HintPane(MessageDialogPane.MessageType.ERROR);
|
HintPane errHintPane = new HintPane(MessageDialogPane.MessageType.ERROR);
|
||||||
errHintPane.setText(failed.message());
|
errHintPane.setSegment(failed.message());
|
||||||
rootContainer.getChildren().add(errHintPane);
|
rootContainer.getChildren().add(errHintPane);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
|
|||||||
} else {
|
} else {
|
||||||
handler.reject(e.getMessage());
|
handler.reject(e.getMessage());
|
||||||
}
|
}
|
||||||
}).executor(true), i18n("message.downloading"), TaskCancellationAction.NORMAL);
|
}), i18n("message.downloading"), TaskCancellationAction.NORMAL);
|
||||||
} else {
|
} else {
|
||||||
// otherwise we still consider the file as modpack zip file
|
// otherwise we still consider the file as modpack zip file
|
||||||
// since casually the url may not ends with ".zip"
|
// since casually the url may not ends with ".zip"
|
||||||
@@ -156,7 +156,7 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
|
|||||||
} else {
|
} else {
|
||||||
handler.reject(e.getMessage());
|
handler.reject(e.getMessage());
|
||||||
}
|
}
|
||||||
}).executor(true),
|
}),
|
||||||
i18n("message.downloading"),
|
i18n("message.downloading"),
|
||||||
TaskCancellationAction.NORMAL
|
TaskCancellationAction.NORMAL
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ public final class Versions {
|
|||||||
i18n("install.failed.downloading.detail", file.getFile().getUrl()) + "\n" + StringUtils.getStackTrace(e),
|
i18n("install.failed.downloading.detail", file.getFile().getUrl()) + "\n" + StringUtils.getStackTrace(e),
|
||||||
i18n("download.failed.no_code"), MessageDialogPane.MessageType.ERROR);
|
i18n("download.failed.no_code"), MessageDialogPane.MessageType.ERROR);
|
||||||
}
|
}
|
||||||
}).executor(true),
|
}),
|
||||||
i18n("message.downloading"),
|
i18n("message.downloading"),
|
||||||
TaskCancellationAction.NORMAL
|
TaskCancellationAction.NORMAL
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,12 +24,16 @@ import org.jackhuang.hmcl.util.io.HttpRequest;
|
|||||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.util.Base64;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||||
|
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||||
|
|
||||||
public class OAuth {
|
public class OAuth {
|
||||||
public static final OAuth MICROSOFT = new OAuth(
|
public static final OAuth MICROSOFT = new OAuth(
|
||||||
@@ -77,17 +81,31 @@ public class OAuth {
|
|||||||
|
|
||||||
private Result authenticateAuthorizationCode(Options options) throws IOException, InterruptedException, JsonParseException, ExecutionException, AuthenticationException {
|
private Result authenticateAuthorizationCode(Options options) throws IOException, InterruptedException, JsonParseException, ExecutionException, AuthenticationException {
|
||||||
Session session = options.callback.startServer();
|
Session session = options.callback.startServer();
|
||||||
|
|
||||||
|
String codeVerifier = session.getCodeVerifier();
|
||||||
|
String state = session.getState();
|
||||||
|
String codeChallenge = generateCodeChallenge(codeVerifier);
|
||||||
|
|
||||||
options.callback.openBrowser(GrantFlow.AUTHORIZATION_CODE, NetworkUtils.withQuery(authorizationURL,
|
options.callback.openBrowser(GrantFlow.AUTHORIZATION_CODE, NetworkUtils.withQuery(authorizationURL,
|
||||||
mapOf(pair("client_id", options.callback.getClientId()), pair("response_type", "code"),
|
mapOf(pair("client_id", options.callback.getClientId()),
|
||||||
pair("redirect_uri", session.getRedirectURI()), pair("scope", options.scope),
|
pair("response_type", "code"),
|
||||||
pair("prompt", "select_account"))));
|
pair("redirect_uri", session.getRedirectURI()),
|
||||||
|
pair("scope", options.scope),
|
||||||
|
pair("prompt", "select_account"),
|
||||||
|
pair("code_challenge", codeChallenge),
|
||||||
|
pair("state", state),
|
||||||
|
pair("code_challenge_method", "S256")
|
||||||
|
)));
|
||||||
String code = session.waitFor();
|
String code = session.waitFor();
|
||||||
|
|
||||||
// Authorization Code -> Token
|
// Authorization Code -> Token
|
||||||
AuthorizationResponse response = HttpRequest.POST(accessTokenURL)
|
AuthorizationResponse response = HttpRequest.POST(accessTokenURL)
|
||||||
.form(pair("client_id", options.callback.getClientId()), pair("code", code),
|
.form(pair("client_id", options.callback.getClientId()),
|
||||||
pair("grant_type", "authorization_code"), pair("client_secret", options.callback.getClientSecret()),
|
pair("code", code),
|
||||||
pair("redirect_uri", session.getRedirectURI()), pair("scope", options.scope))
|
pair("grant_type", "authorization_code"),
|
||||||
|
pair("code_verifier", codeVerifier),
|
||||||
|
pair("redirect_uri", session.getRedirectURI()),
|
||||||
|
pair("scope", options.scope))
|
||||||
.ignoreHttpCode()
|
.ignoreHttpCode()
|
||||||
.retry(5)
|
.retry(5)
|
||||||
.getJson(AuthorizationResponse.class);
|
.getJson(AuthorizationResponse.class);
|
||||||
@@ -153,10 +171,6 @@ public class OAuth {
|
|||||||
pair("grant_type", "refresh_token")
|
pair("grant_type", "refresh_token")
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!options.callback.isPublicClient()) {
|
|
||||||
query.put("client_secret", options.callback.getClientSecret());
|
|
||||||
}
|
|
||||||
|
|
||||||
RefreshResponse response = HttpRequest.POST(tokenURL)
|
RefreshResponse response = HttpRequest.POST(tokenURL)
|
||||||
.form(query)
|
.form(query)
|
||||||
.accept("application/json")
|
.accept("application/json")
|
||||||
@@ -174,6 +188,20 @@ public class OAuth {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String generateCodeChallenge(String codeVerifier) {
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc7636#section-4.2
|
||||||
|
try {
|
||||||
|
byte[] bytes = codeVerifier.getBytes(StandardCharsets.US_ASCII);
|
||||||
|
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
|
||||||
|
messageDigest.update(bytes, 0, bytes.length);
|
||||||
|
byte[] digest = messageDigest.digest();
|
||||||
|
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.warning("Failed to generate code challenge", e);
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void handleErrorResponse(ErrorResponse response) throws AuthenticationException {
|
private static void handleErrorResponse(ErrorResponse response) throws AuthenticationException {
|
||||||
if (response.error == null || response.errorDescription == null) {
|
if (response.error == null || response.errorDescription == null) {
|
||||||
return;
|
return;
|
||||||
@@ -207,6 +235,9 @@ public class OAuth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public interface Session {
|
public interface Session {
|
||||||
|
String getState();
|
||||||
|
|
||||||
|
String getCodeVerifier();
|
||||||
|
|
||||||
String getRedirectURI();
|
String getRedirectURI();
|
||||||
|
|
||||||
@@ -243,10 +274,6 @@ public class OAuth {
|
|||||||
void openBrowser(GrantFlow grantFlow, String url) throws IOException;
|
void openBrowser(GrantFlow grantFlow, String url) throws IOException;
|
||||||
|
|
||||||
String getClientId();
|
String getClientId();
|
||||||
|
|
||||||
String getClientSecret();
|
|
||||||
|
|
||||||
boolean isPublicClient();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum GrantFlow {
|
public enum GrantFlow {
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ package org.jackhuang.hmcl.download;
|
|||||||
import org.jackhuang.hmcl.task.Task;
|
import org.jackhuang.hmcl.task.Task;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||||
|
|
||||||
@@ -47,25 +49,60 @@ public class MultipleSourceVersionList extends VersionList<RemoteVersion> {
|
|||||||
|
|
||||||
private Task<?> refreshAsync(String gameVersion, int sourceIndex) {
|
private Task<?> refreshAsync(String gameVersion, int sourceIndex) {
|
||||||
VersionList<?> versionList = backends[sourceIndex];
|
VersionList<?> versionList = backends[sourceIndex];
|
||||||
return versionList.refreshAsync(gameVersion)
|
Task<?> refreshTask = versionList.refreshAsync(gameVersion);
|
||||||
.thenComposeAsync(() -> {
|
|
||||||
|
return new Task<>() {
|
||||||
|
private Task<?> nextTask = null;
|
||||||
|
|
||||||
|
{
|
||||||
|
setSignificance(TaskSignificance.MODERATE);
|
||||||
|
setName("MultipleSourceVersionList.refreshAsync(task=%s, index=%d, all=%d)".formatted(
|
||||||
|
refreshTask.getName(), sourceIndex, backends.length)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<Task<?>> getDependents() {
|
||||||
|
return List.of(refreshTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<? extends Task<?>> getDependencies() {
|
||||||
|
return nextTask != null ? List.of(nextTask) : List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isRelyingOnDependents() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute() throws Exception {
|
||||||
|
if (isDependentsSucceeded()) {
|
||||||
lock.writeLock().lock();
|
lock.writeLock().lock();
|
||||||
try {
|
try {
|
||||||
versions.putAll(gameVersion, versionList.getVersions(gameVersion));
|
versions.putAll(gameVersion, versionList.getVersions(gameVersion));
|
||||||
} catch (Exception e) {
|
|
||||||
if (sourceIndex == backends.length - 1) {
|
|
||||||
LOG.warning("Failed to fetch versions list from all sources", e);
|
|
||||||
throw e;
|
|
||||||
} else {
|
|
||||||
LOG.warning("Failed to fetch versions list and try to fetch from other source", e);
|
|
||||||
return refreshAsync(gameVersion, sourceIndex + 1);
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
lock.writeLock().unlock();
|
lock.writeLock().unlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
setResult(refreshTask.getResult());
|
||||||
});
|
} else {
|
||||||
|
Exception exception = refreshTask.getException();
|
||||||
|
assert exception != null;
|
||||||
|
|
||||||
|
if (sourceIndex == backends.length - 1) {
|
||||||
|
LOG.warning("Failed to fetch versions list from all sources", exception);
|
||||||
|
setSignificance(TaskSignificance.MINOR);
|
||||||
|
throw exception;
|
||||||
|
} else {
|
||||||
|
LOG.warning("Failed to fetch versions list and try to fetch from other source", exception);
|
||||||
|
nextTask = refreshAsync(gameVersion, sourceIndex + 1);
|
||||||
|
nextTask.storeTo(this::setResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
Reference in new issue
Block a user