TokenResponseData class
Пакет: com.hypixel.hytale.server.core.auth.oauth
Файл: com/hypixel/hytale/server/core/auth/oauth/OAuthClient.java
Поля (6)
| Модификаторы | Тип | Имя |
|---|---|---|
|
static final BuilderCodec<OAuthClient.TokenResponseData> |
CODEC |
|
String |
accessToken |
|
String |
error |
|
int |
expiresIn |
|
String |
idToken |
|
String |
refreshToken |
Исходный код
Показать/скрыть
class="kw">package com.hypixel.hytale.server.core.auth.oauth;
class="kw">import com.hypixel.hytale.codec.Codec;
class="kw">import com.hypixel.hytale.codec.EmptyExtraInfo;
class="kw">import com.hypixel.hytale.codec.KeyedCodec;
class="kw">import com.hypixel.hytale.codec.builder.BuilderCodec;
class="kw">import com.hypixel.hytale.codec.util.RawJsonReader;
class="kw">import com.hypixel.hytale.logger.HytaleLogger;
class="kw">import com.hypixel.hytale.server.core.HytaleServer;
class="kw">import com.hypixel.hytale.server.core.auth.AuthConfig;
class="kw">import com.hypixel.hytale.server.core.auth.HttpResponseException;
class="kw">import com.hypixel.hytale.server.core.util.ServiceHttpClientFactory;
class="kw">import com.sun.net.httpserver.HttpServer;
class="kw">import java.io.IOException;
class="kw">import java.io.OutputStream;
class="kw">import java.net.InetAddress;
class="kw">import java.net.InetSocketAddress;
class="kw">import java.net.URI;
class="kw">import java.net.URLDecoder;
class="kw">import java.net.URLEncoder;
class="kw">import java.net.http.HttpClient;
class="kw">import java.net.http.HttpRequest;
class="kw">import java.net.http.HttpResponse;
class="kw">import java.net.http.HttpRequest.BodyPublishers;
class="kw">import java.net.http.HttpResponse.BodyHandlers;
class="kw">import java.nio.charset.StandardCharsets;
class="kw">import java.security.MessageDigest;
class="kw">import java.security.SecureRandom;
class="kw">import java.time.Duration;
class="kw">import java.util.Base64;
class="kw">import java.util.concurrent.CompletableFuture;
class="kw">import java.util.concurrent.TimeUnit;
class="kw">import java.util.concurrent.atomic.AtomicBoolean;
class="kw">import java.util.logging.Level;
class="kw">import java.util.regex.Matcher;
class="kw">import java.util.regex.Pattern;
class="kw">import javax.annotation.Nonnull;
class="kw">import javax.annotation.Nullable;
class="kw">public class OAuthClient {
class="kw">private class="kw">static class="kw">final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
class="kw">private class="kw">static class="kw">final SecureRandom RANDOM = new SecureRandom();
class="kw">private class="kw">final HttpClient httpClient = ServiceHttpClientFactory.create(AuthConfig.HTTP_TIMEOUT);
class="kw">public OAuthClient() {
}
class="kw">public Runnable startFlow(@Nonnull OAuthBrowserFlow var1) {
AtomicBoolean var2 = new AtomicBoolean(false);
CompletableFuture.runAsync(
() -> {
HttpServer var3 = null;
try {
String var4 = this.generateRandomString(32);
String var5 = this.generateRandomString(64);
String var6 = this.generateCodeChallenge(var5);
var3 = HttpServer.create(new InetSocketAddress(InetAddress.ofLiteral("127.0.0.1"), 0), 0);
int var7 = var3.getAddress().getPort();
String var8 = this.encodeStateWithPort(var4, var7);
String var9 = "https://accounts.hytale.com/consent/client";
CompletableFuture var10 = new CompletableFuture<>();
HttpServer var11 = var3;
String var12 = var4;
var3.createContext(
"/",
var4x -> {
try {
String var5 = var4x.getRequestURI().getQuery();
String var6 = this.extractParam(var5, "code");
String var7 = this.extractParam(var5, "state");
String var8;
int var9;
if (var7 == null || !var7.equals(var12)) {
var8 = buildHtmlPage(
false,
"Authentication Failed",
"Authentication Failed",
"Something went wrong during authentication. Please close this window and try again.",
"Invalid state parameter"
);
var9 = 400;
var10.completeExceptionally(new Exception("Invalid state"));
} else if (var6 != null && !var6.isEmpty()) {
var8 = buildHtmlPage(
true,
"Authentication Successful",
"Authentication Successful",
"You have been logged in successfully. You can now close this window and class="kw">return to the server.",
null
);
var9 = 200;
var10.complete(var6);
} else {
String var10 = this.extractParam(var5, "error");
String var11 = var10 != null ? var10 : "No code received";
var8 = buildHtmlPage(
false,
"Authentication Failed",
"Authentication Failed",
"Something went wrong during authentication. Please close this window and try again.",
var11
);
var9 = 400;
var10.completeExceptionally(new Exception(var11));
}
var4x.sendResponseHeaders(var9, var8.length());
try (OutputStream var22 = var4x.getResponseBody()) {
var22.write(var8.getBytes(StandardCharsets.UTF_8));
}
} catch (Exception var20) {
((HytaleLogger.Api)LOGGER.at(Level.WARNING).withCause(var20)).log("Error handling OAuth callback");
} class="kw">finally {
HytaleServer.SCHEDULED_EXECUTOR.schedule(() -> var11.stop(0), 1L, TimeUnit.SECONDS);
}
}
);
var3.setExecutor(null);
var3.start();
String var13 = this.buildAuthUrl(var8, var6, var9);
var1.onFlowInfo(var13, formatVerificationCode(var6));
String var14 = var10.get(5L, TimeUnit.MINUTES);
if (var2.get()) {
var1.onFailure("Authentication cancelled");
class="kw">return;
}
OAuthClient.TokenResponse var15 = this.exchangeCodeForTokens(var14, var5, var9);
if (var15 != null) {
var1.onSuccess(var15);
class="kw">return;
}
var1.onFailure("Failed to exchange authorization code for tokens");
} catch (Exception var19) {
if (var19 class="kw">instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
((HytaleLogger.Api)LOGGER.at(Level.WARNING).withCause(var19)).log("OAuth browser flow failed");
if (!var2.get()) {
var1.onFailure(var19.getMessage());
}
class="kw">return;
} class="kw">finally {
if (var3 != null) {
var3.stop(0);
}
}
}
);
class="kw">return () -> var2.set(true);
}
class="kw">public Runnable startFlow(OAuthDeviceFlow var1) {
AtomicBoolean var2 = new AtomicBoolean(false);
CompletableFuture.runAsync(() -> {
try {
OAuthClient.DeviceAuthResponse var3 = this.requestDeviceAuthorization();
if (var3 == null) {
var1.onFailure("Failed to start device authorization");
class="kw">return;
}
var1.onFlowInfo(var3.userCode(), var3.verificationUri(), var3.verificationUriComplete(), var3.expiresIn());
int var4 = Math.max(var3.interval, 15);
long var5 = System.currentTimeMillis() + var3.expiresIn * 1000L;
while (System.currentTimeMillis() < var5 && !var2.get()) {
Thread.sleep(Duration.ofSeconds(var4));
OAuthClient.TokenResponse var7 = this.pollDeviceToken(var3.deviceCode);
if (var7 != null) {
if (var7.error == null) {
var1.onSuccess(var7);
class="kw">return;
}
if (!"authorization_pending".equals(var7.error)) {
if (!"slow_down".equals(var7.error)) {
var1.onFailure("Device authorization failed: " + var7.error);
class="kw">return;
}
var4 += 5;
}
}
}
if (var2.get()) {
var1.onFailure("Authentication cancelled");
} else {
var1.onFailure("Device authorization expired");
}
} catch (Exception var8) {
if (var8 class="kw">instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
((HytaleLogger.Api)LOGGER.at(Level.WARNING).withCause(var8)).log("OAuth device flow failed");
if (!var2.get()) {
var1.onFailure(var8.getMessage());
}
}
});
class="kw">return () -> var2.set(true);
}
@Nullable
class="kw">public OAuthClient.TokenResponse refreshTokens(@Nonnull String var1) class="kw">throws IOException, InterruptedException {
try {
String var2 = "grant_type=refresh_token&client_id="
+ URLEncoder.encode("hytale-server", StandardCharsets.UTF_8)
+ "&refresh_token="
+ URLEncoder.encode(var1, StandardCharsets.UTF_8);
HttpRequest var3 = HttpRequest.newBuilder()
.uri(URI.create("https://oauth.accounts.hytale.com/oauth2/token"))
.header("Content-Type", "application/x-www-form-urlencoded")
.header("User-Agent", AuthConfig.USER_AGENT)
.POST(BodyPublishers.ofString(var2))
.build();
HttpResponse var4 = this.httpClient.send(var3, BodyHandlers.ofString());
int var5 = var4.statusCode();
if (var5 != 200) {
LOGGER.at(Level.WARNING).log("Token refresh failed: HTTP %d - %s", var5, var4.body());
if (!AuthConfig.isRejectedStatusCode(var5)) {
throw new HttpResponseException(var5, var4.body());
} else {
class="kw">return null;
}
} else {
class="kw">return this.parseTokenResponse(var4.body());
}
} catch (IOException var6) {
((HytaleLogger.Api)LOGGER.at(Level.WARNING).withCause(var6)).log("Token refresh failed (IO error)");
throw var6;
} catch (InterruptedException var7) {
Thread.currentThread().interrupt();
throw var7;
} catch (Exception var8) {
((HytaleLogger.Api)LOGGER.at(Level.WARNING).withCause(var8)).log("Token refresh failed");
class="kw">return null;
}
}
class="kw">private String buildAuthUrl(String var1, String var2, String var3) {
class="kw">return "https://oauth.accounts.hytale.com/oauth2/auth?response_type=code&client_id="
+ URLEncoder.encode("hytale-server", StandardCharsets.UTF_8)
+ "&redirect_uri="
+ URLEncoder.encode(var3, StandardCharsets.UTF_8)
+ "&scope="
+ URLEncoder.encode(String.join(" ", AuthConfig.SCOPES), StandardCharsets.UTF_8)
+ "&state="
+ URLEncoder.encode(var1, StandardCharsets.UTF_8)
+ "&code_challenge="
+ URLEncoder.encode(var2, StandardCharsets.UTF_8)
+ "&code_challenge_method=S256";
}
class="kw">private class="kw">static String formatVerificationCode(String var0) {
String var1 = var0.replaceAll("[^A-Za-z0-9]", "");
class="kw">return var1.length() < 8 ? var1 : var1.substring(0, 4) + "-" + var1.substring(4, 8);
}
@Nullable
class="kw">private OAuthClient.TokenResponse exchangeCodeForTokens(String var1, String var2, String var3) {
try {
String var4 = "grant_type=authorization_code&client_id="
+ URLEncoder.encode("hytale-server", StandardCharsets.UTF_8)
+ "&code="
+ URLEncoder.encode(var1, StandardCharsets.UTF_8)
+ "&redirect_uri="
+ URLEncoder.encode(var3, StandardCharsets.UTF_8)
+ "&code_verifier="
+ URLEncoder.encode(var2, StandardCharsets.UTF_8);
HttpRequest var5 = HttpRequest.newBuilder()
.uri(URI.create("https://oauth.accounts.hytale.com/oauth2/token"))
.header("Content-Type", "application/x-www-form-urlencoded")
.header("User-Agent", AuthConfig.USER_AGENT)
.POST(BodyPublishers.ofString(var4))
.build();
HttpResponse var6 = this.httpClient.send(var5, BodyHandlers.ofString());
if (var6.statusCode() != 200) {
LOGGER.at(Level.WARNING).log("Token exchange failed: HTTP %d - %s", var6.statusCode(), var6.body());
class="kw">return null;
} else {
class="kw">return this.parseTokenResponse(var6.body());
}
} catch (Exception var7) {
if (var7 class="kw">instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
((HytaleLogger.Api)LOGGER.at(Level.WARNING).withCause(var7)).log("Token exchange failed");
class="kw">return null;
}
}
@Nullable
class="kw">private OAuthClient.DeviceAuthResponse requestDeviceAuthorization() {
try {
String var1 = "client_id="
+ URLEncoder.encode("hytale-server", StandardCharsets.UTF_8)
+ "&scope="
+ URLEncoder.encode(String.join(" ", AuthConfig.SCOPES), StandardCharsets.UTF_8);
HttpRequest var2 = HttpRequest.newBuilder()
.uri(URI.create("https://oauth.accounts.hytale.com/oauth2/device/auth"))
.header("Content-Type", "application/x-www-form-urlencoded")
.header("User-Agent", AuthConfig.USER_AGENT)
.POST(BodyPublishers.ofString(var1))
.build();
HttpResponse var3 = this.httpClient.send(var2, BodyHandlers.ofString());
if (var3.statusCode() != 200) {
LOGGER.at(Level.WARNING).log("Device authorization request failed: HTTP %d - %s", var3.statusCode(), var3.body());
class="kw">return null;
} else {
class="kw">return this.parseDeviceAuthResponse(var3.body());
}
} catch (Exception var4) {
if (var4 class="kw">instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
((HytaleLogger.Api)LOGGER.at(Level.WARNING).withCause(var4)).log("Device authorization request failed");
class="kw">return null;
}
}
@Nullable
class="kw">private OAuthClient.TokenResponse pollDeviceToken(String var1) {
try {
String var2 = "grant_type=urn:ietf:params:oauth:grant-type:device_code&client_id="
+ URLEncoder.encode("hytale-server", StandardCharsets.UTF_8)
+ "&device_code="
+ URLEncoder.encode(var1, StandardCharsets.UTF_8);
HttpRequest var3 = HttpRequest.newBuilder()
.uri(URI.create("https://oauth.accounts.hytale.com/oauth2/token"))
.header("Content-Type", "application/x-www-form-urlencoded")
.header("User-Agent", AuthConfig.USER_AGENT)
.POST(BodyPublishers.ofString(var2))
.build();
HttpResponse var4 = this.httpClient.send(var3, BodyHandlers.ofString());
if (var4.statusCode() == 400) {
class="kw">return this.parseTokenResponse(var4.body());
} else if (var4.statusCode() != 200) {
LOGGER.at(Level.WARNING).log("Device token poll failed: HTTP %d - %s", var4.statusCode(), var4.body());
class="kw">return null;
} else {
class="kw">return this.parseTokenResponse(var4.body());
}
} catch (Exception var5) {
if (var5 class="kw">instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
((HytaleLogger.Api)LOGGER.at(Level.WARNING).withCause(var5)).log("Device token poll failed");
class="kw">return null;
}
}
class="kw">private String generateRandomString(int var1) {
byte[] var2 = new byte[var1];
RANDOM.nextBytes(var2);
class="kw">return Base64.getUrlEncoder().withoutPadding().encodeToString(var2).substring(0, var1);
}
class="kw">private String generateCodeChallenge(String var1) {
try {
MessageDigest var2 = MessageDigest.getInstance("SHA-256");
byte[] var3 = var2.digest(var1.getBytes(StandardCharsets.US_ASCII));
class="kw">return Base64.getUrlEncoder().withoutPadding().encodeToString(var3);
} catch (Exception var4) {
throw new RuntimeException("Failed to generate code challenge", var4);
}
}
class="kw">private String extractParam(String var1, String var2) {
if (var1 == null) {
class="kw">return null;
}
Pattern var3 = Pattern.compile(var2 + "=([^&]*)");
Matcher var4 = var3.matcher(var1);
class="kw">return var4.find() ? URLDecoder.decode(var4.group(1), StandardCharsets.UTF_8) : null;
}
class="kw">private String encodeStateWithPort(String var1, int var2) {
String var3 = String.format("{\"state\":\"%s\",\"port\":\"%d\"}", var1, var2);
class="kw">return Base64.getUrlEncoder().withoutPadding().encodeToString(var3.getBytes(StandardCharsets.UTF_8));
}
class="kw">private OAuthClient.TokenResponse parseTokenResponse(String var1) class="kw">throws IOException {
OAuthClient.TokenResponseData var2 = OAuthClient.TokenResponseData.CODEC.decodeJson(new RawJsonReader(var1.toCharArray()), EmptyExtraInfo.EMPTY);
class="kw">return new OAuthClient.TokenResponse(var2.accessToken, var2.refreshToken, var2.idToken, var2.error, var2.expiresIn);
}
class="kw">private OAuthClient.DeviceAuthResponse parseDeviceAuthResponse(String var1) class="kw">throws IOException {
OAuthClient.DeviceAuthResponseData var2 = OAuthClient.DeviceAuthResponseData.CODEC
.decodeJson(new RawJsonReader(var1.toCharArray()), EmptyExtraInfo.EMPTY);
class="kw">return new OAuthClient.DeviceAuthResponse(
var2.deviceCode, var2.userCode, var2.verificationUri, var2.verificationUriComplete, var2.expiresIn, var2.interval
);
}
class="kw">private class="kw">static String buildHtmlPage(boolean var0, String var1, String var2, String var3, @Nullable String var4) {
String var5 = var4 != null && !var4.isEmpty() ? "<div class=\"error\">" + var4 + "</div>" : "";
String var6 = var0 ? "icon-success" : "icon-error";
String var7 = var0
? "<polyline points=\"20 6 9 17 4 12\"></polyline>"
: "<line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"></line><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"></line>";
class="kw">return "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>%s - Hytale</title>\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Lexend:wght@700&family=Nunito+Sans:wght@400;700&display=swap\" rel=\"stylesheet\">\n <style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n html { color-scheme: dark; background: linear-gradient(180deg, #15243A, #0F1418); min-height: 100vh; }\n body { font-family: \"Nunito Sans\", sans-serif; color: #b7cedd; min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; }\n .card { background: rgba(0,0,0,0.4); border: 2px solid rgba(71,81,107,0.6); border-radius: 12px; padding: 48px 40px; max-width: 420px; text-align: center; }\n .icon { width: 64px; height: 64px; margin: 0 auto 24px; border-radius: 50%%; display: flex; align-items: center; justify-content: center; }\n .icon svg { width: 32px; height: 32px; }\n .icon-success { background: linear-gradient(135deg, #2d5a3d, #1e3a2a); border: 2px solid #4a9d6b; }\n .icon-success svg { color: #6fcf97; }\n .icon-error { background: linear-gradient(135deg, #5a2d3d, #3a1e2a); border: 2px solid #c3194c; }\n .icon-error svg { color: #ff6b8a; }\n h1 { font-family: \"Lexend\", sans-serif; font-size: 1.5rem; text-transform: uppercase; background: linear-gradient(#f5fbff, #bfe6ff); -webkit-background-clip: text; background-clip: text; color: transparent; margin-bottom: 12px; }\n p { line-height: 1.6; }\n .error { background: rgba(195,25,76,0.15); border: 1px solid rgba(195,25,76,0.4); border-radius: 6px; padding: 12px; margin-top: 16px; color: #ff8fa8; font-size: 0.875rem; word-break: break-word; }\n </style>\n</head>\n<body><div class=\"card\"><div class=\"icon %s\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">%s</svg></div><h1>%s</h1><p>%s</p>%s</div></body>\n</html>\n"
.formatted(var1, var6, var7, var2, var3, var5);
}
class="kw">private class="kw">static <T> KeyedCodec<T> externalKey(String var0, Codec<T> var1) {
class="kw">return new KeyedCodec<>(var0, var1, false, true);
}
class="kw">public record DeviceAuthResponse(String deviceCode, String userCode, String verificationUri, String verificationUriComplete, int expiresIn, int interval) {
class="kw">public DeviceAuthResponse {
}
}
class="kw">private class="kw">static class="kw">final class DeviceAuthResponseData {
String deviceCode;
String userCode;
String verificationUri;
String verificationUriComplete;
int expiresIn = 600;
int interval = 5;
class="kw">static class="kw">final BuilderCodec<OAuthClient.DeviceAuthResponseData> CODEC = BuilderCodec.builder(
OAuthClient.DeviceAuthResponseData.class, OAuthClient.DeviceAuthResponseData::new
)
.append(OAuthClient.externalKey("device_code", Codec.STRING), (var0, var1) -> var0.deviceCode = var1, var0 -> var0.deviceCode)
.add()
.append(OAuthClient.externalKey("user_code", Codec.STRING), (var0, var1) -> var0.userCode = var1, var0 -> var0.userCode)
.add()
.append(OAuthClient.externalKey("verification_uri", Codec.STRING), (var0, var1) -> var0.verificationUri = var1, var0 -> var0.verificationUri)
.add()
.append(
OAuthClient.externalKey("verification_uri_complete", Codec.STRING),
(var0, var1) -> var0.verificationUriComplete = var1,
var0 -> var0.verificationUriComplete
)
.add()
.append(OAuthClient.externalKey("expires_in", Codec.INTEGER), (var0, var1) -> var0.expiresIn = var1, var0 -> var0.expiresIn)
.add()
.append(OAuthClient.externalKey("interval", Codec.INTEGER), (var0, var1) -> var0.interval = var1, var0 -> var0.interval)
.add()
.build();
class="kw">private DeviceAuthResponseData() {
}
}
class="kw">public record TokenResponse(@Nullable String accessToken, @Nullable String refreshToken, @Nullable String idToken, @Nullable String error, int expiresIn) {
class="kw">public TokenResponse {
}
class="kw">public boolean isSuccess() {
class="kw">return this.error == null && this.accessToken != null;
}
}
class="kw">private class="kw">static class="kw">final class TokenResponseData {
String accessToken;
String refreshToken;
String idToken;
String error;
int expiresIn;
class="kw">static class="kw">final BuilderCodec<OAuthClient.TokenResponseData> CODEC = BuilderCodec.builder(
OAuthClient.TokenResponseData.class, OAuthClient.TokenResponseData::new
)
.append(OAuthClient.externalKey("access_token", Codec.STRING), (var0, var1) -> var0.accessToken = var1, var0 -> var0.accessToken)
.add()
.append(OAuthClient.externalKey("refresh_token", Codec.STRING), (var0, var1) -> var0.refreshToken = var1, var0 -> var0.refreshToken)
.add()
.append(OAuthClient.externalKey("id_token", Codec.STRING), (var0, var1) -> var0.idToken = var1, var0 -> var0.idToken)
.add()
.append(OAuthClient.externalKey("error", Codec.STRING), (var0, var1) -> var0.error = var1, var0 -> var0.error)
.add()
.append(OAuthClient.externalKey("expires_in", Codec.INTEGER), (var0, var1) -> var0.expiresIn = var1, var0 -> var0.expiresIn)
.add()
.build();
class="kw">private TokenResponseData() {
}
}
}