feat(microsoft): WIP: use device code grant flow instead.
This commit is contained in:
350
HMCLCore/src/main/java/org/jackhuang/hmcl/auth/OAuth.java
Normal file
350
HMCLCore/src/main/java/org/jackhuang/hmcl/auth/OAuth.java
Normal file
@@ -0,0 +1,350 @@
|
||||
/*
|
||||
* 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.auth;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
|
||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
public class OAuth {
|
||||
public static final OAuth MICROSOFT = new OAuth(
|
||||
"https://login.live.com/oauth20_authorize.srf",
|
||||
"https://login.live.com/oauth20_token.srf",
|
||||
"https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode",
|
||||
"https://login.microsoftonline.com/consumers/oauth2/v2.0/token");
|
||||
|
||||
private final String authorizationURL;
|
||||
private final String accessTokenURL;
|
||||
private final String deviceCodeURL;
|
||||
private final String tokenURL;
|
||||
|
||||
public OAuth(String authorizationURL, String accessTokenURL, String deviceCodeURL, String tokenURL) {
|
||||
this.authorizationURL = authorizationURL;
|
||||
this.accessTokenURL = accessTokenURL;
|
||||
this.deviceCodeURL = deviceCodeURL;
|
||||
this.tokenURL = tokenURL;
|
||||
}
|
||||
|
||||
public Result authenticate(GrantFlow grantFlow, Options options) throws AuthenticationException {
|
||||
try {
|
||||
switch (grantFlow) {
|
||||
case AUTHORIZATION_CODE:
|
||||
return authenticateAuthorizationCode(options);
|
||||
case DEVICE:
|
||||
return authenticateDevice(options);
|
||||
default:
|
||||
throw new UnsupportedOperationException("grant flow " + grantFlow);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new ServerDisconnectException(e);
|
||||
} catch (InterruptedException e) {
|
||||
throw new NoSelectedCharacterException();
|
||||
} catch (ExecutionException e) {
|
||||
if (e.getCause() instanceof InterruptedException) {
|
||||
throw new NoSelectedCharacterException();
|
||||
} else {
|
||||
throw new ServerDisconnectException(e);
|
||||
}
|
||||
} catch (JsonParseException e) {
|
||||
throw new ServerResponseMalformedException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Result authenticateAuthorizationCode(Options options) throws IOException, InterruptedException, JsonParseException, ExecutionException, AuthenticationException {
|
||||
Session session = options.callback.startServer();
|
||||
options.callback.openBrowser(NetworkUtils.withQuery(authorizationURL,
|
||||
mapOf(pair("client_id", options.callback.getClientId()), pair("response_type", "code"),
|
||||
pair("redirect_uri", session.getRedirectURI()), pair("scope", options.scope),
|
||||
pair("prompt", "select_account"))));
|
||||
String code = session.waitFor();
|
||||
|
||||
// Authorization Code -> Token
|
||||
AuthorizationResponse response = HttpRequest.POST(accessTokenURL)
|
||||
.form(pair("client_id", options.callback.getClientId()), pair("code", code),
|
||||
pair("grant_type", "authorization_code"), pair("client_secret", options.callback.getClientSecret()),
|
||||
pair("redirect_uri", session.getRedirectURI()), pair("scope", options.scope))
|
||||
.ignoreHttpCode()
|
||||
.getJson(AuthorizationResponse.class);
|
||||
handleErrorResponse(response);
|
||||
return new Result(response.accessToken, response.refreshToken);
|
||||
}
|
||||
|
||||
private Result authenticateDevice(Options options) throws IOException, InterruptedException, JsonParseException, AuthenticationException {
|
||||
DeviceTokenResponse deviceTokenResponse = HttpRequest.POST(deviceCodeURL)
|
||||
.form(pair("client_id", options.callback.getClientId()), pair("scope", options.scope))
|
||||
.ignoreHttpCode()
|
||||
.getJson(DeviceTokenResponse.class);
|
||||
|
||||
options.callback.grantDeviceCode(deviceTokenResponse.deviceCode, deviceTokenResponse.verificationURI);
|
||||
|
||||
// Microsoft OAuth Flow
|
||||
options.callback.openBrowser(deviceTokenResponse.verificationURI);
|
||||
|
||||
long startTime = System.nanoTime();
|
||||
int interval = deviceTokenResponse.interval;
|
||||
|
||||
while (true) {
|
||||
Thread.sleep(Math.max(interval, 1));
|
||||
|
||||
// We stop waiting if user does not respond our authentication request in 15 minutes.
|
||||
long estimatedTime = System.nanoTime() - startTime;
|
||||
if (TimeUnit.MINUTES.convert(estimatedTime, TimeUnit.SECONDS) >= Math.min(deviceTokenResponse.expiresIn, 900)) {
|
||||
throw new NoSelectedCharacterException();
|
||||
}
|
||||
|
||||
TokenResponse tokenResponse = HttpRequest.POST(tokenURL)
|
||||
.form(
|
||||
pair("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
|
||||
pair("code", deviceTokenResponse.deviceCode),
|
||||
pair("client_id", options.callback.getClientId()))
|
||||
.getJson(TokenResponse.class);
|
||||
|
||||
if ("authorization_pending".equals(tokenResponse.error)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ("expired_token".equals(tokenResponse.error)) {
|
||||
throw new NoSelectedCharacterException();
|
||||
}
|
||||
|
||||
if ("slow_down".equals(tokenResponse.error)) {
|
||||
interval += 5;
|
||||
continue;
|
||||
}
|
||||
|
||||
return new Result(tokenResponse.accessToken, tokenResponse.refreshToken);
|
||||
}
|
||||
}
|
||||
|
||||
public Result refresh(String refreshToken, Options options) throws AuthenticationException {
|
||||
try {
|
||||
RefreshResponse response = HttpRequest.POST(accessTokenURL)
|
||||
.form(pair("client_id", options.callback.getClientId()),
|
||||
pair("client_secret", options.callback.getClientSecret()),
|
||||
pair("refresh_token", refreshToken),
|
||||
pair("grant_type", "refresh_token"))
|
||||
.accept("application/json")
|
||||
.ignoreHttpCode()
|
||||
.getJson(RefreshResponse.class);
|
||||
|
||||
handleErrorResponse(response);
|
||||
|
||||
return new Result(response.accessToken, response.refreshToken);
|
||||
} catch (IOException e) {
|
||||
throw new ServerDisconnectException(e);
|
||||
} catch (JsonParseException e) {
|
||||
throw new ServerResponseMalformedException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void handleErrorResponse(ErrorResponse response) throws AuthenticationException {
|
||||
if (response.error == null || response.errorDescription == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (response.error) {
|
||||
case "invalid_grant":
|
||||
if (response.errorDescription.contains("The user must sign in again and if needed grant the client application access to the requested scope")) {
|
||||
throw new CredentialExpiredException();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
throw new RemoteAuthenticationException(response.error, response.errorDescription, "");
|
||||
}
|
||||
|
||||
public static class Options {
|
||||
private String userAgent;
|
||||
private final String scope;
|
||||
private final Callback callback;
|
||||
|
||||
public Options(String scope, Callback callback) {
|
||||
this.scope = scope;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
public Options setUserAgent(String userAgent) {
|
||||
this.userAgent = userAgent;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public interface Session {
|
||||
|
||||
String getRedirectURI();
|
||||
|
||||
/**
|
||||
* Wait for authentication
|
||||
*
|
||||
* @return authentication code
|
||||
* @throws InterruptedException if interrupted
|
||||
* @throws ExecutionException if an I/O error occurred.
|
||||
*/
|
||||
String waitFor() throws InterruptedException, ExecutionException;
|
||||
|
||||
default String getIdToken() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
/**
|
||||
* Start OAuth callback server at localhost.
|
||||
*
|
||||
* @throws IOException if an I/O error occurred.
|
||||
*/
|
||||
Session startServer() throws IOException, AuthenticationException;
|
||||
|
||||
void grantDeviceCode(String userCode, String verificationURI);
|
||||
|
||||
/**
|
||||
* Open browser
|
||||
*
|
||||
* @param url OAuth url.
|
||||
*/
|
||||
void openBrowser(String url) throws IOException;
|
||||
|
||||
String getClientId();
|
||||
|
||||
String getClientSecret();
|
||||
}
|
||||
|
||||
public enum GrantFlow {
|
||||
AUTHORIZATION_CODE,
|
||||
DEVICE,
|
||||
}
|
||||
|
||||
public class Result {
|
||||
private final String accessToken;
|
||||
private final String refreshToken;
|
||||
|
||||
public Result(String accessToken, String refreshToken) {
|
||||
this.accessToken = accessToken;
|
||||
this.refreshToken = refreshToken;
|
||||
}
|
||||
|
||||
public String getAccessToken() {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
public String getRefreshToken() {
|
||||
return refreshToken;
|
||||
}
|
||||
}
|
||||
|
||||
private static class DeviceTokenResponse {
|
||||
@SerializedName("user_code")
|
||||
public String userCode;
|
||||
|
||||
@SerializedName("device_code")
|
||||
public String deviceCode;
|
||||
|
||||
// The URI to be visited for user.
|
||||
@SerializedName("verification_uri")
|
||||
public String verificationURI;
|
||||
|
||||
// Life time in seconds for device_code and user_code
|
||||
@SerializedName("expires_in")
|
||||
public int expiresIn;
|
||||
|
||||
// Polling interval
|
||||
@SerializedName("interval")
|
||||
public int interval;
|
||||
}
|
||||
|
||||
private static class TokenResponse extends ErrorResponse {
|
||||
@SerializedName("token_type")
|
||||
public String tokenType;
|
||||
|
||||
@SerializedName("expires_in")
|
||||
public int expiresIn;
|
||||
|
||||
@SerializedName("ext_expires_in")
|
||||
public int extExpiresIn;
|
||||
|
||||
@SerializedName("scope")
|
||||
public String scope;
|
||||
|
||||
@SerializedName("access_token")
|
||||
public String accessToken;
|
||||
|
||||
@SerializedName("refresh_token")
|
||||
public String refreshToken;
|
||||
|
||||
}
|
||||
|
||||
private static class ErrorResponse {
|
||||
@SerializedName("error")
|
||||
public String error;
|
||||
|
||||
@SerializedName("error_description")
|
||||
public String errorDescription;
|
||||
|
||||
@SerializedName("correlation_id")
|
||||
public String correlationId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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":"??????"}
|
||||
*/
|
||||
public static class AuthorizationResponse extends ErrorResponse {
|
||||
@SerializedName("token_type")
|
||||
public String tokenType;
|
||||
|
||||
@SerializedName("expires_in")
|
||||
public int expiresIn;
|
||||
|
||||
@SerializedName("scope")
|
||||
public String scope;
|
||||
|
||||
@SerializedName("access_token")
|
||||
public String accessToken;
|
||||
|
||||
@SerializedName("refresh_token")
|
||||
public String refreshToken;
|
||||
|
||||
@SerializedName("user_id")
|
||||
public String userId;
|
||||
|
||||
@SerializedName("foci")
|
||||
public String foci;
|
||||
}
|
||||
|
||||
private static class RefreshResponse extends ErrorResponse {
|
||||
@SerializedName("expires_in")
|
||||
int expiresIn;
|
||||
|
||||
@SerializedName("access_token")
|
||||
String accessToken;
|
||||
|
||||
@SerializedName("refresh_token")
|
||||
String refreshToken;
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.auth.*;
|
||||
import org.jackhuang.hmcl.auth.OAuth;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.CompleteGameProfile;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
|
||||
@@ -36,11 +37,9 @@ import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
@@ -50,20 +49,15 @@ import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
public class MicrosoftService {
|
||||
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 OAuthCallback callback;
|
||||
private final OAuth.Callback callback;
|
||||
|
||||
private final ObservableOptionalCache<UUID, CompleteGameProfile, AuthenticationException> profileRepository;
|
||||
|
||||
public MicrosoftService(OAuthCallback callback) {
|
||||
public MicrosoftService(OAuth.Callback callback) {
|
||||
this.callback = requireNonNull(callback);
|
||||
this.profileRepository = new ObservableOptionalCache<>(uuid -> {
|
||||
LOG.info("Fetching properties of " + uuid);
|
||||
@@ -76,37 +70,11 @@ public class MicrosoftService {
|
||||
}
|
||||
|
||||
public MicrosoftSession authenticate() throws AuthenticationException {
|
||||
// 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
|
||||
OAuthSession session = callback.startServer();
|
||||
callback.openBrowser(NetworkUtils.withQuery(AUTHORIZATION_URL,
|
||||
mapOf(pair("client_id", callback.getClientId()), 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(ACCESS_TOKEN_URL)
|
||||
.form(mapOf(pair("client_id", callback.getClientId()), pair("code", code),
|
||||
pair("grant_type", "authorization_code"), pair("client_secret", callback.getClientSecret()),
|
||||
pair("redirect_uri", session.getRedirectURI()), pair("scope", SCOPE)))
|
||||
.getString();
|
||||
LiveAuthorizationResponse response = JsonUtils.fromNonNullJson(responseText,
|
||||
LiveAuthorizationResponse.class);
|
||||
|
||||
return authenticateViaLiveAccessToken(response.accessToken, response.refreshToken);
|
||||
OAuth.Result result = OAuth.MICROSOFT.authenticate(OAuth.GrantFlow.DEVICE, new OAuth.Options(SCOPE, callback));
|
||||
return authenticateViaLiveAccessToken(result.getAccessToken(), result.getRefreshToken());
|
||||
} catch (IOException e) {
|
||||
throw new ServerDisconnectException(e);
|
||||
} catch (InterruptedException e) {
|
||||
throw new NoSelectedCharacterException();
|
||||
} catch (ExecutionException e) {
|
||||
if (e.getCause() instanceof InterruptedException) {
|
||||
throw new NoSelectedCharacterException();
|
||||
} else {
|
||||
throw new ServerDisconnectException(e);
|
||||
}
|
||||
} catch (JsonParseException e) {
|
||||
throw new ServerResponseMalformedException(e);
|
||||
}
|
||||
@@ -114,19 +82,8 @@ public class MicrosoftService {
|
||||
|
||||
public MicrosoftSession refresh(MicrosoftSession oldSession) throws AuthenticationException {
|
||||
try {
|
||||
LiveRefreshResponse response = HttpRequest.POST(ACCESS_TOKEN_URL)
|
||||
.form(pair("client_id", callback.getClientId()),
|
||||
pair("client_secret", callback.getClientSecret()),
|
||||
pair("refresh_token", oldSession.getRefreshToken()),
|
||||
pair("grant_type", "refresh_token"))
|
||||
.accept("application/json")
|
||||
.ignoreHttpErrorCode(400)
|
||||
.ignoreHttpErrorCode(401)
|
||||
.getJson(LiveRefreshResponse.class);
|
||||
|
||||
handleLiveErrorMessage(response);
|
||||
|
||||
return authenticateViaLiveAccessToken(response.accessToken, response.refreshToken);
|
||||
OAuth.Result result = OAuth.MICROSOFT.refresh(oldSession.getRefreshToken(), new OAuth.Options(SCOPE, callback));
|
||||
return authenticateViaLiveAccessToken(result.getAccessToken(), result.getRefreshToken());
|
||||
} catch (IOException e) {
|
||||
throw new ServerDisconnectException(e);
|
||||
} catch (JsonParseException e) {
|
||||
@@ -134,22 +91,6 @@ public class MicrosoftService {
|
||||
}
|
||||
}
|
||||
|
||||
private void handleLiveErrorMessage(LiveErrorResponse response) throws AuthenticationException {
|
||||
if (response.error == null || response.errorDescription == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (response.error) {
|
||||
case "invalid_grant":
|
||||
if (response.errorDescription.contains("The user must sign in again and if needed grant the client application access to the requested scope")) {
|
||||
throw new CredentialExpiredException();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
throw new RemoteAuthenticationException(response.error, response.errorDescription, "");
|
||||
}
|
||||
|
||||
private String getUhs(XBoxLiveAuthenticationResponse response, String existingUhs) throws AuthenticationException {
|
||||
if (response.errorCode != 0) {
|
||||
throw new XboxAuthorizationException(response.errorCode, response.redirectUrl);
|
||||
@@ -357,57 +298,6 @@ public class MicrosoftService {
|
||||
public static class NoXuiException extends AuthenticationException {
|
||||
}
|
||||
|
||||
public static class LiveErrorResponse {
|
||||
@SerializedName("error")
|
||||
public String error;
|
||||
|
||||
@SerializedName("error_description")
|
||||
public String errorDescription;
|
||||
|
||||
@SerializedName("correlation_id")
|
||||
public String correlationId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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":"??????"}
|
||||
*/
|
||||
public static class LiveAuthorizationResponse extends LiveErrorResponse {
|
||||
@SerializedName("token_type")
|
||||
public String tokenType;
|
||||
|
||||
@SerializedName("expires_in")
|
||||
public int expiresIn;
|
||||
|
||||
@SerializedName("scope")
|
||||
public String scope;
|
||||
|
||||
@SerializedName("access_token")
|
||||
public String accessToken;
|
||||
|
||||
@SerializedName("refresh_token")
|
||||
public String refreshToken;
|
||||
|
||||
@SerializedName("user_id")
|
||||
public String userId;
|
||||
|
||||
@SerializedName("foci")
|
||||
public String foci;
|
||||
}
|
||||
|
||||
private static class LiveRefreshResponse extends LiveErrorResponse {
|
||||
@SerializedName("expires_in")
|
||||
int expiresIn;
|
||||
|
||||
@SerializedName("access_token")
|
||||
String accessToken;
|
||||
|
||||
@SerializedName("refresh_token")
|
||||
String refreshToken;
|
||||
}
|
||||
|
||||
private static class XBoxLiveAuthenticationResponseDisplayClaims {
|
||||
List<Map<Object, Object>> xui;
|
||||
}
|
||||
@@ -530,44 +420,6 @@ public class MicrosoftService {
|
||||
public String developerMessage;
|
||||
}
|
||||
|
||||
public interface OAuthCallback {
|
||||
/**
|
||||
* Start OAuth callback server at localhost.
|
||||
*
|
||||
* @throws IOException if an I/O error occurred.
|
||||
*/
|
||||
OAuthSession startServer() throws IOException, AuthenticationException;
|
||||
|
||||
/**
|
||||
* Open browser
|
||||
*
|
||||
* @param url OAuth url.
|
||||
*/
|
||||
void openBrowser(String url) throws IOException;
|
||||
|
||||
String getClientId();
|
||||
|
||||
String getClientSecret();
|
||||
}
|
||||
|
||||
public interface OAuthSession {
|
||||
|
||||
String getRedirectURI();
|
||||
|
||||
/**
|
||||
* Wait for authentication
|
||||
*
|
||||
* @return authentication code
|
||||
* @throws InterruptedException if interrupted
|
||||
* @throws ExecutionException if an I/O error occurred.
|
||||
*/
|
||||
String waitFor() throws InterruptedException, ExecutionException;
|
||||
|
||||
default String getIdToken() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static final Gson GSON = new GsonBuilder()
|
||||
.registerTypeAdapter(UUID.class, UUIDTypeAdapter.INSTANCE)
|
||||
.registerTypeAdapterFactory(ValidationTypeAdapterFactory.INSTANCE)
|
||||
|
||||
@@ -49,6 +49,7 @@ public abstract class HttpRequest {
|
||||
protected final Map<String, String> headers = new HashMap<>();
|
||||
protected ExceptionalBiConsumer<URL, Integer, IOException> responseCodeTester;
|
||||
protected final Set<Integer> toleratedHttpCodes = new HashSet<>();
|
||||
protected boolean ignoreHttpCode;
|
||||
|
||||
private HttpRequest(String url, String method) {
|
||||
this.url = url;
|
||||
@@ -76,6 +77,11 @@ public abstract class HttpRequest {
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpRequest ignoreHttpCode() {
|
||||
ignoreHttpCode = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public abstract String getString() throws IOException;
|
||||
|
||||
public CompletableFuture<String> getStringAsync() {
|
||||
@@ -173,7 +179,7 @@ public abstract class HttpRequest {
|
||||
responseCodeTester.accept(new URL(url), con.getResponseCode());
|
||||
} else {
|
||||
if (con.getResponseCode() / 100 != 2) {
|
||||
if (!toleratedHttpCodes.contains(con.getResponseCode())) {
|
||||
if (!ignoreHttpCode && !toleratedHttpCodes.contains(con.getResponseCode())) {
|
||||
String data = NetworkUtils.readData(con);
|
||||
throw new ResponseCodeException(new URL(url), con.getResponseCode(), data);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user