mirror of
https://github.com/unmojang/authlib-injector.git
synced 2025-10-04 00:32:40 -04:00
将到skins.minecraft.net的请求导向本地, fix #7
This commit is contained in:
parent
42aefc9f4a
commit
77105062de
@ -3,14 +3,17 @@ package org.to2mbn.authlibinjector;
|
|||||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
import static java.util.Optional.empty;
|
import static java.util.Optional.empty;
|
||||||
import static java.util.Optional.of;
|
import static java.util.Optional.of;
|
||||||
import static org.to2mbn.authlibinjector.util.IOUtils.readURL;
|
import static org.to2mbn.authlibinjector.util.IOUtils.asString;
|
||||||
|
import static org.to2mbn.authlibinjector.util.IOUtils.getURL;
|
||||||
import static org.to2mbn.authlibinjector.util.IOUtils.removeNewLines;
|
import static org.to2mbn.authlibinjector.util.IOUtils.removeNewLines;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.instrument.ClassFileTransformer;
|
import java.lang.instrument.ClassFileTransformer;
|
||||||
import java.text.MessageFormat;
|
import java.text.MessageFormat;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
import org.to2mbn.authlibinjector.httpd.DeprecatedApiHandle;
|
||||||
import org.to2mbn.authlibinjector.transform.ClassTransformer;
|
import org.to2mbn.authlibinjector.transform.ClassTransformer;
|
||||||
import org.to2mbn.authlibinjector.transform.SkinWhitelistTransformUnit;
|
import org.to2mbn.authlibinjector.transform.SkinWhitelistTransformUnit;
|
||||||
import org.to2mbn.authlibinjector.transform.YggdrasilApiTransformUnit;
|
import org.to2mbn.authlibinjector.transform.YggdrasilApiTransformUnit;
|
||||||
@ -26,7 +29,7 @@ public final class AuthlibInjector {
|
|||||||
|
|
||||||
private AuthlibInjector() {}
|
private AuthlibInjector() {}
|
||||||
|
|
||||||
private static boolean booted = false;
|
private static AtomicBoolean booted = new AtomicBoolean(false);
|
||||||
private static boolean debug = "true".equals(System.getProperty("org.to2mbn.authlibinjector.debug"));
|
private static boolean debug = "true".equals(System.getProperty("org.to2mbn.authlibinjector.debug"));
|
||||||
|
|
||||||
public static void info(String message, Object... args) {
|
public static void info(String message, Object... args) {
|
||||||
@ -40,19 +43,17 @@ public final class AuthlibInjector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void bootstrap(Consumer<ClassFileTransformer> transformerRegistry) {
|
public static void bootstrap(Consumer<ClassFileTransformer> transformerRegistry) {
|
||||||
if (booted) {
|
if (!booted.compareAndSet(false, true)) {
|
||||||
info("already booted, skipping");
|
info("already booted, skipping");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
booted = true;
|
|
||||||
|
|
||||||
Optional<YggdrasilConfiguration> optionalConfig = configure();
|
Optional<YggdrasilConfiguration> optionalConfig = configure();
|
||||||
if (!optionalConfig.isPresent()) {
|
if (optionalConfig.isPresent()) {
|
||||||
|
transformerRegistry.accept(createTransformer(optionalConfig.get()));
|
||||||
|
} else {
|
||||||
info("no config available");
|
info("no config available");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
transformerRegistry.accept(createTransformer(optionalConfig.get()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Optional<YggdrasilConfiguration> configure() {
|
private static Optional<YggdrasilConfiguration> configure() {
|
||||||
@ -66,7 +67,7 @@ public final class AuthlibInjector {
|
|||||||
if (prefetched == null) {
|
if (prefetched == null) {
|
||||||
info("fetching metadata");
|
info("fetching metadata");
|
||||||
try {
|
try {
|
||||||
metadataResponse = readURL(apiRoot);
|
metadataResponse = asString(getURL(apiRoot));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
info("unable to fetch metadata: {0}", e);
|
info("unable to fetch metadata: {0}", e);
|
||||||
return empty();
|
return empty();
|
||||||
@ -106,8 +107,14 @@ public final class AuthlibInjector {
|
|||||||
for (String ignore : nonTransformablePackages)
|
for (String ignore : nonTransformablePackages)
|
||||||
transformer.ignores.add(ignore);
|
transformer.ignores.add(ignore);
|
||||||
|
|
||||||
|
if (!"true".equals(System.getProperty("org.to2mbn.authlibinjector.httpd.disable"))) {
|
||||||
|
transformer.units.add(DeprecatedApiHandle.createTransformUnit(config));
|
||||||
|
}
|
||||||
|
|
||||||
transformer.units.add(new YggdrasilApiTransformUnit(config.getApiRoot()));
|
transformer.units.add(new YggdrasilApiTransformUnit(config.getApiRoot()));
|
||||||
|
|
||||||
transformer.units.add(new SkinWhitelistTransformUnit(config.getSkinDomains().toArray(new String[0])));
|
transformer.units.add(new SkinWhitelistTransformUnit(config.getSkinDomains().toArray(new String[0])));
|
||||||
|
|
||||||
config.getDecodedPublickey().ifPresent(
|
config.getDecodedPublickey().ifPresent(
|
||||||
key -> transformer.units.add(new YggdrasilKeyTransformUnit(key.getEncoded())));
|
key -> transformer.units.add(new YggdrasilKeyTransformUnit(key.getEncoded())));
|
||||||
|
|
||||||
|
@ -0,0 +1,54 @@
|
|||||||
|
package org.to2mbn.authlibinjector.httpd;
|
||||||
|
|
||||||
|
import static org.to2mbn.authlibinjector.AuthlibInjector.info;
|
||||||
|
import java.io.IOException;
|
||||||
|
import org.to2mbn.authlibinjector.YggdrasilConfiguration;
|
||||||
|
import org.to2mbn.authlibinjector.transform.DeprecatedApiTransformUnit;
|
||||||
|
import org.to2mbn.authlibinjector.transform.TransformUnit;
|
||||||
|
|
||||||
|
public class DeprecatedApiHandle {
|
||||||
|
|
||||||
|
public static TransformUnit createTransformUnit(YggdrasilConfiguration configuration) {
|
||||||
|
DeprecatedApiHandle handle = new DeprecatedApiHandle(configuration);
|
||||||
|
return new DeprecatedApiTransformUnit(() -> {
|
||||||
|
handle.ensureStarted();
|
||||||
|
return "http://127.0.0.1:" + handle.getLocalApiPort();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean started = false;
|
||||||
|
private YggdrasilConfiguration configuration;
|
||||||
|
private DeprecatedApiHttpd httpd;
|
||||||
|
|
||||||
|
private final Object _lock = new Object();
|
||||||
|
|
||||||
|
public DeprecatedApiHandle(YggdrasilConfiguration configuration) {
|
||||||
|
this.configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ensureStarted() {
|
||||||
|
if (started)
|
||||||
|
return;
|
||||||
|
synchronized (_lock) {
|
||||||
|
if (started)
|
||||||
|
return;
|
||||||
|
if (configuration == null)
|
||||||
|
throw new IllegalStateException("configuration hasn't been set yet");
|
||||||
|
httpd = new DeprecatedApiHttpd(0, configuration);
|
||||||
|
try {
|
||||||
|
httpd.start();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalStateException("httpd failed to start");
|
||||||
|
}
|
||||||
|
info("httpd is running on port {0,number,#}", getLocalApiPort());
|
||||||
|
started = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getLocalApiPort() {
|
||||||
|
if (httpd == null)
|
||||||
|
return -1;
|
||||||
|
return httpd.getListeningPort();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,134 @@
|
|||||||
|
package org.to2mbn.authlibinjector.httpd;
|
||||||
|
|
||||||
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
import static java.util.Optional.empty;
|
||||||
|
import static java.util.Optional.of;
|
||||||
|
import static org.to2mbn.authlibinjector.AuthlibInjector.debug;
|
||||||
|
import static org.to2mbn.authlibinjector.AuthlibInjector.info;
|
||||||
|
import static org.to2mbn.authlibinjector.util.IOUtils.asString;
|
||||||
|
import static org.to2mbn.authlibinjector.util.IOUtils.getURL;
|
||||||
|
import static org.to2mbn.authlibinjector.util.IOUtils.postURL;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import org.to2mbn.authlibinjector.YggdrasilConfiguration;
|
||||||
|
import org.to2mbn.authlibinjector.internal.org.json.JSONArray;
|
||||||
|
import org.to2mbn.authlibinjector.internal.org.json.JSONException;
|
||||||
|
import org.to2mbn.authlibinjector.internal.org.json.JSONObject;
|
||||||
|
import fi.iki.elonen.NanoHTTPD;
|
||||||
|
import fi.iki.elonen.NanoHTTPD.Response.Status;
|
||||||
|
|
||||||
|
public class DeprecatedApiHttpd extends NanoHTTPD {
|
||||||
|
|
||||||
|
public static final String CONTENT_TYPE_JSON = "application/json; charset=utf-8";
|
||||||
|
|
||||||
|
// ^/MinecraftSkins/([^/]+)\.png$
|
||||||
|
private static final Pattern URL_SKINS = Pattern.compile("^/MinecraftSkins/(?<username>[^/]+)\\.png$");
|
||||||
|
|
||||||
|
private YggdrasilConfiguration configuration;
|
||||||
|
|
||||||
|
public DeprecatedApiHttpd(int port, YggdrasilConfiguration configuration) {
|
||||||
|
super("127.0.0.1", port);
|
||||||
|
this.configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response serve(IHTTPSession session) {
|
||||||
|
return processAsSkin(session)
|
||||||
|
.orElseGet(() -> super.serve(session));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<Response> processAsSkin(IHTTPSession session) {
|
||||||
|
Matcher matcher = URL_SKINS.matcher(session.getUri());
|
||||||
|
if (!matcher.find()) return empty();
|
||||||
|
String username = matcher.group("username");
|
||||||
|
|
||||||
|
Optional<String> skinUrl;
|
||||||
|
try {
|
||||||
|
skinUrl = queryCharacterUUID(username)
|
||||||
|
.flatMap(uuid -> queryCharacterProperty(uuid, "textures"))
|
||||||
|
.map(encoded -> asString(Base64.getDecoder().decode(encoded)))
|
||||||
|
.flatMap(texturesPayload -> obtainTextureUrl(texturesPayload, "SKIN"));
|
||||||
|
} catch (UncheckedIOException | JSONException e) {
|
||||||
|
info("[httpd] unable to fetch skin for {0}: {1}", username, e);
|
||||||
|
return of(newFixedLengthResponse(Status.INTERNAL_ERROR, null, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skinUrl.isPresent()) {
|
||||||
|
String url = skinUrl.get();
|
||||||
|
debug("[httpd] retrieving skin for {0} from {1}", username, url);
|
||||||
|
byte[] data;
|
||||||
|
try {
|
||||||
|
data = getURL(url);
|
||||||
|
} catch (IOException e) {
|
||||||
|
info("[httpd] unable to retrieve skin from {0}: {1}", url, e);
|
||||||
|
return of(newFixedLengthResponse(Status.NOT_FOUND, null, null));
|
||||||
|
}
|
||||||
|
info("[httpd] retrieved skin for {0} from {1}, {2} bytes", username, url, data.length);
|
||||||
|
return of(newFixedLengthResponse(Status.OK, "image/png", new ByteArrayInputStream(data), data.length));
|
||||||
|
|
||||||
|
} else {
|
||||||
|
info("[httpd] no skin found for {0}", username);
|
||||||
|
return of(newFixedLengthResponse(Status.NOT_FOUND, null, null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<String> queryCharacterUUID(String username) throws UncheckedIOException, JSONException {
|
||||||
|
String responseText;
|
||||||
|
try {
|
||||||
|
responseText = asString(postURL(
|
||||||
|
configuration.getApiRoot() + "api/profiles/minecraft",
|
||||||
|
CONTENT_TYPE_JSON,
|
||||||
|
new JSONArray(new String[] { username })
|
||||||
|
.toString().getBytes(UTF_8)));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UncheckedIOException(e);
|
||||||
|
}
|
||||||
|
debug("[httpd] query uuid of username {0}, response: {1}", username, responseText);
|
||||||
|
JSONArray response = new JSONArray(responseText);
|
||||||
|
if (response.length() == 0) {
|
||||||
|
return empty();
|
||||||
|
} else if (response.length() == 1) {
|
||||||
|
return of(response.getJSONObject(0).getString("id"));
|
||||||
|
} else {
|
||||||
|
throw new JSONException("Unexpected response length");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<String> queryCharacterProperty(String uuid, String property) throws UncheckedIOException, JSONException {
|
||||||
|
String responseText;
|
||||||
|
try {
|
||||||
|
responseText = asString(getURL(
|
||||||
|
configuration.getApiRoot() + "sessionserver/session/minecraft/profile/" + uuid));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UncheckedIOException(e);
|
||||||
|
}
|
||||||
|
if (responseText.isEmpty()) {
|
||||||
|
debug("[httpd] query profile of {0}, not found", uuid);
|
||||||
|
return empty();
|
||||||
|
}
|
||||||
|
debug("[httpd] query profile of {0}, response: {1}", uuid, responseText);
|
||||||
|
JSONObject response = new JSONObject(responseText);
|
||||||
|
for (Object element_ : response.getJSONArray("properties")) {
|
||||||
|
JSONObject element = (JSONObject) element_;
|
||||||
|
if (property.equals(element.getString("name"))) {
|
||||||
|
return of(element.getString("value"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<String> obtainTextureUrl(String texturesPayload, String textureType) throws JSONException {
|
||||||
|
JSONObject textures = new JSONObject(texturesPayload).getJSONObject("textures");
|
||||||
|
if (textures.has(textureType)) {
|
||||||
|
return of(textures.getJSONObject(textureType).getString("url"));
|
||||||
|
} else {
|
||||||
|
return empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
package org.to2mbn.authlibinjector.transform;
|
||||||
|
|
||||||
|
import static java.util.Optional.empty;
|
||||||
|
import static java.util.Optional.of;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public class DeprecatedApiTransformUnit extends LdcTransformUnit {
|
||||||
|
|
||||||
|
// ^https?:\/\/(skins\.minecraft\.net)(?<path>\/.*)$
|
||||||
|
// => <localApiRoot>${path}
|
||||||
|
public static final Pattern REGEX = Pattern.compile("^https?:\\/\\/(skins\\.minecraft\\.net)(?<path>\\/.*)$");
|
||||||
|
|
||||||
|
public DeprecatedApiTransformUnit(Supplier<String> localApiRoot) {
|
||||||
|
super(string -> {
|
||||||
|
Matcher matcher = REGEX.matcher(string);
|
||||||
|
if (matcher.find()) {
|
||||||
|
return of(matcher.replaceAll(localApiRoot.get() + "${path}"));
|
||||||
|
} else {
|
||||||
|
return empty();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "deprecated-api-transform";
|
||||||
|
}
|
||||||
|
}
|
@ -1,30 +1,52 @@
|
|||||||
package org.to2mbn.authlibinjector.util;
|
package org.to2mbn.authlibinjector.util;
|
||||||
|
|
||||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
import java.io.CharArrayWriter;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.OutputStream;
|
||||||
import java.io.Reader;
|
import java.net.HttpURLConnection;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
|
||||||
public final class IOUtils {
|
public final class IOUtils {
|
||||||
|
|
||||||
public static String readURL(String url) throws IOException {
|
public static byte[] getURL(String url) throws IOException {
|
||||||
try (InputStream in = new URL(url).openStream()) {
|
try (InputStream in = new URL(url).openStream()) {
|
||||||
return asString(in);
|
return asBytes(in);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String asString(InputStream in) throws IOException {
|
public static byte[] postURL(String url, String contentType, byte[] payload) throws IOException {
|
||||||
CharArrayWriter w = new CharArrayWriter();
|
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
|
||||||
Reader reader = new InputStreamReader(in, UTF_8);
|
conn.setRequestMethod("POST");
|
||||||
char[] buf = new char[4096]; // 8192 bytes
|
conn.setRequestProperty("Content-Type", contentType);
|
||||||
int read;
|
conn.setRequestProperty("Content-Length", String.valueOf(payload.length));
|
||||||
while ((read = reader.read(buf)) != -1) {
|
conn.setDoOutput(true);
|
||||||
w.write(buf, 0, read);
|
try {
|
||||||
|
conn.connect();
|
||||||
|
try (OutputStream out = conn.getOutputStream()) {
|
||||||
|
out.write(payload);
|
||||||
|
}
|
||||||
|
try (InputStream in = conn.getInputStream()) {
|
||||||
|
return asBytes(in);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
conn.disconnect();
|
||||||
}
|
}
|
||||||
return new String(w.toCharArray());
|
}
|
||||||
|
|
||||||
|
public static byte[] asBytes(InputStream in) throws IOException {
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
byte[] buf = new byte[8192];
|
||||||
|
int read;
|
||||||
|
while ((read = in.read(buf)) != -1) {
|
||||||
|
out.write(buf, 0, read);
|
||||||
|
}
|
||||||
|
return out.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String asString(byte[] bytes) {
|
||||||
|
return new String(bytes, UTF_8);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String removeNewLines(String input) {
|
public static String removeNewLines(String input) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user