diff --git a/src/main/java/moe/yushi/authlibinjector/AuthlibInjector.java b/src/main/java/moe/yushi/authlibinjector/AuthlibInjector.java index 972e741..499b0b1 100644 --- a/src/main/java/moe/yushi/authlibinjector/AuthlibInjector.java +++ b/src/main/java/moe/yushi/authlibinjector/AuthlibInjector.java @@ -3,12 +3,16 @@ package moe.yushi.authlibinjector; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Optional.empty; import static java.util.Optional.of; +import static moe.yushi.authlibinjector.util.IOUtils.asBytes; import static moe.yushi.authlibinjector.util.IOUtils.asString; -import static moe.yushi.authlibinjector.util.IOUtils.getURL; import static moe.yushi.authlibinjector.util.IOUtils.removeNewLines; import java.io.IOException; import java.io.UncheckedIOException; import java.lang.instrument.ClassFileTransformer; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; import java.util.Base64; import java.util.Optional; import java.util.concurrent.atomic.AtomicBoolean; @@ -61,8 +65,18 @@ public final class AuthlibInjector { */ public static final String PROP_DUMP_CLASS = "authlibinjector.dumpClass"; + /** + * The side that authlib-injector runs on. + * Possible values: client, server. + */ + public static final String PROP_SIDE = "authlibinjector.side"; + + public static final String PROP_ALI_REDIRECT_LIMIT = "authlibinjector.ali.redirectLimit"; + // ==== + private static final int REDIRECT_LIMIT = Integer.getInteger(PROP_ALI_REDIRECT_LIMIT, 5); + private AuthlibInjector() {} private static AtomicBoolean booted = new AtomicBoolean(false); @@ -99,7 +113,10 @@ public final class AuthlibInjector { String apiRoot = System.getProperty(PROP_API_ROOT); if (apiRoot == null) return empty(); - apiRoot = appendSuffixSlash(apiRoot); + ExecutionEnvironment side = detectSide(); + Logging.LAUNCH.fine("Detected side: " + side); + + apiRoot = parseInputUrl(apiRoot); Logging.CONFIG.info("API root: " + apiRoot); warnIfHttp(apiRoot); @@ -107,8 +124,40 @@ public final class AuthlibInjector { Optional prefetched = getPrefetchedResponse(); if (!prefetched.isPresent()) { + try { - metadataResponse = asString(getURL(apiRoot)); + HttpURLConnection connection; + boolean redirectAllowed = side == ExecutionEnvironment.SERVER; + int redirectCount = 0; + for (;;) { + connection = (HttpURLConnection) new URL(apiRoot).openConnection(); + Optional ali = getApiLocationIndication(connection); + if (ali.isPresent()) { + if (!redirectAllowed) { + Logging.CONFIG.warning("Redirect is not allowed, ignoring ALI: " + ali.get()); + break; + } + + connection.disconnect(); + + apiRoot = ali.get(); + if (redirectCount >= REDIRECT_LIMIT) { + Logging.CONFIG.severe("Exceeded maximum number of redirects (" + REDIRECT_LIMIT + "), refusing to redirect to: " + apiRoot); + throw new InjectorInitializationException(); + } + redirectCount++; + Logging.CONFIG.info("Redirect to: " + apiRoot); + warnIfHttp(apiRoot); + } else { + break; + } + } + + try { + metadataResponse = asString(asBytes(connection.getInputStream())); + } finally { + connection.disconnect(); + } } catch (IOException e) { Logging.CONFIG.severe("Failed to fetch metadata: " + e); throw new InjectorInitializationException(e); @@ -155,6 +204,60 @@ public final class AuthlibInjector { } } + private static String parseInputUrl(String url) { + String lowercased = url.toLowerCase(); + if (!lowercased.startsWith("http://") && !lowercased.startsWith("https://")) { + url = "https://" + url; + } + + url = appendSuffixSlash(url); + return url; + } + + private static Optional getApiLocationIndication(URLConnection conn) { + return Optional.ofNullable(conn.getHeaderFields().get("X-Authlib-Injector-API-Location")) + .flatMap(list -> list.isEmpty() ? Optional.empty() : Optional.of(list.get(0))) + .flatMap(indication -> { + String currentUrl = appendSuffixSlash(conn.getURL().toString()); + String newUrl; + try { + newUrl = appendSuffixSlash(new URL(conn.getURL(), indication).toString()); + } catch (MalformedURLException e) { + Logging.CONFIG.warning("Failed to resolve absolute ALI, the header is [" + indication + "]. Ignore it."); + return Optional.empty(); + } + + if (newUrl.equals(currentUrl)) { + return Optional.empty(); + } else { + return Optional.of(newUrl); + } + }); + } + + private static ExecutionEnvironment detectSide() { + String specifiedSide = System.getProperty(PROP_SIDE); + if (specifiedSide != null) { + switch (specifiedSide) { + case "client": + return ExecutionEnvironment.CLIENT; + case "server": + return ExecutionEnvironment.SERVER; + default: + Logging.LAUNCH.warning("Invalid value [" + specifiedSide + "] for parameter " + PROP_SIDE + ", ignoring."); + break; + } + } + + // fallback + if (System.getProperty(PROP_PREFETCHED_DATA) != null || System.getProperty(PROP_PREFETCHED_DATA_OLD) != null) { + Logging.LAUNCH.warning("Prefetched configuration must be used along with parameter " + PROP_SIDE); + return ExecutionEnvironment.CLIENT; + } else { + return ExecutionEnvironment.SERVER; + } + } + private static ClassTransformer createTransformer(YggdrasilConfiguration config) { ClassTransformer transformer = new ClassTransformer(); transformer.debugSaveClass = "true".equals(System.getProperty(PROP_DUMP_CLASS)); diff --git a/src/main/java/moe/yushi/authlibinjector/ExecutionEnvironment.java b/src/main/java/moe/yushi/authlibinjector/ExecutionEnvironment.java new file mode 100644 index 0000000..a1a11b8 --- /dev/null +++ b/src/main/java/moe/yushi/authlibinjector/ExecutionEnvironment.java @@ -0,0 +1,5 @@ +package moe.yushi.authlibinjector; + +public enum ExecutionEnvironment { + CLIENT, SERVER; +}