support prefetched metadata & refactor

This commit is contained in:
yushijinhun 2018-02-13 12:36:30 +08:00
parent cd5b42140c
commit a671ee8339
No known key found for this signature in database
GPG key ID: 5BC167F73EA558E4
8 changed files with 86 additions and 238 deletions

View file

@ -2,12 +2,16 @@ package org.to2mbn.authlibinjector;
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 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.Optional; import java.util.Optional;
import java.util.function.Consumer; import java.util.function.Consumer;
import org.to2mbn.authlibinjector.transform.ClassTransformer; import org.to2mbn.authlibinjector.transform.ClassTransformer;
import org.to2mbn.authlibinjector.transform.SkinWhitelistTransformUnit;
import org.to2mbn.authlibinjector.transform.YggdrasilApiTransformUnit;
import org.to2mbn.authlibinjector.transform.YggdrasilKeyTransformUnit;
public final class AuthlibInjector { public final class AuthlibInjector {
@ -19,60 +23,82 @@ public final class AuthlibInjector {
private AuthlibInjector() {} private AuthlibInjector() {}
private static boolean booted = false; private static boolean booted = false;
private static boolean debug = "true".equals(System.getProperty("org.to2mbn.authlibinjector.debug"));
public static void log(String message, Object... args) { public static void info(String message, Object... args) {
System.err.println("[authlib-injector] " + MessageFormat.format(message, args)); System.err.println("[authlib-injector] " + MessageFormat.format(message, args));
} }
public static void debug(String message, Object... args) {
if (debug) {
info(message, args);
}
}
public static void bootstrap(Consumer<ClassFileTransformer> transformerRegistry) { public static void bootstrap(Consumer<ClassFileTransformer> transformerRegistry) {
if (booted) { if (booted) {
log("already booted, skipping"); info("already booted, skipping");
return; return;
} }
booted = true; booted = true;
Optional<InjectorConfig> optionalConfig = configure(); Optional<YggdrasilConfiguration> optionalConfig = configure();
if (!optionalConfig.isPresent()) { if (!optionalConfig.isPresent()) {
log("no config is found, exiting"); info("no config available");
return; return;
} }
InjectorConfig config = optionalConfig.get(); transformerRegistry.accept(createTransformer(optionalConfig.get()));
}
private static Optional<YggdrasilConfiguration> configure() {
String apiRoot = System.getProperty("org.to2mbn.authlibinjector.config");
if (apiRoot == null) return empty();
info("api root: {0}", apiRoot);
String metadataResponse = System.getProperty("org.to2mbn.authlibinjector.config.prefetched");
if (metadataResponse == null) {
info("fetching metadata");
try {
metadataResponse = readURL(apiRoot);
} catch (IOException e) {
info("unable to fetch metadata: {0}", e);
return empty();
}
} else {
info("prefetched metadata detected");
}
debug("metadata: {0}", metadataResponse);
YggdrasilConfiguration configuration;
try {
configuration = YggdrasilConfiguration.parse(apiRoot, metadataResponse);
} catch (IOException e) {
info("unable to parse metadata: {0}\n"
+ "metadata to parse:\n"
+ "{1}",
e, metadataResponse);
return empty();
}
debug("parsed metadata: {0}", configuration);
return of(configuration);
}
private static ClassTransformer createTransformer(YggdrasilConfiguration config) {
ClassTransformer transformer = new ClassTransformer(); ClassTransformer transformer = new ClassTransformer();
transformer.debugSaveClass = debug;
if (config.isDebug()) transformer.debug = true;
for (String ignore : nonTransformablePackages) for (String ignore : nonTransformablePackages)
transformer.ignores.add(ignore); transformer.ignores.add(ignore);
config.applyTransformers(transformer.units); transformer.units.add(new YggdrasilApiTransformUnit(config.getApiRoot()));
transformerRegistry.accept(transformer); transformer.units.add(new SkinWhitelistTransformUnit(config.getSkinDomains().toArray(new String[0])));
} config.getDecodedPublickey().ifPresent(
key -> transformer.units.add(new YggdrasilKeyTransformUnit(key.getEncoded())));
private static Optional<InjectorConfig> configure() { return transformer;
String url = System.getProperty("org.to2mbn.authlibinjector.config");
if (url == null) {
return empty();
}
log("trying to config remotely: {0}", url);
InjectorConfig config = new InjectorConfig();
config.setDebug("true".equals(System.getProperty("org.to2mbn.authlibinjector.debug")));
RemoteConfiguration remoteConfig;
try {
remoteConfig = RemoteConfiguration.fetch(url);
} catch (IOException e) {
log("unable to configure remotely: {0}", e);
return empty();
}
if (config.isDebug()) {
log("fetched remote config: {0}", remoteConfig);
}
remoteConfig.applyToInjectorConfig(config);
return of(config);
} }
} }

View file

@ -1,56 +0,0 @@
package org.to2mbn.authlibinjector;
import static org.to2mbn.authlibinjector.util.KeyUtils.decodePublicKey;
import java.util.List;
import org.to2mbn.authlibinjector.transform.SkinWhitelistTransformUnit;
import org.to2mbn.authlibinjector.transform.TransformUnit;
import org.to2mbn.authlibinjector.transform.YggdrasilApiTransformUnit;
import org.to2mbn.authlibinjector.transform.YggdrasilKeyTransformUnit;
public class InjectorConfig {
private String apiRoot;
private List<String> skinWhitelistDomains;
private String publicKey;
private boolean debug;
public String getApiRoot() {
return apiRoot;
}
public void setApiRoot(String apiRoot) {
this.apiRoot = apiRoot;
}
public List<String> getSkinWhitelistDomains() {
return skinWhitelistDomains;
}
public void setSkinWhitelistDomains(List<String> skinWhitelistDomains) {
this.skinWhitelistDomains = skinWhitelistDomains;
}
public String getPublicKey() {
return publicKey;
}
public void setPublicKey(String publicKey) {
this.publicKey = publicKey;
}
public boolean isDebug() {
return debug;
}
public void setDebug(boolean debug) {
this.debug = debug;
}
public void applyTransformers(List<TransformUnit> units) {
units.add(new YggdrasilApiTransformUnit(apiRoot));
units.add(new SkinWhitelistTransformUnit(skinWhitelistDomains.toArray(new String[0])));
if (publicKey != null) {
units.add(new YggdrasilKeyTransformUnit(decodePublicKey(publicKey)));
}
}
}

View file

@ -6,8 +6,6 @@ import static java.util.Objects.requireNonNull;
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 java.util.Optional.ofNullable; import static java.util.Optional.ofNullable;
import static org.to2mbn.authlibinjector.util.HttpRequester.http;
import static org.to2mbn.authlibinjector.util.IOUtils.asJson;
import static org.to2mbn.authlibinjector.util.KeyUtils.decodePublicKey; import static org.to2mbn.authlibinjector.util.KeyUtils.decodePublicKey;
import static org.to2mbn.authlibinjector.util.KeyUtils.loadX509PublicKey; import static org.to2mbn.authlibinjector.util.KeyUtils.loadX509PublicKey;
import java.io.IOException; import java.io.IOException;
@ -21,13 +19,13 @@ import java.util.TreeMap;
import org.to2mbn.authlibinjector.internal.org.json.JSONException; import org.to2mbn.authlibinjector.internal.org.json.JSONException;
import org.to2mbn.authlibinjector.internal.org.json.JSONObject; import org.to2mbn.authlibinjector.internal.org.json.JSONObject;
public class RemoteConfiguration { public class YggdrasilConfiguration {
public static RemoteConfiguration fetch(String apiRoot) throws IOException { public static YggdrasilConfiguration parse(String apiRoot, String metadataResponse) throws IOException {
if (!apiRoot.endsWith("/")) apiRoot += "/"; if (!apiRoot.endsWith("/")) apiRoot += "/";
try { try {
JSONObject response = asJson(http.request("GET", apiRoot)); JSONObject response = new JSONObject(metadataResponse);
List<String> skinDomains = new ArrayList<>(); List<String> skinDomains = new ArrayList<>();
ofNullable(response.optJSONArray("skinDomains")) ofNullable(response.optJSONArray("skinDomains"))
@ -54,7 +52,7 @@ public class RemoteConfiguration {
.map(JSONObject::toMap) .map(JSONObject::toMap)
.ifPresent(it -> it.forEach((k, v) -> meta.put(k, String.valueOf(v)))); .ifPresent(it -> it.forEach((k, v) -> meta.put(k, String.valueOf(v))));
return new RemoteConfiguration(apiRoot, unmodifiableList(skinDomains), signaturePublickey, unmodifiableMap(meta), decodedPublickey); return new YggdrasilConfiguration(apiRoot, unmodifiableList(skinDomains), signaturePublickey, unmodifiableMap(meta), decodedPublickey);
} catch (JSONException e) { } catch (JSONException e) {
throw new IOException("Invalid json", e); throw new IOException("Invalid json", e);
} }
@ -66,7 +64,7 @@ public class RemoteConfiguration {
private Optional<PublicKey> decodedPublickey; private Optional<PublicKey> decodedPublickey;
private Map<String, String> meta; private Map<String, String> meta;
public RemoteConfiguration(String apiRoot, List<String> skinDomains, Optional<String> signaturePublickey, Map<String, String> meta, Optional<PublicKey> decodedPublickey) { public YggdrasilConfiguration(String apiRoot, List<String> skinDomains, Optional<String> signaturePublickey, Map<String, String> meta, Optional<PublicKey> decodedPublickey) {
this.apiRoot = requireNonNull(apiRoot); this.apiRoot = requireNonNull(apiRoot);
this.skinDomains = requireNonNull(skinDomains); this.skinDomains = requireNonNull(skinDomains);
this.signaturePublickey = requireNonNull(signaturePublickey); this.signaturePublickey = requireNonNull(signaturePublickey);
@ -94,14 +92,8 @@ public class RemoteConfiguration {
return decodedPublickey; return decodedPublickey;
} }
public void applyToInjectorConfig(InjectorConfig config) {
config.setApiRoot(apiRoot);
config.setPublicKey(signaturePublickey.orElse(null));
config.setSkinWhitelistDomains(skinDomains);
}
@Override @Override
public String toString() { public String toString() {
return "RemoteConfiguration [apiRoot=" + apiRoot + ", skinDomains=" + skinDomains + ", signaturePublickey=" + signaturePublickey + ", meta=" + meta + "]"; return "YggdrasilConfiguration [apiRoot=" + apiRoot + ", skinDomains=" + skinDomains + ", signaturePublickey=" + signaturePublickey + ", meta=" + meta + "]";
} }
} }

View file

@ -1,14 +1,14 @@
package org.to2mbn.authlibinjector.javaagent; package org.to2mbn.authlibinjector.javaagent;
import static org.to2mbn.authlibinjector.AuthlibInjector.bootstrap; import static org.to2mbn.authlibinjector.AuthlibInjector.bootstrap;
import static org.to2mbn.authlibinjector.AuthlibInjector.log; import static org.to2mbn.authlibinjector.AuthlibInjector.info;
import java.lang.instrument.Instrumentation; import java.lang.instrument.Instrumentation;
public class AuthlibInjectorPremain { public class AuthlibInjectorPremain {
public static void premain(String arg, Instrumentation instrumentation) { public static void premain(String arg, Instrumentation instrumentation) {
try { try {
log("launched from javaagent"); info("launched from javaagent");
if (arg != null && !arg.isEmpty()) { if (arg != null && !arg.isEmpty()) {
System.setProperty("org.to2mbn.authlibinjector.config", arg); System.setProperty("org.to2mbn.authlibinjector.config", arg);
} }

View file

@ -1,6 +1,7 @@
package org.to2mbn.authlibinjector.transform; package org.to2mbn.authlibinjector.transform;
import static org.to2mbn.authlibinjector.AuthlibInjector.log; import static org.to2mbn.authlibinjector.AuthlibInjector.debug;
import static org.to2mbn.authlibinjector.AuthlibInjector.info;
import java.io.IOException; import java.io.IOException;
import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException; import java.lang.instrument.IllegalClassFormatException;
@ -21,7 +22,7 @@ public class ClassTransformer implements ClassFileTransformer {
public List<TransformUnit> units = new ArrayList<>(); public List<TransformUnit> units = new ArrayList<>();
public Set<String> ignores = new HashSet<>(); public Set<String> ignores = new HashSet<>();
public boolean debug; public boolean debugSaveClass;
private static class TransformHandle { private static class TransformHandle {
@ -51,7 +52,7 @@ public class ClassTransformer implements ClassFileTransformer {
ClassReader reader = new ClassReader(classBuffer); ClassReader reader = new ClassReader(classBuffer);
reader.accept(optionalVisitor.get(), 0); reader.accept(optionalVisitor.get(), 0);
if (currentModified) { if (currentModified) {
log("transform {0} using {1}", className, unit); info("transform {0} using {1}", className, unit);
modified = true; modified = true;
classBuffer = writer.toByteArray(); classBuffer = writer.toByteArray();
} }
@ -82,18 +83,16 @@ public class ClassTransformer implements ClassFileTransformer {
units.forEach(handle::accept); units.forEach(handle::accept);
if (handle.getResult().isPresent()) { if (handle.getResult().isPresent()) {
byte[] classBuffer = handle.getResult().get(); byte[] classBuffer = handle.getResult().get();
if (debug) { if (debugSaveClass) {
saveClassFile(className, classBuffer); saveClassFile(className, classBuffer);
} }
return classBuffer; return classBuffer;
} else { } else {
if (debug) { debug("no transform performed on {0}", className);
log("no transform performed on {0}", className);
}
return null; return null;
} }
} catch (Throwable e) { } catch (Throwable e) {
log("unable to transform {0}: {1}", internalClassName, e); info("unable to transform {0}: {1}", internalClassName, e);
e.printStackTrace(); e.printStackTrace();
} }
} }
@ -104,7 +103,7 @@ public class ClassTransformer implements ClassFileTransformer {
try { try {
Files.write(Paths.get(className + "_dump.class"), classBuffer, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); Files.write(Paths.get(className + "_dump.class"), classBuffer, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
} catch (IOException e) { } catch (IOException e) {
log("unable to dump class {0}: {1}", className, e); info("unable to dump class {0}: {1}", className, e);
e.printStackTrace(); e.printStackTrace();
} }
} }

View file

@ -1,7 +1,7 @@
package org.to2mbn.authlibinjector.transform; package org.to2mbn.authlibinjector.transform;
import static org.objectweb.asm.Opcodes.ASM6; import static org.objectweb.asm.Opcodes.ASM6;
import static org.to2mbn.authlibinjector.AuthlibInjector.log; import static org.to2mbn.authlibinjector.AuthlibInjector.info;
import java.util.Optional; import java.util.Optional;
import java.util.function.Function; import java.util.function.Function;
import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.ClassVisitor;
@ -29,7 +29,7 @@ public class LdcTransformUnit implements TransformUnit {
Optional<String> transformed = ldcMapper.apply((String) cst); Optional<String> transformed = ldcMapper.apply((String) cst);
if (transformed.isPresent() && !transformed.get().equals(cst)) { if (transformed.isPresent() && !transformed.get().equals(cst)) {
modifiedCallback.run(); modifiedCallback.run();
log("transform [{0}] to [{1}]", cst, transformed.get()); info("transform [{0}] to [{1}]", cst, transformed.get());
super.visitLdcInsn(transformed.get()); super.visitLdcInsn(transformed.get());
} else { } else {
super.visitLdcInsn(cst); super.visitLdcInsn(cst);

View file

@ -1,114 +0,0 @@
package org.to2mbn.authlibinjector.util;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.to2mbn.authlibinjector.util.IOUtils.asString;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Map;
public class HttpRequester {
/** Common http requester */
public static final HttpRequester http = new HttpRequester();
private int timeout = 15000;
public int getTimeout() {
return timeout;
}
public void setTimeout(int timeout) {
this.timeout = timeout;
}
public String request(String method, String url) throws IOException {
return request(method, url, null);
}
public String request(String method, String url, Map<String, String> headers) throws IOException {
HttpURLConnection conn = createConnection(url, headers);
conn.setRequestMethod(method);
try {
conn.connect();
try (InputStream in = conn.getInputStream()) {
return asString(in);
}
} catch (IOException e) {
try (InputStream in = conn.getErrorStream()) {
return readErrorStream(in, e);
}
} finally {
conn.disconnect();
}
}
public String requestWithPayload(String method, String url, Object payload, String contentType) throws IOException {
return requestWithPayload(method, url, payload, contentType, null);
}
public String requestWithPayload(String method, String url, Object payload, String contentType, Map<String, String> headers) throws IOException {
byte[] bytePayload;
if (payload instanceof byte[]) {
bytePayload = (byte[]) payload;
} else if (payload == null) {
bytePayload = new byte[0];
} else {
bytePayload = String.valueOf(payload).getBytes(UTF_8);
}
HttpURLConnection conn = createConnection(url, headers);
conn.setRequestMethod(method);
conn.setRequestProperty("Content-Type", contentType);
conn.setRequestProperty("Content-Length", String.valueOf(bytePayload.length));
conn.setDoOutput(true);
try {
conn.connect();
try (OutputStream out = conn.getOutputStream()) {
out.write(bytePayload);
}
try (InputStream in = conn.getInputStream()) {
return asString(in);
}
} catch (IOException e) {
try (InputStream in = conn.getErrorStream()) {
return readErrorStream(in, e);
}
} finally {
conn.disconnect();
}
}
private String readErrorStream(InputStream in, IOException e) throws IOException {
if (in == null)
throw e;
try {
return asString(in);
} catch (IOException e1) {
if (e != e1)
e1.addSuppressed(e);
throw e1;
}
}
private HttpURLConnection createConnection(String url, Map<String, String> headers) throws IOException {
HttpURLConnection conn = createConnection(new URL(url));
if (headers != null)
headers.forEach((key, value) -> conn.setRequestProperty(key, value));
return conn;
}
private HttpURLConnection createConnection(URL url) throws IOException {
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(timeout);
conn.setReadTimeout(timeout);
conn.setUseCaches(false);
return conn;
}
}

View file

@ -6,11 +6,16 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.io.Reader; import java.io.Reader;
import org.to2mbn.authlibinjector.internal.org.json.JSONException; import java.net.URL;
import org.to2mbn.authlibinjector.internal.org.json.JSONObject;
public final class IOUtils { public final class IOUtils {
public static String readURL(String url) throws IOException {
try (InputStream in = new URL(url).openStream()) {
return asString(in);
}
}
public static String asString(InputStream in) throws IOException { public static String asString(InputStream in) throws IOException {
CharArrayWriter w = new CharArrayWriter(); CharArrayWriter w = new CharArrayWriter();
Reader reader = new InputStreamReader(in, UTF_8); Reader reader = new InputStreamReader(in, UTF_8);
@ -22,10 +27,6 @@ public final class IOUtils {
return new String(w.toCharArray()); return new String(w.toCharArray());
} }
public static JSONObject asJson(String data) throws JSONException {
return new JSONObject(data);
}
private IOUtils() {} private IOUtils() {}
} }