mirror of
https://github.com/yushijinhun/authlib-injector.git
synced 2024-06-02 16:48:54 +02: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.util.Optional.empty;
|
||||
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 java.io.IOException;
|
||||
import java.lang.instrument.ClassFileTransformer;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.Base64;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Consumer;
|
||||
import org.to2mbn.authlibinjector.httpd.DeprecatedApiHandle;
|
||||
import org.to2mbn.authlibinjector.transform.ClassTransformer;
|
||||
import org.to2mbn.authlibinjector.transform.SkinWhitelistTransformUnit;
|
||||
import org.to2mbn.authlibinjector.transform.YggdrasilApiTransformUnit;
|
||||
|
@ -26,7 +29,7 @@ public final class 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"));
|
||||
|
||||
public static void info(String message, Object... args) {
|
||||
|
@ -40,19 +43,17 @@ public final class AuthlibInjector {
|
|||
}
|
||||
|
||||
public static void bootstrap(Consumer<ClassFileTransformer> transformerRegistry) {
|
||||
if (booted) {
|
||||
if (!booted.compareAndSet(false, true)) {
|
||||
info("already booted, skipping");
|
||||
return;
|
||||
}
|
||||
booted = true;
|
||||
|
||||
Optional<YggdrasilConfiguration> optionalConfig = configure();
|
||||
if (!optionalConfig.isPresent()) {
|
||||
if (optionalConfig.isPresent()) {
|
||||
transformerRegistry.accept(createTransformer(optionalConfig.get()));
|
||||
} else {
|
||||
info("no config available");
|
||||
return;
|
||||
}
|
||||
|
||||
transformerRegistry.accept(createTransformer(optionalConfig.get()));
|
||||
}
|
||||
|
||||
private static Optional<YggdrasilConfiguration> configure() {
|
||||
|
@ -66,7 +67,7 @@ public final class AuthlibInjector {
|
|||
if (prefetched == null) {
|
||||
info("fetching metadata");
|
||||
try {
|
||||
metadataResponse = readURL(apiRoot);
|
||||
metadataResponse = asString(getURL(apiRoot));
|
||||
} catch (IOException e) {
|
||||
info("unable to fetch metadata: {0}", e);
|
||||
return empty();
|
||||
|
@ -106,8 +107,14 @@ public final class AuthlibInjector {
|
|||
for (String ignore : nonTransformablePackages)
|
||||
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 SkinWhitelistTransformUnit(config.getSkinDomains().toArray(new String[0])));
|
||||
|
||||
config.getDecodedPublickey().ifPresent(
|
||||
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;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import java.io.CharArrayWriter;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
|
||||
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()) {
|
||||
return asString(in);
|
||||
return asBytes(in);
|
||||
}
|
||||
}
|
||||
|
||||
public static String asString(InputStream in) throws IOException {
|
||||
CharArrayWriter w = new CharArrayWriter();
|
||||
Reader reader = new InputStreamReader(in, UTF_8);
|
||||
char[] buf = new char[4096]; // 8192 bytes
|
||||
int read;
|
||||
while ((read = reader.read(buf)) != -1) {
|
||||
w.write(buf, 0, read);
|
||||
public static byte[] postURL(String url, String contentType, byte[] payload) throws IOException {
|
||||
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setRequestProperty("Content-Type", contentType);
|
||||
conn.setRequestProperty("Content-Length", String.valueOf(payload.length));
|
||||
conn.setDoOutput(true);
|
||||
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) {
|
||||
|
|
Loading…
Reference in a new issue