mirror of
https://github.com/HMCL-dev/HMCL.git
synced 2024-11-27 06:10:08 +08:00
feat: authenticate Microsoft accounts via external browser
This commit is contained in:
parent
b39508922f
commit
5890f0c782
@ -36,6 +36,7 @@ def buildnumber = System.getenv("BUILD_NUMBER") ?: dev ?: "SNAPSHOT"
|
||||
if (System.getenv("BUILD_NUMBER") != null && System.getenv("BUILD_NUMBER_OFFSET") != null)
|
||||
buildnumber = (Integer.parseInt(System.getenv("BUILD_NUMBER")) - Integer.parseInt(System.getenv("BUILD_NUMBER_OFFSET"))).toString()
|
||||
def versionroot = System.getenv("VERSION_ROOT") ?: "3.3"
|
||||
def microsoftAuthSecret = System.getenv("MICROSOFT_AUTH_SECRET") ?: ""
|
||||
version = versionroot + '.' + buildnumber
|
||||
|
||||
mainClassName = 'org.jackhuang.hmcl.Main'
|
||||
@ -120,10 +121,11 @@ shadowJar {
|
||||
classifier = null
|
||||
|
||||
manifest {
|
||||
attributes 'Created-By': 'Copyright(c) 2013-2020 huangyuhui.',
|
||||
attributes 'Created-By': 'Copyright(c) 2013-2021 huangyuhui.',
|
||||
'Main-Class': mainClassName,
|
||||
'Multi-Release': 'true',
|
||||
'Implementation-Version': project.version,
|
||||
'Microsoft-Auth-Secret': microsoftAuthSecret,
|
||||
'Class-Path': 'pack200.jar',
|
||||
'Add-Opens': [
|
||||
'java.base/java.lang',
|
||||
|
@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import fi.iki.elonen.NanoHTTPD;
|
||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.microsoft.MicrosoftService;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
|
||||
public class MicrosoftAuthenticationServer extends NanoHTTPD implements MicrosoftService.OAuthSession {
|
||||
private final int port;
|
||||
private final CompletableFuture<String> future = new CompletableFuture<>();
|
||||
|
||||
private MicrosoftAuthenticationServer(int port) {
|
||||
super(port);
|
||||
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRedirectURI() {
|
||||
return String.format("http://localhost:%d/auth-response", port);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientSecret() {
|
||||
return System.getProperty("hmcl.microsoft.auth.secret",
|
||||
JarUtils.thisJar().flatMap(JarUtils::getManifest).map(manifest -> manifest.getMainAttributes().getValue("Microsoft-Auth-Secret")).orElse(""));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String waitFor() throws InterruptedException, ExecutionException {
|
||||
return future.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response serve(IHTTPSession session) {
|
||||
if (session.getMethod() != Method.GET || !"/auth-response".equals(session.getUri())) {
|
||||
return newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_HTML, "");
|
||||
}
|
||||
Map<String, String> query = mapOf(NetworkUtils.parseQuery(session.getQueryParameterString()));
|
||||
if (query.containsKey("code")) {
|
||||
future.complete(query.get("code"));
|
||||
} else {
|
||||
future.completeExceptionally(new AuthenticationException("failed to authenticate"));
|
||||
}
|
||||
|
||||
String html;
|
||||
try {
|
||||
html = IOUtils.readFullyAsString(MicrosoftAuthenticationServer.class.getResourceAsStream("/assets/microsoft_auth.html"));
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Failed to load html");
|
||||
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_HTML, "");
|
||||
}
|
||||
thread(() -> {
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
stop();
|
||||
} catch (InterruptedException e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Failed to sleep for 1 second");
|
||||
}
|
||||
});
|
||||
return newFixedLengthResponse(html);
|
||||
}
|
||||
|
||||
public static class Factory implements MicrosoftService.OAuthCallback {
|
||||
|
||||
@Override
|
||||
public MicrosoftService.OAuthSession startServer() throws IOException {
|
||||
IOException exception = null;
|
||||
for (int port : new int[]{29111, 29112, 29113, 29114, 29115}) {
|
||||
try {
|
||||
MicrosoftAuthenticationServer server = new MicrosoftAuthenticationServer(port);
|
||||
server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
|
||||
return server;
|
||||
} catch (IOException e) {
|
||||
exception = e;
|
||||
}
|
||||
}
|
||||
throw exception;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void openBrowser(String url) throws IOException {
|
||||
// TODO: error!
|
||||
FXUtils.openLink(url);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* 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
|
||||
@ -27,13 +27,7 @@ import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.AccountFactory;
|
||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccountFactory;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorArtifactInfo;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorArtifactProvider;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloader;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.SimpleAuthlibInjectorArtifactProvider;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.*;
|
||||
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
|
||||
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccountFactory;
|
||||
import org.jackhuang.hmcl.auth.microsoft.MicrosoftService;
|
||||
@ -41,8 +35,8 @@ import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
|
||||
import org.jackhuang.hmcl.game.MicrosoftAuthenticationServer;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.account.MicrosoftAccountLoginStage;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Paths;
|
||||
@ -84,7 +78,7 @@ public final class Accounts {
|
||||
public static final OfflineAccountFactory FACTORY_OFFLINE = OfflineAccountFactory.INSTANCE;
|
||||
public static final YggdrasilAccountFactory FACTORY_MOJANG = YggdrasilAccountFactory.MOJANG;
|
||||
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER, Accounts::getOrCreateAuthlibInjectorServer);
|
||||
public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(MicrosoftAccountLoginStage.INSTANCE));
|
||||
public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(new MicrosoftAuthenticationServer.Factory()));
|
||||
public static final List<AccountFactory<?>> FACTORIES = immutableListOf(FACTORY_OFFLINE, FACTORY_MOJANG, FACTORY_MICROSOFT, FACTORY_AUTHLIB_INJECTOR);
|
||||
|
||||
// ==== login type / account factory mapping ====
|
||||
|
@ -35,7 +35,6 @@ import org.jackhuang.hmcl.setting.Profiles;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.account.AuthlibInjectorServersPage;
|
||||
import org.jackhuang.hmcl.ui.account.MicrosoftAccountLoginStage;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.construct.InputDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
@ -151,7 +150,6 @@ public final class Controllers {
|
||||
Logging.LOG.info("Start initializing application");
|
||||
|
||||
Controllers.stage = stage;
|
||||
MicrosoftAccountLoginStage.INSTANCE.initOwner(stage);
|
||||
|
||||
stage.setHeight(config().getHeight());
|
||||
stageHeight.bind(stage.heightProperty());
|
||||
|
@ -1,72 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.account;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.stage.Modality;
|
||||
import org.jackhuang.hmcl.auth.microsoft.MicrosoftService;
|
||||
import org.jackhuang.hmcl.ui.WebStage;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import static org.jackhuang.hmcl.Launcher.COOKIE_MANAGER;
|
||||
|
||||
public class MicrosoftAccountLoginStage extends WebStage implements MicrosoftService.WebViewCallback {
|
||||
public static final MicrosoftAccountLoginStage INSTANCE = new MicrosoftAccountLoginStage();
|
||||
|
||||
CompletableFuture<String> future;
|
||||
Predicate<String> urlTester;
|
||||
|
||||
public MicrosoftAccountLoginStage() {
|
||||
super(600, 600);
|
||||
initModality(Modality.APPLICATION_MODAL);
|
||||
|
||||
titleProperty().bind(webEngine.titleProperty());
|
||||
|
||||
webEngine.locationProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (urlTester != null && urlTester.test(newValue)) {
|
||||
future.complete(newValue);
|
||||
hide();
|
||||
}
|
||||
});
|
||||
|
||||
showingProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (!newValue) {
|
||||
if (future != null) {
|
||||
future.completeExceptionally(new InterruptedException());
|
||||
}
|
||||
future = null;
|
||||
urlTester = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<String> show(MicrosoftService service, Predicate<String> urlTester, String initialURL) {
|
||||
Platform.runLater(() -> {
|
||||
COOKIE_MANAGER.getCookieStore().removeAll();
|
||||
|
||||
webEngine.load(initialURL);
|
||||
show();
|
||||
});
|
||||
this.future = new CompletableFuture<>();
|
||||
this.urlTester = urlTester;
|
||||
return future;
|
||||
}
|
||||
}
|
37
HMCL/src/main/resources/assets/microsoft_auth.html
Normal file
37
HMCL/src/main/resources/assets/microsoft_auth.html
Normal file
@ -0,0 +1,37 @@
|
||||
<!--
|
||||
Hello Minecraft! Launcher
|
||||
Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Hello Minecraft! Launcher</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div>
|
||||
你可以关闭本标签页了
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.close()
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
@ -12,6 +12,7 @@ dependencies {
|
||||
api group: 'org.jenkins-ci', name: 'constant-pool-scanner', version: '1.2'
|
||||
api group: 'com.github.steveice10', name: 'opennbt', version: '1.1'
|
||||
api group: 'com.nqzero', name: 'permit-reflect', version: '0.3'
|
||||
api group: 'org.nanohttpd', name: 'nanohttpd', version: '2.3.1'
|
||||
compileOnlyApi group: 'org.jetbrains', name: 'annotations', version: '16.0.3'
|
||||
|
||||
// compileOnlyApi group: 'org.openjfx', name: 'javafx-base', version: '15', classifier: 'win'
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* 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
|
||||
@ -20,7 +20,9 @@ package org.jackhuang.hmcl.auth.microsoft;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.auth.*;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.*;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
@ -30,16 +32,15 @@ import org.jackhuang.hmcl.util.io.ResponseCodeException;
|
||||
import org.jackhuang.hmcl.util.javafx.ObservableOptionalCache;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.logging.Level;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Lang.threadPool;
|
||||
@ -47,22 +48,26 @@ import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
public class MicrosoftService {
|
||||
private static final ThreadPoolExecutor POOL = threadPool("MicrosoftProfileProperties", true, 2, 10, TimeUnit.SECONDS);
|
||||
private static final Pattern OAUTH_URL_PATTERN = Pattern.compile("^https://login\\.live\\.com/oauth20_desktop\\.srf\\?code=(.*?)&lc=(.*?)$");
|
||||
private static final String CLIENT_ID = "6a3728d6-27a3-4180-99bb-479895b8f88e";
|
||||
private static final String AUTHORIZATION_URL = "https://login.live.com/oauth20_authorize.srf";
|
||||
private static final String ACCESS_TOKEN_URL = "https://login.live.com/oauth20_token.srf";
|
||||
private static final String SCOPE = "XboxLive.signin offline_access";
|
||||
private static final int[] PORTS = { 29111, 29112, 29113, 29114, 29115 };
|
||||
private static final ThreadPoolExecutor POOL = threadPool("MicrosoftProfileProperties", true, 2, 10,
|
||||
TimeUnit.SECONDS);
|
||||
private static final Pattern OAUTH_URL_PATTERN = Pattern
|
||||
.compile("^https://login\\.live\\.com/oauth20_desktop\\.srf\\?code=(.*?)&lc=(.*?)$");
|
||||
|
||||
private final WebViewCallback callback;
|
||||
private final OAuthCallback callback;
|
||||
|
||||
private final ObservableOptionalCache<String, MinecraftProfileResponse, AuthenticationException> profileRepository;
|
||||
|
||||
public MicrosoftService(WebViewCallback callback) {
|
||||
this.callback = callback;
|
||||
this.profileRepository = new ObservableOptionalCache<>(
|
||||
authorization -> {
|
||||
LOG.info("Fetching properties");
|
||||
return getCompleteProfile(authorization);
|
||||
},
|
||||
(uuid, e) -> LOG.log(Level.WARNING, "Failed to fetch properties of " + uuid, e),
|
||||
POOL);
|
||||
public MicrosoftService(OAuthCallback callback) {
|
||||
this.callback = requireNonNull(callback);
|
||||
this.profileRepository = new ObservableOptionalCache<>(authorization -> {
|
||||
LOG.info("Fetching properties");
|
||||
return getCompleteProfile(authorization);
|
||||
}, (uuid, e) -> LOG.log(Level.WARNING, "Failed to fetch properties of " + uuid, e), POOL);
|
||||
}
|
||||
|
||||
public ObservableOptionalCache<String, MinecraftProfileResponse, AuthenticationException> getProfileRepository() {
|
||||
@ -70,62 +75,78 @@ public class MicrosoftService {
|
||||
}
|
||||
|
||||
public MicrosoftSession authenticate() throws AuthenticationException {
|
||||
requireNonNull(callback);
|
||||
|
||||
// Example URL:
|
||||
// https://login.live.com/oauth20_authorize.srf?response_type=code&client_id=6a3728d6-27a3-4180-99bb-479895b8f88e&redirect_uri=http://localhost:29111/auth-response&scope=XboxLive.signin+offline_access&state=612fd24a2447427383e8b222b597db66&prompt=select_account
|
||||
try {
|
||||
// Microsoft OAuth Flow
|
||||
String code = callback.show(this, urlToBeTested -> OAUTH_URL_PATTERN.matcher(urlToBeTested).find(), "https://login.live.com/oauth20_authorize.srf" +
|
||||
"?client_id=00000000402b5328" +
|
||||
"&response_type=code" +
|
||||
"&scope=service%3A%3Auser.auth.xboxlive.com%3A%3AMBI_SSL" +
|
||||
"&redirect_uri=https%3A%2F%2Flogin.live.com%2Foauth20_desktop.srf")
|
||||
.thenApply(url -> {
|
||||
Matcher matcher = OAUTH_URL_PATTERN.matcher(url);
|
||||
matcher.find();
|
||||
return matcher.group(1);
|
||||
})
|
||||
.get();
|
||||
OAuthSession session = callback.startServer();
|
||||
callback.openBrowser(NetworkUtils.withQuery(AUTHORIZATION_URL,
|
||||
mapOf(pair("client_id", CLIENT_ID), pair("response_type", "code"),
|
||||
pair("redirect_uri", session.getRedirectURI()), pair("scope", SCOPE),
|
||||
pair("prompt", "select_account"))));
|
||||
String code = session.waitFor();
|
||||
|
||||
// Authorization Code -> Token
|
||||
String responseText = HttpRequest.POST("https://login.live.com/oauth20_token.srf").form(mapOf(
|
||||
pair("client_id", "00000000402b5328"),
|
||||
pair("code", code),
|
||||
pair("grant_type", "authorization_code"),
|
||||
pair("redirect_uri", "https://login.live.com/oauth20_desktop.srf"),
|
||||
pair("scope", "service::user.auth.xboxlive.com::MBI_SSL"))).getString();
|
||||
LiveAuthorizationResponse response = JsonUtils.fromNonNullJson(responseText, LiveAuthorizationResponse.class);
|
||||
String responseText = HttpRequest.POST("https://login.live.com/oauth20_token.srf")
|
||||
.form(mapOf(pair("client_id", CLIENT_ID), pair("code", code),
|
||||
pair("grant_type", "authorization_code"), pair("client_secret", session.getClientSecret()),
|
||||
pair("redirect_uri", session.getRedirectURI()), pair("scope", SCOPE)))
|
||||
.getString();
|
||||
LiveAuthorizationResponse response = JsonUtils.fromNonNullJson(responseText,
|
||||
LiveAuthorizationResponse.class);
|
||||
|
||||
// Authenticate with XBox Live
|
||||
XBoxLiveAuthenticationResponse xboxResponse = HttpRequest.POST("https://user.auth.xboxlive.com/user/authenticate")
|
||||
XBoxLiveAuthenticationResponse xboxResponse = HttpRequest
|
||||
.POST("https://user.auth.xboxlive.com/user/authenticate")
|
||||
.json(mapOf(
|
||||
pair("Properties", mapOf(
|
||||
pair("AuthMethod", "RPS"),
|
||||
pair("SiteName", "user.auth.xboxlive.com"),
|
||||
pair("RpsTicket", response.accessToken)
|
||||
)),
|
||||
pair("RelyingParty", "http://auth.xboxlive.com"),
|
||||
pair("TokenType", "JWT")))
|
||||
.getJson(XBoxLiveAuthenticationResponse.class);
|
||||
pair("Properties",
|
||||
mapOf(pair("AuthMethod", "RPS"), pair("SiteName", "user.auth.xboxlive.com"),
|
||||
pair("RpsTicket", "d=" + response.accessToken))),
|
||||
pair("RelyingParty", "http://auth.xboxlive.com"), pair("TokenType", "JWT")))
|
||||
.accept("application/json").getJson(XBoxLiveAuthenticationResponse.class);
|
||||
String uhs = (String) xboxResponse.displayClaims.xui.get(0).get("uhs");
|
||||
|
||||
// Authenticate with XSTS
|
||||
XBoxLiveAuthenticationResponse xstsResponse = HttpRequest.POST("https://xsts.auth.xboxlive.com/xsts/authorize")
|
||||
// Authenticate Minecraft with XSTS
|
||||
XBoxLiveAuthenticationResponse minecraftXstsResponse = HttpRequest
|
||||
.POST("https://xsts.auth.xboxlive.com/xsts/authorize")
|
||||
.json(mapOf(
|
||||
pair("Properties", mapOf(
|
||||
pair("SandboxId", "RETAIL"),
|
||||
pair("UserTokens", Collections.singletonList(xboxResponse.token))
|
||||
)),
|
||||
pair("RelyingParty", "rp://api.minecraftservices.com/"),
|
||||
pair("TokenType", "JWT")))
|
||||
pair("Properties",
|
||||
mapOf(pair("SandboxId", "RETAIL"),
|
||||
pair("UserTokens", Collections.singletonList(xboxResponse.token)))),
|
||||
pair("RelyingParty", "rp://api.minecraftservices.com/"), pair("TokenType", "JWT")))
|
||||
.getJson(XBoxLiveAuthenticationResponse.class);
|
||||
String minecraftXstsUhs = (String) minecraftXstsResponse.displayClaims.xui.get(0).get("uhs");
|
||||
if (!Objects.equals(uhs, minecraftXstsUhs)) {
|
||||
throw new ServerResponseMalformedException("uhs mismatched");
|
||||
}
|
||||
|
||||
// Authenticate XBox with XSTS
|
||||
XBoxLiveAuthenticationResponse xboxXstsResponse = HttpRequest
|
||||
.POST("https://xsts.auth.xboxlive.com/xsts/authorize")
|
||||
.json(mapOf(
|
||||
pair("Properties",
|
||||
mapOf(pair("SandboxId", "RETAIL"),
|
||||
pair("UserTokens", Collections.singletonList(xboxResponse.token)))),
|
||||
pair("RelyingParty", "http://xboxlive.com"), pair("TokenType", "JWT")))
|
||||
.getJson(XBoxLiveAuthenticationResponse.class);
|
||||
String xboxXstsUhs = (String) xboxXstsResponse.displayClaims.xui.get(0).get("uhs");
|
||||
if (!Objects.equals(uhs, xboxXstsUhs)) {
|
||||
throw new ServerResponseMalformedException("uhs mismatched");
|
||||
}
|
||||
|
||||
getXBoxProfile(uhs, xboxXstsResponse.token);
|
||||
|
||||
// Authenticate with Minecraft
|
||||
MinecraftLoginWithXBoxResponse minecraftResponse = HttpRequest.POST("https://api.minecraftservices.com/authentication/login_with_xbox")
|
||||
.json(mapOf(pair("identityToken", "XBL3.0 x=" + uhs + ";" + xstsResponse.token)))
|
||||
.getJson(MinecraftLoginWithXBoxResponse.class);
|
||||
MinecraftLoginWithXBoxResponse minecraftResponse = HttpRequest
|
||||
.POST("https://api.minecraftservices.com/authentication/login_with_xbox")
|
||||
.json(mapOf(pair("identityToken", "XBL3.0 x=" + uhs + ";" + minecraftXstsResponse.token)))
|
||||
.accept("application/json").getJson(MinecraftLoginWithXBoxResponse.class);
|
||||
|
||||
long notAfter = minecraftResponse.expiresIn * 1000L + System.currentTimeMillis();
|
||||
|
||||
// Checking Game Ownership
|
||||
MinecraftStoreResponse storeResponse = HttpRequest.GET("https://api.minecraftservices.com/entitlements/mcstore")
|
||||
MinecraftStoreResponse storeResponse = HttpRequest
|
||||
.GET("https://api.minecraftservices.com/entitlements/mcstore")
|
||||
.authorization(String.format("%s %s", minecraftResponse.tokenType, minecraftResponse.accessToken))
|
||||
.getJson(MinecraftStoreResponse.class);
|
||||
handleErrorResponse(storeResponse);
|
||||
@ -133,7 +154,8 @@ public class MicrosoftService {
|
||||
throw new NoCharacterException();
|
||||
}
|
||||
|
||||
return new MicrosoftSession(minecraftResponse.tokenType, minecraftResponse.accessToken, new MicrosoftSession.User(minecraftResponse.username), null);
|
||||
return new MicrosoftSession(minecraftResponse.tokenType, minecraftResponse.accessToken, notAfter,
|
||||
new MicrosoftSession.User(minecraftResponse.username), null);
|
||||
} catch (IOException e) {
|
||||
throw new ServerDisconnectException(e);
|
||||
} catch (InterruptedException e) {
|
||||
@ -152,11 +174,14 @@ public class MicrosoftService {
|
||||
public MicrosoftSession refresh(MicrosoftSession oldSession) throws AuthenticationException {
|
||||
try {
|
||||
// Get the profile
|
||||
MinecraftProfileResponse profileResponse = HttpRequest.GET(NetworkUtils.toURL("https://api.minecraftservices.com/minecraft/profile"))
|
||||
MinecraftProfileResponse profileResponse = HttpRequest
|
||||
.GET(NetworkUtils.toURL("https://api.minecraftservices.com/minecraft/profile"))
|
||||
.authorization(String.format("%s %s", oldSession.getTokenType(), oldSession.getAccessToken()))
|
||||
.getJson(MinecraftProfileResponse.class);
|
||||
handleErrorResponse(profileResponse);
|
||||
return new MicrosoftSession(oldSession.getTokenType(), oldSession.getAccessToken(), oldSession.getUser(), new MicrosoftSession.GameProfile(profileResponse.id, profileResponse.name));
|
||||
return new MicrosoftSession(oldSession.getTokenType(), oldSession.getAccessToken(),
|
||||
oldSession.getNotAfter(), oldSession.getUser(),
|
||||
new MicrosoftSession.GameProfile(profileResponse.id, profileResponse.name));
|
||||
} catch (IOException e) {
|
||||
throw new ServerDisconnectException(e);
|
||||
} catch (JsonParseException e) {
|
||||
@ -166,9 +191,9 @@ public class MicrosoftService {
|
||||
|
||||
public Optional<MinecraftProfileResponse> getCompleteProfile(String authorization) throws AuthenticationException {
|
||||
try {
|
||||
return Optional.ofNullable(HttpRequest.GET(NetworkUtils.toURL("https://api.minecraftservices.com/minecraft/profile"))
|
||||
.authorization(authorization)
|
||||
.getJson(MinecraftProfileResponse.class));
|
||||
return Optional.ofNullable(
|
||||
HttpRequest.GET(NetworkUtils.toURL("https://api.minecraftservices.com/minecraft/profile"))
|
||||
.authorization(authorization).getJson(MinecraftProfileResponse.class));
|
||||
} catch (IOException e) {
|
||||
throw new ServerDisconnectException(e);
|
||||
} catch (JsonParseException e) {
|
||||
@ -182,13 +207,11 @@ public class MicrosoftService {
|
||||
|
||||
try {
|
||||
HttpRequest.GET(NetworkUtils.toURL("https://api.minecraftservices.com/minecraft/profile"))
|
||||
.authorization(String.format("%s %s", tokenType, accessToken))
|
||||
.filter((url, responseCode) -> {
|
||||
.authorization(String.format("%s %s", tokenType, accessToken)).filter((url, responseCode) -> {
|
||||
if (responseCode / 100 == 4) {
|
||||
throw new ResponseCodeException(url, responseCode);
|
||||
}
|
||||
})
|
||||
.getString();
|
||||
}).getString();
|
||||
return true;
|
||||
} catch (ResponseCodeException e) {
|
||||
return false;
|
||||
@ -211,13 +234,44 @@ public class MicrosoftService {
|
||||
if (!profile.skins.isEmpty()) {
|
||||
textures.put(TextureType.SKIN, new Texture(profile.skins.get(0).url, null));
|
||||
}
|
||||
// if (!profile.capes.isEmpty()) {
|
||||
// textures.put(TextureType.CAPE, new Texture(profile.capes.get(0).url, null);
|
||||
// }
|
||||
// if (!profile.capes.isEmpty()) {
|
||||
// textures.put(TextureType.CAPE, new Texture(profile.capes.get(0).url, null);
|
||||
// }
|
||||
|
||||
return Optional.of(textures);
|
||||
}
|
||||
|
||||
private static void getXBoxProfile(String uhs, String xstsToken) throws IOException {
|
||||
HttpRequest.GET("https://profile.xboxlive.com/users/me/profile/settings",
|
||||
pair("settings", "GameDisplayName,AppDisplayName,AppDisplayPicRaw,GameDisplayPicRaw,"
|
||||
+ "PublicGamerpic,ShowUserAsAvatar,Gamerscore,Gamertag,ModernGamertag,ModernGamertagSuffix,"
|
||||
+ "UniqueModernGamertag,AccountTier,TenureLevel,XboxOneRep,"
|
||||
+ "PreferredColor,Location,Bio,Watermarks," + "RealName,RealNameOverride,IsQuarantined"))
|
||||
.contentType("application/json").accept("application/json")
|
||||
.authorization(String.format("XBL3.0 x=%s;%s", uhs, xstsToken)).header("x-xbl-contract-version", "3")
|
||||
.getString();
|
||||
}
|
||||
|
||||
private static MinecraftProfileResponse getMinecraftProfile(String tokenType, String accessToken)
|
||||
throws IOException, AuthenticationException {
|
||||
HttpURLConnection conn = HttpRequest.GET("https://api.minecraftservices.com/minecraft/profile")
|
||||
.contentType("application/json").authorization(String.format("%s %s", tokenType, accessToken))
|
||||
.createConnection();
|
||||
int responseCode = conn.getResponseCode();
|
||||
if (responseCode == HTTP_NOT_FOUND) {
|
||||
throw new NoCharacterException();
|
||||
}
|
||||
|
||||
String result = NetworkUtils.readData(conn);
|
||||
return JsonUtils.fromNonNullJson(result, MinecraftProfileResponse.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Error response: {"error":"invalid_grant","error_description":"The provided
|
||||
* value for the 'redirect_uri' is not valid. The value must exactly match the
|
||||
* redirect URI used to obtain the authorization
|
||||
* code.","correlation_id":"??????"}
|
||||
*/
|
||||
private static class LiveAuthorizationResponse {
|
||||
@SerializedName("token_type")
|
||||
String tokenType;
|
||||
@ -245,6 +299,18 @@ public class MicrosoftService {
|
||||
List<Map<Object, Object>> xui;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Success Response: { "IssueInstant":"2020-12-07T19:52:08.4463796Z",
|
||||
* "NotAfter":"2020-12-21T19:52:08.4463796Z", "Token":"token", "DisplayClaims":{
|
||||
* "xui":[ { "uhs":"userhash" } ] } }
|
||||
*
|
||||
* Error response: { "Identity":"0", "XErr":2148916238, "Message":"",
|
||||
* "Redirect":"https://start.ui.xboxlive.com/AddChildToFamily" }
|
||||
*
|
||||
* XErr Candidates: 2148916233 = missing XBox account 2148916238 = child account
|
||||
* not linked to a family
|
||||
*/
|
||||
private static class XBoxLiveAuthenticationResponse {
|
||||
@SerializedName("IssueInstant")
|
||||
String issueInstant;
|
||||
@ -341,8 +407,36 @@ public class MicrosoftService {
|
||||
public String developerMessage;
|
||||
}
|
||||
|
||||
public interface WebViewCallback {
|
||||
CompletableFuture<String> show(MicrosoftService service, Predicate<String> urlTester, String initialURL);
|
||||
public interface OAuthCallback {
|
||||
/**
|
||||
* Start OAuth callback server at localhost.
|
||||
*
|
||||
* @throws IOException if an I/O error occurred.
|
||||
*/
|
||||
OAuthSession startServer() throws IOException;
|
||||
|
||||
/**
|
||||
* Open browser
|
||||
*
|
||||
* @param url OAuth url.
|
||||
*/
|
||||
void openBrowser(String url) throws IOException;
|
||||
}
|
||||
|
||||
public interface OAuthSession {
|
||||
|
||||
String getRedirectURI();
|
||||
|
||||
String getClientSecret() throws IOException;
|
||||
|
||||
/**
|
||||
* Wait for authentication
|
||||
*
|
||||
* @return authentication code
|
||||
* @throws InterruptedException if interrupted
|
||||
* @throws ExecutionException if an I/O error occurred.
|
||||
*/
|
||||
String waitFor() throws InterruptedException, ExecutionException;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* 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
|
||||
@ -30,13 +30,15 @@ import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
public class MicrosoftSession {
|
||||
private final String tokenType;
|
||||
private final long notAfter;
|
||||
private final String accessToken;
|
||||
private final User user;
|
||||
private final GameProfile profile;
|
||||
|
||||
public MicrosoftSession(String tokenType, String accessToken, User user, GameProfile profile) {
|
||||
public MicrosoftSession(String tokenType, String accessToken, long notAfter, User user, GameProfile profile) {
|
||||
this.tokenType = tokenType;
|
||||
this.accessToken = accessToken;
|
||||
this.notAfter = notAfter;
|
||||
this.user = user;
|
||||
this.profile = profile;
|
||||
}
|
||||
@ -49,6 +51,10 @@ public class MicrosoftSession {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
public long getNotAfter() {
|
||||
return notAfter;
|
||||
}
|
||||
|
||||
public String getAuthorization() {
|
||||
return String.format("%s %s", getTokenType(), getAccessToken());
|
||||
}
|
||||
@ -62,25 +68,27 @@ public class MicrosoftSession {
|
||||
}
|
||||
|
||||
public static MicrosoftSession fromStorage(Map<?, ?> storage) {
|
||||
UUID uuid = tryCast(storage.get("uuid"), String.class).map(UUIDTypeAdapter::fromString).orElseThrow(() -> new IllegalArgumentException("uuid is missing"));
|
||||
String name = tryCast(storage.get("displayName"), String.class).orElseThrow(() -> new IllegalArgumentException("displayName is missing"));
|
||||
String tokenType = tryCast(storage.get("tokenType"), String.class).orElseThrow(() -> new IllegalArgumentException("tokenType is missing"));
|
||||
String accessToken = tryCast(storage.get("accessToken"), String.class).orElseThrow(() -> new IllegalArgumentException("accessToken is missing"));
|
||||
String userId = tryCast(storage.get("userid"), String.class).orElseThrow(() -> new IllegalArgumentException("userid is missing"));
|
||||
return new MicrosoftSession(tokenType, accessToken, new User(userId), new GameProfile(uuid, name));
|
||||
UUID uuid = tryCast(storage.get("uuid"), String.class).map(UUIDTypeAdapter::fromString)
|
||||
.orElseThrow(() -> new IllegalArgumentException("uuid is missing"));
|
||||
String name = tryCast(storage.get("displayName"), String.class)
|
||||
.orElseThrow(() -> new IllegalArgumentException("displayName is missing"));
|
||||
String tokenType = tryCast(storage.get("tokenType"), String.class)
|
||||
.orElseThrow(() -> new IllegalArgumentException("tokenType is missing"));
|
||||
String accessToken = tryCast(storage.get("accessToken"), String.class)
|
||||
.orElseThrow(() -> new IllegalArgumentException("accessToken is missing"));
|
||||
Long notAfter = tryCast(storage.get("notAfter"), Long.class).orElse(0L);
|
||||
String userId = tryCast(storage.get("userid"), String.class)
|
||||
.orElseThrow(() -> new IllegalArgumentException("userid is missing"));
|
||||
return new MicrosoftSession(tokenType, accessToken, notAfter, new User(userId), new GameProfile(uuid, name));
|
||||
}
|
||||
|
||||
public Map<Object, Object> toStorage() {
|
||||
requireNonNull(profile);
|
||||
requireNonNull(user);
|
||||
|
||||
return mapOf(
|
||||
pair("tokenType", tokenType),
|
||||
pair("accessToken", accessToken),
|
||||
pair("uuid", UUIDTypeAdapter.fromUUID(profile.getId())),
|
||||
pair("displayName", profile.getName()),
|
||||
pair("userid", user.id)
|
||||
);
|
||||
return mapOf(pair("tokenType", tokenType), pair("accessToken", accessToken),
|
||||
pair("uuid", UUIDTypeAdapter.fromUUID(profile.getId())), pair("displayName", profile.getName()),
|
||||
pair("userid", user.id));
|
||||
}
|
||||
|
||||
public AuthInfo toAuthInfo() {
|
||||
|
@ -47,6 +47,17 @@ public final class Lang {
|
||||
*/
|
||||
@SafeVarargs
|
||||
public static <K, V> Map<K, V> mapOf(Pair<K, V>... pairs) {
|
||||
return mapOf(Arrays.asList(pairs));
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a mutable map by given key-value pairs.
|
||||
* @param pairs entries in the new map
|
||||
* @param <K> the type of keys
|
||||
* @param <V> the type of values
|
||||
* @return the map which contains data in {@code pairs}.
|
||||
*/
|
||||
public static <K, V> Map<K, V> mapOf(Iterable<Pair<K, V>> pairs) {
|
||||
Map<K, V> map = new LinkedHashMap<>();
|
||||
for (Pair<K, V> pair : pairs)
|
||||
map.put(pair.getKey(), pair.getValue());
|
||||
|
@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.util.io;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalBiConsumer;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
|
||||
@ -30,6 +31,7 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.gson.JsonUtils.GSON;
|
||||
import static org.jackhuang.hmcl.util.io.NetworkUtils.createHttpConnection;
|
||||
import static org.jackhuang.hmcl.util.io.NetworkUtils.resolveConnection;
|
||||
@ -73,7 +75,7 @@ public abstract class HttpRequest {
|
||||
return this;
|
||||
}
|
||||
|
||||
protected HttpURLConnection createConnection() throws IOException {
|
||||
public HttpURLConnection createConnection() throws IOException {
|
||||
HttpURLConnection con = createHttpConnection(url);
|
||||
con.setRequestMethod(method);
|
||||
for (Map.Entry<String, String> entry : headers.entrySet()) {
|
||||
@ -102,8 +104,7 @@ public abstract class HttpRequest {
|
||||
}
|
||||
|
||||
public <T> HttpPostRequest json(Object payload) throws JsonParseException {
|
||||
return string(payload instanceof String ? (String) payload : GSON.toJson(payload),
|
||||
"application/json");
|
||||
return string(payload instanceof String ? (String) payload : GSON.toJson(payload), "application/json");
|
||||
}
|
||||
|
||||
public HttpPostRequest form(Map<String, String> params) {
|
||||
@ -136,6 +137,11 @@ public abstract class HttpRequest {
|
||||
return GET(new URL(url));
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static HttpGetRequest GET(String url, Pair<String, String>... query) throws MalformedURLException {
|
||||
return GET(new URL(NetworkUtils.withQuery(url, mapOf(query))));
|
||||
}
|
||||
|
||||
public static HttpGetRequest GET(URL url) {
|
||||
return new HttpGetRequest(url);
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* 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
|
||||
@ -17,14 +17,15 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.util.io;
|
||||
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.StringUtils.*;
|
||||
|
||||
/**
|
||||
@ -32,6 +33,8 @@ import static org.jackhuang.hmcl.util.StringUtils.*;
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class NetworkUtils {
|
||||
public static final String PARAMETER_SEPARATOR = "&";
|
||||
public static final String NAME_VALUE_SEPARATOR = "=";
|
||||
|
||||
private NetworkUtils() {
|
||||
}
|
||||
@ -48,15 +51,38 @@ public final class NetworkUtils {
|
||||
}
|
||||
first = false;
|
||||
} else {
|
||||
sb.append('&');
|
||||
sb.append(PARAMETER_SEPARATOR);
|
||||
}
|
||||
sb.append(encodeURL(param.getKey()));
|
||||
sb.append('=');
|
||||
sb.append(NAME_VALUE_SEPARATOR);
|
||||
sb.append(encodeURL(param.getValue()));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static List<Pair<String, String>> parseQuery(URI uri) {
|
||||
return parseQuery(uri.getRawQuery());
|
||||
}
|
||||
|
||||
public static List<Pair<String, String>> parseQuery(String queryParameterString) {
|
||||
List<Pair<String, String>> result = new ArrayList<>();
|
||||
|
||||
try (Scanner scanner = new Scanner(queryParameterString)) {
|
||||
scanner.useDelimiter("&");
|
||||
while (scanner.hasNext()) {
|
||||
String[] nameValue = scanner.next().split(NAME_VALUE_SEPARATOR);
|
||||
if (nameValue.length <= 0 || nameValue.length > 2) {
|
||||
throw new IllegalArgumentException("bad query string");
|
||||
}
|
||||
|
||||
String name = decodeURL(nameValue[0]);
|
||||
String value = nameValue.length == 2 ? decodeURL(nameValue[1]) : null;
|
||||
result.add(pair(name, value));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static URLConnection createConnection(URL url) throws IOException {
|
||||
URLConnection connection = url.openConnection();
|
||||
connection.setUseCaches(false);
|
||||
@ -71,7 +97,8 @@ public final class NetworkUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* @see <a href="https://github.com/curl/curl/blob/3f7b1bb89f92c13e69ee51b710ac54f775aab320/lib/transfer.c#L1427-L1461">Curl</a>
|
||||
* @see <a href=
|
||||
* "https://github.com/curl/curl/blob/3f7b1bb89f92c13e69ee51b710ac54f775aab320/lib/transfer.c#L1427-L1461">Curl</a>
|
||||
* @param location the url to be URL encoded
|
||||
* @return encoded URL
|
||||
*/
|
||||
@ -81,8 +108,10 @@ public final class NetworkUtils {
|
||||
for (char ch : location.toCharArray()) {
|
||||
switch (ch) {
|
||||
case ' ':
|
||||
if (left) sb.append("%20");
|
||||
else sb.append('+');
|
||||
if (left)
|
||||
sb.append("%20");
|
||||
else
|
||||
sb.append('+');
|
||||
break;
|
||||
case '?':
|
||||
left = false;
|
||||
@ -100,7 +129,9 @@ public final class NetworkUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is a work-around that aims to solve problem when "Location" in stupid server's response is not encoded.
|
||||
* This method is a work-around that aims to solve problem when "Location" in
|
||||
* stupid server's response is not encoded.
|
||||
*
|
||||
* @see <a href="https://github.com/curl/curl/issues/473">Issue with libcurl</a>
|
||||
* @param conn the stupid http connection.
|
||||
* @return manually redirected http connection.
|
||||
@ -125,8 +156,10 @@ public final class NetworkUtils {
|
||||
throw new IOException("Too much redirects");
|
||||
}
|
||||
|
||||
HttpURLConnection redirected = (HttpURLConnection) new URL(conn.getURL(), encodeLocation(newURL)).openConnection();
|
||||
properties.forEach((key, value) -> value.forEach(element -> redirected.addRequestProperty(key, element)));
|
||||
HttpURLConnection redirected = (HttpURLConnection) new URL(conn.getURL(), encodeLocation(newURL))
|
||||
.openConnection();
|
||||
properties
|
||||
.forEach((key, value) -> value.forEach(element -> redirected.addRequestProperty(key, element)));
|
||||
redirected.setRequestMethod(method);
|
||||
conn = redirected;
|
||||
++redirect;
|
||||
@ -178,7 +211,8 @@ public final class NetworkUtils {
|
||||
}
|
||||
} catch (IOException e) {
|
||||
try (InputStream stderr = con.getErrorStream()) {
|
||||
if (stderr == null) throw e;
|
||||
if (stderr == null)
|
||||
throw e;
|
||||
return IOUtils.readFullyAsString(stderr, UTF_8);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user