Merge pull request #28 from yushijinhun/citizens-support

支持Citizens2及@mojang后缀
This commit is contained in:
Haowei Wen 2018-12-31 14:26:08 +08:00 committed by GitHub
commit ead8866a40
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 2967 additions and 492 deletions

View file

@ -10,7 +10,6 @@ repositories {
dependencies {
compile 'org.ow2.asm:asm:6.2.1'
compile 'org.nanohttpd:nanohttpd:2.3.1'
testCompile 'junit:junit:4.12'
}
@ -53,13 +52,7 @@ shadowJar {
exclude 'META-INF/maven/**'
exclude 'module-info.class'
// nanohttpd
exclude 'LICENSE.txt'
exclude 'fi/iki/elonen/util/**'
exclude 'META-INF/nanohttpd/mimetypes.properties'
relocate 'org.objectweb.asm', 'moe.yushi.authlibinjector.internal.org.objectweb.asm'
relocate 'fi.iki.elonen', 'moe.yushi.authlibinjector.internal.fi.iki.elonen'
}
defaultTasks 'clean', 'shadowJar'

View file

@ -1,6 +1,7 @@
package moe.yushi.authlibinjector;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Collections.emptyList;
import static java.util.Optional.empty;
import static java.util.Optional.of;
import static moe.yushi.authlibinjector.util.IOUtils.asBytes;
@ -14,19 +15,30 @@ import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import moe.yushi.authlibinjector.httpd.DefaultURLRedirector;
import moe.yushi.authlibinjector.httpd.LegacySkinAPIFilter;
import moe.yushi.authlibinjector.httpd.QueryProfileFilter;
import moe.yushi.authlibinjector.httpd.QueryUUIDsFilter;
import moe.yushi.authlibinjector.httpd.URLFilter;
import moe.yushi.authlibinjector.httpd.URLProcessor;
import moe.yushi.authlibinjector.transform.AuthlibLogInterceptor;
import moe.yushi.authlibinjector.transform.ClassTransformer;
import moe.yushi.authlibinjector.transform.ConstantURLTransformUnit;
import moe.yushi.authlibinjector.transform.DumpClassListener;
import moe.yushi.authlibinjector.transform.SkinWhitelistTransformUnit;
import moe.yushi.authlibinjector.transform.LocalYggdrasilApiTransformUnit;
import moe.yushi.authlibinjector.transform.RemoteYggdrasilTransformUnit;
import moe.yushi.authlibinjector.transform.YggdrasilKeyTransformUnit;
import moe.yushi.authlibinjector.transform.support.CitizensTransformer;
import moe.yushi.authlibinjector.util.Logging;
import moe.yushi.authlibinjector.yggdrasil.CustomYggdrasilAPIProvider;
import moe.yushi.authlibinjector.yggdrasil.MojangYggdrasilAPIProvider;
import moe.yushi.authlibinjector.yggdrasil.YggdrasilClient;
public final class AuthlibInjector {
@ -54,11 +66,6 @@ public final class AuthlibInjector {
*/
public static final String PROP_PREFETCHED_DATA_OLD = "org.to2mbn.authlibinjector.config.prefetched";
/**
* Whether to disable the local httpd server.
*/
public static final String PROP_DISABLE_HTTPD = "authlibinjector.httpd.disable";
/**
* The name of loggers to have debug level turned on.
*/
@ -80,6 +87,8 @@ public final class AuthlibInjector {
*/
public static final String PROP_SIDE = "authlibinjector.side";
public static final String PROP_DISABLE_HTTPD = "authlibinjector.httpd.disable";
public static final String PROP_ALI_REDIRECT_LIMIT = "authlibinjector.ali.redirectLimit";
// ====
@ -267,9 +276,32 @@ public final class AuthlibInjector {
}
}
private static ClassTransformer createTransformer(YggdrasilConfiguration config) {
ClassTransformer transformer = new ClassTransformer();
private static List<URLFilter> createFilters(YggdrasilConfiguration config) {
if (Boolean.getBoolean(PROP_DISABLE_HTTPD)) {
return emptyList();
}
List<URLFilter> filters = new ArrayList<>();
YggdrasilClient customClient = new YggdrasilClient(new CustomYggdrasilAPIProvider(config));
YggdrasilClient mojangClient = new YggdrasilClient(new MojangYggdrasilAPIProvider());
if (Boolean.TRUE.equals(config.getMeta().get("feature.legacy_skin_api"))) {
Logging.CONFIG.info("Disabled local redirect for legacy skin API, as the remote Yggdrasil server supports it");
} else {
filters.add(new LegacySkinAPIFilter(customClient));
}
filters.add(new QueryUUIDsFilter(mojangClient, customClient));
filters.add(new QueryProfileFilter(mojangClient, customClient));
return filters;
}
private static ClassTransformer createTransformer(YggdrasilConfiguration config) {
URLProcessor urlProcessor = new URLProcessor(createFilters(config), new DefaultURLRedirector(config));
ClassTransformer transformer = new ClassTransformer();
for (String ignore : nonTransformablePackages) {
transformer.ignores.add(ignore);
}
@ -282,16 +314,13 @@ public final class AuthlibInjector {
transformer.units.add(new AuthlibLogInterceptor());
}
if (!"true".equals(System.getProperty(PROP_DISABLE_HTTPD))) {
transformer.units.add(new LocalYggdrasilApiTransformUnit(config));
}
transformer.units.add(new RemoteYggdrasilTransformUnit(config.getApiRoot()));
transformer.units.add(new ConstantURLTransformUnit(urlProcessor));
transformer.units.add(new CitizensTransformer());
transformer.units.add(new SkinWhitelistTransformUnit(config.getSkinDomains().toArray(new String[0])));
config.getDecodedPublickey().ifPresent(
key -> transformer.units.add(new YggdrasilKeyTransformUnit(key.getEncoded())));
transformer.units.add(new YggdrasilKeyTransformUnit());
config.getDecodedPublickey().ifPresent(YggdrasilKeyTransformUnit.getPublicKeys()::add);
return transformer;
}

View file

@ -0,0 +1,37 @@
package moe.yushi.authlibinjector.httpd;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import moe.yushi.authlibinjector.YggdrasilConfiguration;
public class DefaultURLRedirector implements URLRedirector {
private Map<String, String> domainMapping = new HashMap<>();
private String apiRoot;
public DefaultURLRedirector(YggdrasilConfiguration config) {
initDomainMapping();
apiRoot = config.getApiRoot();
}
private void initDomainMapping() {
domainMapping.put("api.mojang.com", "api");
domainMapping.put("authserver.mojang.com", "authserver");
domainMapping.put("sessionserver.mojang.com", "sessionserver");
domainMapping.put("skins.minecraft.net", "skins");
}
@Override
public Optional<String> redirect(String domain, String path) {
String subdirectory = domainMapping.get(domain);
if (subdirectory == null) {
return Optional.empty();
}
return Optional.of(apiRoot + subdirectory + path);
}
}

View file

@ -0,0 +1,91 @@
package moe.yushi.authlibinjector.httpd;
import static java.util.Optional.empty;
import static java.util.Optional.of;
import static java.util.Optional.ofNullable;
import static moe.yushi.authlibinjector.util.IOUtils.asString;
import static moe.yushi.authlibinjector.util.IOUtils.getURL;
import static moe.yushi.authlibinjector.util.IOUtils.newUncheckedIOException;
import static moe.yushi.authlibinjector.util.JsonUtils.asJsonObject;
import static moe.yushi.authlibinjector.util.JsonUtils.parseJson;
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 moe.yushi.authlibinjector.internal.fi.iki.elonen.IHTTPSession;
import moe.yushi.authlibinjector.internal.fi.iki.elonen.Response;
import moe.yushi.authlibinjector.internal.fi.iki.elonen.Status;
import moe.yushi.authlibinjector.internal.org.json.simple.JSONObject;
import moe.yushi.authlibinjector.util.JsonUtils;
import moe.yushi.authlibinjector.util.Logging;
import moe.yushi.authlibinjector.yggdrasil.YggdrasilClient;
public class LegacySkinAPIFilter implements URLFilter {
private static final Pattern PATH_SKINS = Pattern.compile("^/MinecraftSkins/(?<username>[^/]+)\\.png$");
private YggdrasilClient upstream;
public LegacySkinAPIFilter(YggdrasilClient upstream) {
this.upstream = upstream;
}
@Override
public boolean canHandle(String domain, String path) {
return domain.equals("skins.minecraft.net");
}
@Override
public Optional<Response> handle(String domain, String path, IHTTPSession session) {
if (!domain.equals("skins.minecraft.net"))
return empty();
Matcher matcher = PATH_SKINS.matcher(path);
if (!matcher.find())
return empty();
String username = matcher.group("username");
Optional<String> skinUrl;
try {
skinUrl = upstream.queryUUID(username)
.flatMap(uuid -> upstream.queryProfile(uuid, false))
.flatMap(profile -> Optional.ofNullable(profile.properties.get("textures")))
.map(property -> asString(Base64.getDecoder().decode(property.value)))
.flatMap(texturesPayload -> obtainTextureUrl(texturesPayload, "SKIN"));
} catch (UncheckedIOException e) {
throw newUncheckedIOException("Failed to fetch skin metadata for " + username, e);
}
if (skinUrl.isPresent()) {
String url = skinUrl.get();
Logging.HTTPD.fine("Retrieving skin for " + username + " from " + url);
byte[] data;
try {
data = getURL(url);
} catch (IOException e) {
throw newUncheckedIOException("Failed to retrieve skin from " + url, e);
}
Logging.HTTPD.info("Retrieved skin for " + username + " from " + url + ", " + data.length + " bytes");
return of(Response.newFixedLength(Status.OK, "image/png", new ByteArrayInputStream(data), data.length));
} else {
Logging.HTTPD.info("No skin is found for " + username);
return of(Response.newFixedLength(Status.NOT_FOUND, null, null));
}
}
private Optional<String> obtainTextureUrl(String texturesPayload, String textureType) throws UncheckedIOException {
JSONObject payload = asJsonObject(parseJson(texturesPayload));
JSONObject textures = asJsonObject(payload.get("textures"));
return ofNullable(textures.get(textureType))
.map(JsonUtils::asJsonObject)
.map(it -> ofNullable(it.get("url"))
.map(JsonUtils::asJsonString)
.orElseThrow(() -> newUncheckedIOException("Invalid JSON: Missing texture url")));
}
}

View file

@ -1,44 +0,0 @@
package moe.yushi.authlibinjector.httpd;
import java.io.IOException;
import moe.yushi.authlibinjector.YggdrasilConfiguration;
import moe.yushi.authlibinjector.util.Logging;
public class LocalYggdrasilHandle {
private boolean started = false;
private YggdrasilConfiguration configuration;
private LocalYggdrasilHttpd httpd;
private final Object _lock = new Object();
public LocalYggdrasilHandle(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 LocalYggdrasilHttpd(0, configuration);
try {
httpd.start();
} catch (IOException e) {
throw new IllegalStateException("Httpd failed to start");
}
Logging.HTTPD.info("Httpd is running on port " + getLocalApiPort());
started = true;
}
}
public int getLocalApiPort() {
if (httpd == null)
return -1;
return httpd.getListeningPort();
}
}

View file

@ -1,142 +0,0 @@
package moe.yushi.authlibinjector.httpd;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Collections.singleton;
import static java.util.Optional.empty;
import static java.util.Optional.of;
import static java.util.Optional.ofNullable;
import static moe.yushi.authlibinjector.util.IOUtils.asString;
import static moe.yushi.authlibinjector.util.IOUtils.getURL;
import static moe.yushi.authlibinjector.util.IOUtils.newUncheckedIOException;
import static moe.yushi.authlibinjector.util.IOUtils.postURL;
import static moe.yushi.authlibinjector.util.JsonUtils.asJsonArray;
import static moe.yushi.authlibinjector.util.JsonUtils.asJsonObject;
import static moe.yushi.authlibinjector.util.JsonUtils.asJsonString;
import static moe.yushi.authlibinjector.util.JsonUtils.parseJson;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Base64;
import java.util.Optional;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import fi.iki.elonen.NanoHTTPD;
import fi.iki.elonen.NanoHTTPD.Response.Status;
import moe.yushi.authlibinjector.YggdrasilConfiguration;
import moe.yushi.authlibinjector.internal.org.json.simple.JSONArray;
import moe.yushi.authlibinjector.internal.org.json.simple.JSONObject;
import moe.yushi.authlibinjector.util.JsonUtils;
import moe.yushi.authlibinjector.util.Logging;
public class LocalYggdrasilHttpd extends NanoHTTPD {
public static final String CONTENT_TYPE_JSON = "application/json; charset=utf-8";
private static final Pattern URL_SKINS = Pattern.compile("^/skins/MinecraftSkins/(?<username>[^/]+)\\.png$");
private YggdrasilConfiguration configuration;
public LocalYggdrasilHttpd(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 e) {
Logging.HTTPD.log(Level.WARNING, "Failed to fetch skin for " + username, e);
return of(newFixedLengthResponse(Status.INTERNAL_ERROR, null, null));
}
if (skinUrl.isPresent()) {
String url = skinUrl.get();
Logging.HTTPD.fine("Retrieving skin for " + username + " from " + url);
byte[] data;
try {
data = getURL(url);
} catch (IOException e) {
Logging.HTTPD.log(Level.WARNING, "Failed to retrieve skin from " + url, e);
return of(newFixedLengthResponse(Status.INTERNAL_ERROR, null, null));
}
Logging.HTTPD.info("Retrieved skin for " + username + " from " + url + ", " + data.length + " bytes");
return of(newFixedLengthResponse(Status.OK, "image/png", new ByteArrayInputStream(data), data.length));
} else {
Logging.HTTPD.info("No skin is found for " + username);
return of(newFixedLengthResponse(Status.NOT_FOUND, null, null));
}
}
private Optional<String> queryCharacterUUID(String username) throws UncheckedIOException {
String responseText;
try {
responseText = asString(postURL(
configuration.getApiRoot() + "api/profiles/minecraft",
CONTENT_TYPE_JSON,
JSONArray.toJSONString(singleton(username)).getBytes(UTF_8)));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
Logging.HTTPD.fine("Query UUID of username " + username + ", response: " + responseText);
JSONArray response = asJsonArray(parseJson(responseText));
if (response.size() == 0) {
return empty();
} else if (response.size() == 1) {
JSONObject profile = asJsonObject(response.get(0));
return of(asJsonString(profile.get("id")));
} else {
throw newUncheckedIOException("Invalid JSON: Unexpected response length");
}
}
private Optional<String> queryCharacterProperty(String uuid, String propertyName) throws UncheckedIOException {
String responseText;
try {
responseText = asString(getURL(
configuration.getApiRoot() + "sessionserver/session/minecraft/profile/" + uuid));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
if (responseText.isEmpty()) {
Logging.HTTPD.fine("Query profile of " + uuid + ", not found");
return empty();
}
Logging.HTTPD.fine("Query profile of " + uuid + ", response: " + responseText);
JSONObject response = asJsonObject(parseJson(responseText));
return asJsonArray(response.get("properties")).stream()
.map(JsonUtils::asJsonObject)
.filter(property -> asJsonString(property.get("name")).equals(propertyName))
.findFirst()
.map(property -> asJsonString(property.get("value")));
}
private Optional<String> obtainTextureUrl(String texturesPayload, String textureType) throws UncheckedIOException {
JSONObject payload = asJsonObject(parseJson(texturesPayload));
JSONObject textures = asJsonObject(payload.get("textures"));
return ofNullable(textures.get(textureType))
.map(JsonUtils::asJsonObject)
.map(it -> ofNullable(it.get("url"))
.map(JsonUtils::asJsonString)
.orElseThrow(() -> newUncheckedIOException("Invalid JSON: Missing texture url")));
}
}

View file

@ -0,0 +1,76 @@
package moe.yushi.authlibinjector.httpd;
import static java.util.Optional.empty;
import static moe.yushi.authlibinjector.util.UUIDUtils.fromUnsignedUUID;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import moe.yushi.authlibinjector.internal.fi.iki.elonen.IHTTPSession;
import moe.yushi.authlibinjector.internal.fi.iki.elonen.Response;
import moe.yushi.authlibinjector.internal.fi.iki.elonen.Status;
import moe.yushi.authlibinjector.yggdrasil.GameProfile;
import moe.yushi.authlibinjector.yggdrasil.YggdrasilClient;
import moe.yushi.authlibinjector.yggdrasil.YggdrasilResponseBuilder;
public class QueryProfileFilter implements URLFilter {
private static final Pattern PATH_REGEX = Pattern.compile("^/session/minecraft/profile/(?<uuid>[0-9a-f]{32})$");
private YggdrasilClient mojangClient;
private YggdrasilClient customClient;
public QueryProfileFilter(YggdrasilClient mojangClient, YggdrasilClient customClient) {
this.mojangClient = mojangClient;
this.customClient = customClient;
}
@Override
public boolean canHandle(String domain, String path) {
return domain.equals("sessionserver.mojang.com") && path.startsWith("/session/minecraft/profile/");
}
@Override
public Optional<Response> handle(String domain, String path, IHTTPSession session) throws IOException {
if (!domain.equals("sessionserver.mojang.com"))
return empty();
Matcher matcher = PATH_REGEX.matcher(path);
if (!matcher.find())
return empty();
UUID uuid;
try {
uuid = fromUnsignedUUID(matcher.group("uuid"));
} catch (IllegalArgumentException e) {
return Optional.of(Response.newFixedLength(Status.NO_CONTENT, null, null));
}
boolean withSignature = false;
List<String> unsignedValues = session.getParameters().get("unsigned");
if (unsignedValues != null && unsignedValues.get(0).equals("false")) {
withSignature = true;
}
Optional<GameProfile> response;
if (QueryUUIDsFilter.isMaskedUUID(uuid)) {
response = mojangClient.queryProfile(QueryUUIDsFilter.unmaskUUID(uuid), withSignature);
response.ifPresent(profile -> {
profile.id = uuid;
profile.name += QueryUUIDsFilter.NAME_SUFFIX;
});
} else {
response = customClient.queryProfile(uuid, withSignature);
}
if (response.isPresent()) {
return Optional.of(Response.newFixedLength(Status.OK, null, YggdrasilResponseBuilder.queryProfile(response.get(), withSignature)));
} else {
return Optional.of(Response.newFixedLength(Status.NO_CONTENT, null, null));
}
}
}

View file

@ -0,0 +1,95 @@
package moe.yushi.authlibinjector.httpd;
import static moe.yushi.authlibinjector.util.IOUtils.CONTENT_TYPE_JSON;
import static moe.yushi.authlibinjector.util.IOUtils.asBytes;
import static moe.yushi.authlibinjector.util.IOUtils.asString;
import static moe.yushi.authlibinjector.util.JsonUtils.asJsonArray;
import static moe.yushi.authlibinjector.util.JsonUtils.asJsonString;
import static moe.yushi.authlibinjector.util.JsonUtils.parseJson;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import moe.yushi.authlibinjector.internal.fi.iki.elonen.IHTTPSession;
import moe.yushi.authlibinjector.internal.fi.iki.elonen.Response;
import moe.yushi.authlibinjector.internal.fi.iki.elonen.Status;
import moe.yushi.authlibinjector.util.Logging;
import moe.yushi.authlibinjector.yggdrasil.YggdrasilClient;
import moe.yushi.authlibinjector.yggdrasil.YggdrasilResponseBuilder;
public class QueryUUIDsFilter implements URLFilter {
private YggdrasilClient mojangClient;
private YggdrasilClient customClient;
public QueryUUIDsFilter(YggdrasilClient mojangClient, YggdrasilClient customClient) {
this.mojangClient = mojangClient;
this.customClient = customClient;
}
@Override
public boolean canHandle(String domain, String path) {
return domain.equals("api.mojang.com") && path.startsWith("/profiles/");
}
@Override
public Optional<Response> handle(String domain, String path, IHTTPSession session) throws IOException {
if (domain.equals("api.mojang.com") && path.equals("/profiles/minecraft") && session.getMethod().equals("POST")) {
Set<String> request = new LinkedHashSet<>();
asJsonArray(parseJson(asString(asBytes(session.getInputStream()))))
.forEach(element -> request.add(asJsonString(element)));
return Optional.of(Response.newFixedLength(Status.OK, CONTENT_TYPE_JSON,
YggdrasilResponseBuilder.queryUUIDs(performQuery(request))));
} else {
return Optional.empty();
}
}
private Map<String, UUID> performQuery(Set<String> names) {
Set<String> customNames = new LinkedHashSet<>();
Set<String> mojangNames = new LinkedHashSet<>();
names.forEach(name -> {
if (name.endsWith(NAME_SUFFIX)) {
mojangNames.add(name.substring(0, name.length() - NAME_SUFFIX.length()));
} else {
customNames.add(name);
}
});
Map<String, UUID> result = new LinkedHashMap<>();
if (!customNames.isEmpty()) {
result.putAll(customClient.queryUUIDs(customNames));
}
if (!mojangNames.isEmpty()) {
mojangClient.queryUUIDs(mojangNames)
.forEach((name, uuid) -> {
result.put(name + NAME_SUFFIX, maskUUID(uuid));
});
}
return result;
}
private static final int MSB_MASK = 0x00008000;
static final String NAME_SUFFIX = "@mojang";
static UUID maskUUID(UUID uuid) {
if (isMaskedUUID(uuid)) {
Logging.HTTPD.warning("UUID already masked: " + uuid);
}
return new UUID(uuid.getMostSignificantBits() | MSB_MASK, uuid.getLeastSignificantBits());
}
static boolean isMaskedUUID(UUID uuid) {
return (uuid.getMostSignificantBits() & MSB_MASK) != 0;
}
static UUID unmaskUUID(UUID uuid) {
return new UUID(uuid.getMostSignificantBits() & (~MSB_MASK), uuid.getLeastSignificantBits());
}
}

View file

@ -0,0 +1,30 @@
package moe.yushi.authlibinjector.httpd;
import java.io.IOException;
import java.util.Optional;
import moe.yushi.authlibinjector.internal.fi.iki.elonen.IHTTPSession;
import moe.yushi.authlibinjector.internal.fi.iki.elonen.Response;
/**
* A URLFilter filters the URLs in the bytecode, and intercepts those he is interested in.
*/
public interface URLFilter {
/**
* Returns true if the filter MAY be interested in the given URL.
*
* The URL is grabbed from the bytecode, and it may be different from the URL being used at runtime.
* Therefore, the URLs may be incomplete or malformed, or contain some template symbols. eg:
* https://api.mojang.com/profiles/ (the actual one being used is https://api.mojang.com/profiles/minecraft)
* https://sessionserver.mojang.com/session/minecraft/profile/<uuid> (template symbols)
*
* If this method returns true for the given URL, the URL will be intercepted.
* And when a request is sent to this URL, handle() will be invoked.
* If it turns out that the filter doesn't really want to intercept the URL (handle() returns empty),
* the request will be reverse-proxied to the original URL, as if nothing happened.
*/
boolean canHandle(String domain, String path);
Optional<Response> handle(String domain, String path, IHTTPSession session) throws IOException;
}

View file

@ -0,0 +1,215 @@
package moe.yushi.authlibinjector.httpd;
import static moe.yushi.authlibinjector.util.IOUtils.transfer;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import moe.yushi.authlibinjector.internal.fi.iki.elonen.IHTTPSession;
import moe.yushi.authlibinjector.internal.fi.iki.elonen.IStatus;
import moe.yushi.authlibinjector.internal.fi.iki.elonen.NanoHTTPD;
import moe.yushi.authlibinjector.internal.fi.iki.elonen.Response;
import moe.yushi.authlibinjector.internal.fi.iki.elonen.Status;
import moe.yushi.authlibinjector.util.Logging;
public class URLProcessor {
private static final Pattern URL_REGEX = Pattern.compile("^(?<protocol>https?):\\/\\/(?<domain>[^\\/]+)(?<path>\\/.*)$");
private static final Pattern LOCAL_URL_REGEX = Pattern.compile("^/(?<protocol>https?)/(?<domain>[^\\/]+)(?<path>\\/.*)$");
private List<URLFilter> filters;
private URLRedirector redirector;
public URLProcessor(List<URLFilter> filters, URLRedirector redirector) {
this.filters = filters;
this.redirector = redirector;
}
/**
* Transforms the input URL(which is grabbed from the bytecode).
*
* If any filter is interested in the URL, the URL will be redirected to the local HTTP server.
* Otherwise, the URLRedirector will be invoked to determine whether the URL should be modified
* and pointed to the customized authentication server.
* If none of above happens, empty is returned.
*
* @return the transformed URL, or empty if it doesn't need to be transformed
*/
public Optional<String> transformURL(String inputUrl) {
Matcher matcher = URL_REGEX.matcher(inputUrl);
if (!matcher.find()) {
return Optional.empty();
}
String protocol = matcher.group("protocol");
String domain = matcher.group("domain");
String path = matcher.group("path");
Optional<String> result = transform(protocol, domain, path);
if (result.isPresent()) {
Logging.TRANSFORM.fine("Transformed url [" + inputUrl + "] to [" + result.get() + "]");
}
return result;
}
private Optional<String> transform(String protocol, String domain, String path) {
boolean handleLocally = false;
for (URLFilter filter : filters) {
if (filter.canHandle(domain, path)) {
handleLocally = true;
break;
}
}
if (handleLocally) {
return Optional.of("http://127.0.0.1:" + getLocalApiPort() + "/" + protocol + "/" + domain + path);
}
return redirector.redirect(domain, path);
}
private volatile NanoHTTPD httpd;
private final Object httpdLock = new Object();
private int getLocalApiPort() {
synchronized (httpdLock) {
if (httpd == null) {
httpd = createHttpd();
try {
httpd.start();
} catch (IOException e) {
throw new IllegalStateException("Httpd failed to start");
}
Logging.HTTPD.info("Httpd is running on port " + httpd.getListeningPort());
}
return httpd.getListeningPort();
}
}
private NanoHTTPD createHttpd() {
return new NanoHTTPD("127.0.0.1", 0) {
@Override
public Response serve(IHTTPSession session) {
Matcher matcher = LOCAL_URL_REGEX.matcher(session.getUri());
if (matcher.find()) {
String protocol = matcher.group("protocol");
String domain = matcher.group("domain");
String path = matcher.group("path");
for (URLFilter filter : filters) {
if (filter.canHandle(domain, path)) {
Optional<Response> result;
try {
result = filter.handle(domain, path, session);
} catch (Throwable e) {
Logging.HTTPD.log(Level.WARNING, "An error occurred while processing request [" + session.getUri() + "]", e);
return Response.newFixedLength(Status.INTERNAL_ERROR, MIME_PLAINTEXT, "Internal Server Error");
}
if (result.isPresent()) {
Logging.HTTPD.fine("Request to [" + session.getUri() + "] is handled by [" + filter + "]");
return result.get();
}
}
}
String target = redirector.redirect(domain, path)
.orElseGet(() -> protocol + "://" + domain + path);
try {
return reverseProxy(session, target);
} catch (IOException e) {
Logging.HTTPD.log(Level.WARNING, "Reverse proxy error", e);
return Response.newFixedLength(Status.BAD_GATEWAY, MIME_PLAINTEXT, "Bad Gateway");
}
} else {
Logging.HTTPD.fine("No handler is found for [" + session.getUri() + "]");
return Response.newFixedLength(Status.NOT_FOUND, MIME_PLAINTEXT, "Not Found");
}
}
};
}
private static final Set<String> ignoredHeaders = new HashSet<>(Arrays.asList("host", "expect", "connection", "keep-alive", "transfer-encoding"));
@SuppressWarnings("resource")
private Response reverseProxy(IHTTPSession session, String upstream) throws IOException {
String method = session.getMethod();
String url = session.getQueryParameterString() == null ? upstream : upstream + "?" + session.getQueryParameterString();
Map<String, String> requestHeaders = session.getHeaders();
ignoredHeaders.forEach(requestHeaders::remove);
InputStream clientIn = session.getInputStream();
Logging.HTTPD.fine(() -> "Reverse proxy: > " + method + " " + url + ", headers: " + requestHeaders);
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setRequestMethod(method);
conn.setDoOutput(clientIn != null);
requestHeaders.forEach(conn::setRequestProperty);
if (clientIn != null) {
try (OutputStream upstreamOut = conn.getOutputStream()) {
transfer(clientIn, upstreamOut);
}
}
int responseCode = conn.getResponseCode();
String reponseMessage = conn.getResponseMessage();
Map<String, List<String>> responseHeaders = new LinkedHashMap<>();
conn.getHeaderFields().forEach((name, values) -> {
if (name != null && !ignoredHeaders.contains(name.toLowerCase())) {
responseHeaders.put(name, values);
}
});
InputStream upstreamIn;
try {
upstreamIn = conn.getInputStream();
} catch (IOException e) {
upstreamIn = conn.getErrorStream();
}
Logging.HTTPD.fine(() -> "Reverse proxy: < " + responseCode + " " + reponseMessage + " , headers: " + responseHeaders);
IStatus status = new IStatus() {
@Override
public int getRequestStatus() {
return responseCode;
}
@Override
public String getDescription() {
return responseCode + " " + reponseMessage;
}
};
long contentLength = -1;
for (Entry<String, List<String>> header : responseHeaders.entrySet()) {
if ("content-length".equalsIgnoreCase(header.getKey())) {
contentLength = Long.parseLong(header.getValue().get(0));
break;
}
}
Response response;
if (contentLength != -1) {
response = Response.newFixedLength(status, null, upstreamIn, contentLength);
} else {
response = Response.newChunked(status, null, upstreamIn);
}
responseHeaders.forEach((name, values) -> values.forEach(value -> response.addHeader(name, value)));
return response;
}
}

View file

@ -0,0 +1,11 @@
package moe.yushi.authlibinjector.httpd;
import java.util.Optional;
/**
* A URLRedirector modifies the URLs found in the bytecode,
* and points them to the customized authentication server.
*/
public interface URLRedirector {
Optional<String> redirect(String domain, String path);
}

View file

@ -0,0 +1,111 @@
package moe.yushi.authlibinjector.internal.fi.iki.elonen;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
/**
* @author yushijinhun
*/
class ChunkedInputStream extends InputStream {
private final InputStream in;
// 0 = end of chunk, \r\n hasn't been read
// -1 = begin of chunk
// -2 = closed
// other values = bytes remaining in current chunk
private int currentRemaining = -1;
public ChunkedInputStream(InputStream in) {
this.in = in;
}
@Override
public synchronized int read() throws IOException {
if (currentRemaining == -2) {
return -1;
}
if (currentRemaining == 0) {
readCRLF();
currentRemaining = -1;
}
if (currentRemaining == -1) {
currentRemaining = readChunkLength();
if (currentRemaining == 0) {
readCRLF();
currentRemaining = -2;
return -1;
}
}
int result = in.read();
currentRemaining--;
if (result == -1) {
throw new EOFException();
}
return result;
}
private int readChunkLength() throws IOException {
int length = 0;
int b;
for (;;) {
b = in.read();
if (b == -1) {
throw new EOFException();
}
if (b == '\r') {
b = in.read();
if (b == -1) {
throw new EOFException();
} else if (b == '\n') {
return length;
} else {
throw new IOException("LF is expected, read: " + b);
}
}
int digit = hexDigit(b);
if (digit == -1) {
throw new IOException("Hex digit is expected, read: " + b);
}
if ((length & 0xf8000000) != 0) { // highest 5 bits must be zero
throw new IOException("Chunk is too long");
}
length <<= 4;
length += digit;
}
}
private void readCRLF() throws IOException {
int b1 = in.read();
int b2 = in.read();
if (b1 == '\r' && b2 == '\n') {
return;
}
if (b1 == -1 || b2 == -1) {
throw new EOFException();
}
throw new IOException("CRLF is expected, read: " + b1 + " " + b2);
}
private static int hexDigit(int ch) {
if (ch >= '0' && ch <= '9') {
return ch - '0';
} else if (ch >= 'a' && ch <= 'f') {
return ch - 'a' + 10;
} else if (ch >= 'A' && ch <= 'F') {
return ch - 'A' + 10;
} else {
return -1;
}
}
@Override
public synchronized int available() throws IOException {
if (currentRemaining > 0) {
return Math.min(currentRemaining, in.available());
} else {
return 0;
}
}
}

View file

@ -0,0 +1,45 @@
package moe.yushi.authlibinjector.internal.fi.iki.elonen;
import static java.nio.charset.StandardCharsets.US_ASCII;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
/**
* Output stream that will automatically send every write to the wrapped
* OutputStream according to chunked transfer:
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1
*/
class ChunkedOutputStream extends FilterOutputStream {
public ChunkedOutputStream(OutputStream out) {
super(out);
}
@Override
public void write(int b) throws IOException {
byte[] data = {
(byte) b
};
write(data, 0, 1);
}
@Override
public void write(byte[] b) throws IOException {
write(b, 0, b.length);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
if (len == 0)
return;
out.write(String.format("%x\r\n", len).getBytes(US_ASCII));
out.write(b, off, len);
out.write("\r\n".getBytes(US_ASCII));
}
public void finish() throws IOException {
out.write("0\r\n\r\n".getBytes(US_ASCII));
}
}

View file

@ -0,0 +1,79 @@
package moe.yushi.authlibinjector.internal.fi.iki.elonen;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
class ContentType {
private static final String ASCII_ENCODING = "US-ASCII";
private static final String MULTIPART_FORM_DATA_HEADER = "multipart/form-data";
private static final String CONTENT_REGEX = "[ |\t]*([^/^ ^;^,]+/[^ ^;^,]+)";
private static final Pattern MIME_PATTERN = Pattern.compile(CONTENT_REGEX, Pattern.CASE_INSENSITIVE);
private static final String CHARSET_REGEX = "[ |\t]*(charset)[ |\t]*=[ |\t]*['|\"]?([^\"^'^;^,]*)['|\"]?";
private static final Pattern CHARSET_PATTERN = Pattern.compile(CHARSET_REGEX, Pattern.CASE_INSENSITIVE);
private static final String BOUNDARY_REGEX = "[ |\t]*(boundary)[ |\t]*=[ |\t]*['|\"]?([^\"^'^;^,]*)['|\"]?";
private static final Pattern BOUNDARY_PATTERN = Pattern.compile(BOUNDARY_REGEX, Pattern.CASE_INSENSITIVE);
private final String contentTypeHeader;
private final String contentType;
private final String encoding;
private final String boundary;
public ContentType(String contentTypeHeader) {
this.contentTypeHeader = contentTypeHeader;
if (contentTypeHeader != null) {
contentType = getDetailFromContentHeader(contentTypeHeader, MIME_PATTERN, "", 1);
encoding = getDetailFromContentHeader(contentTypeHeader, CHARSET_PATTERN, null, 2);
} else {
contentType = "";
encoding = "UTF-8";
}
if (MULTIPART_FORM_DATA_HEADER.equalsIgnoreCase(contentType)) {
boundary = getDetailFromContentHeader(contentTypeHeader, BOUNDARY_PATTERN, null, 2);
} else {
boundary = null;
}
}
private String getDetailFromContentHeader(String contentTypeHeader, Pattern pattern, String defaultValue, int group) {
Matcher matcher = pattern.matcher(contentTypeHeader);
return matcher.find() ? matcher.group(group) : defaultValue;
}
public String getContentTypeHeader() {
return contentTypeHeader;
}
public String getContentType() {
return contentType;
}
public String getEncoding() {
return encoding == null ? ASCII_ENCODING : encoding;
}
public String getBoundary() {
return boundary;
}
public boolean isMultipart() {
return MULTIPART_FORM_DATA_HEADER.equalsIgnoreCase(contentType);
}
public ContentType tryUTF8() {
if (encoding == null) {
return new ContentType(this.contentTypeHeader + "; charset=UTF-8");
}
return this;
}
}

View file

@ -0,0 +1,38 @@
package moe.yushi.authlibinjector.internal.fi.iki.elonen;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
/**
* @author yushijinhun
*/
class FixedLengthInputStream extends InputStream {
private final InputStream in;
private long remaining = 0;
public FixedLengthInputStream(InputStream in, long length) {
this.remaining = length;
this.in = in;
}
@Override
public synchronized int read() throws IOException {
if (remaining > 0) {
int result = in.read();
if (result == -1) {
throw new EOFException();
}
remaining--;
return result;
} else {
return -1;
}
}
@Override
public synchronized int available() throws IOException {
return Math.min(in.available(), (int) remaining);
}
}

View file

@ -0,0 +1,44 @@
package moe.yushi.authlibinjector.internal.fi.iki.elonen;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
/**
* Handles one session, i.e. parses the HTTP request and returns the
* response.
*/
public interface IHTTPSession {
void execute() throws IOException;
Map<String, String> getHeaders();
InputStream getInputStream() throws IOException;
String getMethod();
Map<String, List<String>> getParameters();
String getQueryParameterString();
/**
* @return the path part of the URL.
*/
String getUri();
/**
* Get the remote ip address of the requester.
*
* @return the IP address.
*/
String getRemoteIpAddress();
/**
* Get the remote hostname of the requester.
*
* @return the hostname.
*/
String getRemoteHostName();
}

View file

@ -0,0 +1,8 @@
package moe.yushi.authlibinjector.internal.fi.iki.elonen;
public interface IStatus {
String getDescription();
int getRequestStatus();
}

View file

@ -0,0 +1,859 @@
package moe.yushi.authlibinjector.internal.fi.iki.elonen;
import static java.nio.charset.StandardCharsets.US_ASCII;
/*
* #%L
* NanoHttpd-Core
* %%
* Copyright (C) 2012 - 2015 nanohttpd
* %%
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the nanohttpd nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
* OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
* #L%
*/
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A simple, tiny, nicely embeddable HTTP server in Java
* <p/>
* <p/>
* NanoHTTPD
* <p>
* Copyright (c) 2012-2013 by Paul S. Hawke, 2001,2005-2013 by Jarno Elonen,
* 2010 by Konstantinos Togias
* </p>
* See the separate "META-INF/licenses/nanohttpd.txt" file for the distribution license (Modified BSD licence)
*/
public abstract class NanoHTTPD {
/**
* Pluggable strategy for asynchronously executing requests.
*/
public interface AsyncRunner {
void closeAll();
void closed(ClientHandler clientHandler);
void exec(ClientHandler code);
}
/**
* The runnable that will be used for every new client connection.
*/
public class ClientHandler implements Runnable {
private final InputStream inputStream;
private final Socket acceptSocket;
public ClientHandler(InputStream inputStream, Socket acceptSocket) {
this.inputStream = inputStream;
this.acceptSocket = acceptSocket;
}
public void close() {
safeClose(this.inputStream);
safeClose(this.acceptSocket);
}
@Override
public void run() {
OutputStream outputStream = null;
try {
outputStream = this.acceptSocket.getOutputStream();
HTTPSession session = new HTTPSession(this.inputStream, outputStream, this.acceptSocket.getInetAddress());
while (!this.acceptSocket.isClosed()) {
session.execute();
}
} catch (Exception e) {
// When the socket is closed by the client,
// we throw our own SocketException
// to break the "keep alive" loop above. If
// the exception was anything other
// than the expected SocketException OR a
// SocketTimeoutException, print the
// stacktrace
if (!(e instanceof SocketException && "NanoHttpd Shutdown".equals(e.getMessage())) && !(e instanceof SocketTimeoutException)) {
NanoHTTPD.LOG.log(Level.SEVERE, "Communication with the client broken, or an bug in the handler code", e);
}
} finally {
safeClose(outputStream);
safeClose(this.inputStream);
safeClose(this.acceptSocket);
NanoHTTPD.this.asyncRunner.closed(this);
}
}
}
/**
* Default threading strategy for NanoHTTPD.
* <p/>
* <p>
* By default, the server spawns a new Thread for every incoming request.
* These are set to <i>daemon</i> status, and named according to the request
* number. The name is useful when profiling the application.
* </p>
*/
public static class DefaultAsyncRunner implements AsyncRunner {
private long requestCount;
private final List<ClientHandler> running = Collections.synchronizedList(new ArrayList<NanoHTTPD.ClientHandler>());
/**
* @return a list with currently running clients.
*/
public List<ClientHandler> getRunning() {
return running;
}
@Override
public void closeAll() {
// copy of the list for concurrency
for (ClientHandler clientHandler : new ArrayList<>(this.running)) {
clientHandler.close();
}
}
@Override
public void closed(ClientHandler clientHandler) {
this.running.remove(clientHandler);
}
@Override
public void exec(ClientHandler clientHandler) {
++this.requestCount;
Thread t = new Thread(clientHandler);
t.setDaemon(true);
t.setName("NanoHttpd Request Processor (#" + this.requestCount + ")");
this.running.add(clientHandler);
t.start();
}
}
/**
* Creates a normal ServerSocket for TCP connections
*/
public static class DefaultServerSocketFactory implements ServerSocketFactory {
@Override
public ServerSocket create() throws IOException {
return new ServerSocket();
}
}
protected class HTTPSession implements IHTTPSession {
public static final int BUFSIZE = 8192;
public static final int MAX_HEADER_SIZE = 1024;
private final OutputStream outputStream;
private final BufferedInputStream inputStream;
private InputStream parsedInputStream;
private int splitbyte;
private int rlen;
private String uri;
private String method;
private Map<String, List<String>> parms;
private Map<String, String> headers;
private String queryParameterString;
private String remoteIp;
private String remoteHostname;
private String protocolVersion;
private boolean expect100Continue;
private boolean continueSent;
private boolean isServing;
private final Object servingLock = new Object();
public HTTPSession(InputStream inputStream, OutputStream outputStream) {
this.inputStream = new BufferedInputStream(inputStream, HTTPSession.BUFSIZE);
this.outputStream = outputStream;
}
public HTTPSession(InputStream inputStream, OutputStream outputStream, InetAddress inetAddress) {
this.inputStream = new BufferedInputStream(inputStream, HTTPSession.BUFSIZE);
this.outputStream = outputStream;
this.remoteIp = inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() ? "127.0.0.1" : inetAddress.getHostAddress();
this.remoteHostname = inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() ? "localhost" : inetAddress.getHostName();
this.headers = new HashMap<>();
}
/**
* Decodes the sent headers and loads the data into Key/value pairs
*/
private void decodeHeader(BufferedReader in, Map<String, String> pre, Map<String, List<String>> parms, Map<String, String> headers) throws ResponseException {
try {
// Read the request line
String inLine = in.readLine();
if (inLine == null) {
return;
}
StringTokenizer st = new StringTokenizer(inLine);
if (!st.hasMoreTokens()) {
throw new ResponseException(Status.BAD_REQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html");
}
pre.put("method", st.nextToken());
if (!st.hasMoreTokens()) {
throw new ResponseException(Status.BAD_REQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html");
}
String uri = st.nextToken();
// Decode parameters from the URI
int qmi = uri.indexOf('?');
if (qmi >= 0) {
decodeParms(uri.substring(qmi + 1), parms);
uri = decodePercent(uri.substring(0, qmi));
} else {
uri = decodePercent(uri);
}
// If there's another token, its protocol version,
// followed by HTTP headers.
// NOTE: this now forces header names lower case since they are
// case insensitive and vary by client.
if (st.hasMoreTokens()) {
protocolVersion = st.nextToken();
} else {
protocolVersion = "HTTP/1.1";
NanoHTTPD.LOG.log(Level.FINE, "no protocol version specified, strange. Assuming HTTP/1.1.");
}
String line = in.readLine();
while (line != null && !line.trim().isEmpty()) {
int p = line.indexOf(':');
if (p >= 0) {
headers.put(line.substring(0, p).trim().toLowerCase(Locale.US), line.substring(p + 1).trim());
}
line = in.readLine();
}
pre.put("uri", uri);
} catch (IOException ioe) {
throw new ResponseException(Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe);
}
}
/**
* Decodes parameters in percent-encoded URI-format ( e.g.
* "name=Jack%20Daniels&pass=Single%20Malt" ) and adds them to given
* Map.
*/
private void decodeParms(String parms, Map<String, List<String>> p) {
if (parms == null) {
this.queryParameterString = "";
return;
}
this.queryParameterString = parms;
StringTokenizer st = new StringTokenizer(parms, "&");
while (st.hasMoreTokens()) {
String e = st.nextToken();
int sep = e.indexOf('=');
String key = null;
String value = null;
if (sep >= 0) {
key = decodePercent(e.substring(0, sep)).trim();
value = decodePercent(e.substring(sep + 1));
} else {
key = decodePercent(e).trim();
value = "";
}
List<String> values = p.get(key);
if (values == null) {
values = new ArrayList<>();
p.put(key, values);
}
values.add(value);
}
}
@SuppressWarnings("resource")
@Override
public void execute() throws IOException {
Response r = null;
try {
// Read the first 8192 bytes.
// The full header should fit in here.
// Apache's default header limit is 8KB.
// Do NOT assume that a single read will get the entire header
// at once!
byte[] buf = new byte[HTTPSession.BUFSIZE];
this.splitbyte = 0;
this.rlen = 0;
int read = -1;
this.inputStream.mark(HTTPSession.BUFSIZE);
try {
read = this.inputStream.read(buf, 0, HTTPSession.BUFSIZE);
} catch (IOException e) {
safeClose(this.inputStream);
safeClose(this.outputStream);
throw new SocketException("NanoHttpd Shutdown");
}
if (read == -1) {
// socket was been closed
safeClose(this.inputStream);
safeClose(this.outputStream);
throw new SocketException("NanoHttpd Shutdown");
}
while (read > 0) {
this.rlen += read;
this.splitbyte = findHeaderEnd(buf, this.rlen);
if (this.splitbyte > 0) {
break;
}
read = this.inputStream.read(buf, this.rlen, HTTPSession.BUFSIZE - this.rlen);
}
if (this.splitbyte < this.rlen) {
this.inputStream.reset();
this.inputStream.skip(this.splitbyte);
}
this.parms = new HashMap<>();
if (null == this.headers) {
this.headers = new HashMap<>();
} else {
this.headers.clear();
}
// Create a BufferedReader for parsing the header.
BufferedReader hin = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, this.rlen), US_ASCII));
// Decode the header into parms and header java properties
Map<String, String> pre = new HashMap<>();
decodeHeader(hin, pre, this.parms, this.headers);
this.method = pre.get("method");
this.uri = pre.get("uri");
String connection = this.headers.get("connection");
boolean keepAlive = "HTTP/1.1".equals(protocolVersion) && (connection == null || !connection.matches("(?i).*close.*"));
String transferEncoding = this.headers.get("transfer-encoding");
String contentLengthStr = this.headers.get("content-length");
if (transferEncoding != null && contentLengthStr == null) {
if ("chunked".equals(transferEncoding)) {
parsedInputStream = new ChunkedInputStream(inputStream);
} else {
throw new ResponseException(Status.NOT_IMPLEMENTED, "Unsupported Transfer-Encoding");
}
} else if (transferEncoding == null && contentLengthStr != null) {
int contentLength = -1;
try {
contentLength = Integer.parseInt(contentLengthStr);
} catch (NumberFormatException e) {
}
if (contentLength < 0) {
throw new ResponseException(Status.BAD_REQUEST, "The request has an invalid Content-Length header.");
}
parsedInputStream = new FixedLengthInputStream(inputStream, contentLength);
} else if (transferEncoding != null && contentLengthStr != null) {
throw new ResponseException(Status.BAD_REQUEST, "Content-Length and Transfer-Encoding cannot exist at the same time.");
} else /* if both are null */ {
// no request payload
}
expect100Continue = "HTTP/1.1".equals(protocolVersion)
&& "100-continue".equals(this.headers.get("expect"))
&& parsedInputStream != null;
// Ok, now do the serve()
this.isServing = true;
try {
r = serve(this);
} finally {
synchronized (servingLock) {
this.isServing = false;
}
}
if (!(parsedInputStream == null || (expect100Continue && !continueSent))) {
// consume the input
while (parsedInputStream.read() != -1)
;
}
if (r == null) {
throw new ResponseException(Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response.");
} else {
r.setRequestMethod(this.method);
r.setKeepAlive(keepAlive);
r.send(this.outputStream);
}
if (!keepAlive || r.isCloseConnection()) {
throw new SocketException("NanoHttpd Shutdown");
}
} catch (SocketException e) {
// throw it out to close socket object (finalAccept)
throw e;
} catch (SocketTimeoutException ste) {
// treat socket timeouts the same way we treat socket exceptions
// i.e. close the stream & finalAccept object by throwing the
// exception up the call stack.
throw ste;
} catch (IOException ioe) {
Response resp = Response.newFixedLength(Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
resp.send(this.outputStream);
safeClose(this.outputStream);
} catch (ResponseException re) {
Response resp = Response.newFixedLength(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage());
resp.send(this.outputStream);
safeClose(this.outputStream);
} finally {
safeClose(r);
}
}
/**
* Find byte index separating header from body. It must be the last byte
* of the first two sequential new lines.
*/
private int findHeaderEnd(final byte[] buf, int rlen) {
int splitbyte = 0;
while (splitbyte + 1 < rlen) {
// RFC2616
if (buf[splitbyte] == '\r' && buf[splitbyte + 1] == '\n' && splitbyte + 3 < rlen && buf[splitbyte + 2] == '\r' && buf[splitbyte + 3] == '\n') {
return splitbyte + 4;
}
// tolerance
if (buf[splitbyte] == '\n' && buf[splitbyte + 1] == '\n') {
return splitbyte + 2;
}
splitbyte++;
}
return 0;
}
@Override
public final Map<String, String> getHeaders() {
return this.headers;
}
@Override
public final InputStream getInputStream() throws IOException {
synchronized (servingLock) {
if (!isServing) {
throw new IllegalStateException();
}
if (expect100Continue && !continueSent) {
continueSent = true;
this.outputStream.write("HTTP/1.1 100 Continue\r\n\r\n".getBytes(US_ASCII));
}
}
return this.parsedInputStream;
}
@Override
public final String getMethod() {
return this.method;
}
@Override
public final Map<String, List<String>> getParameters() {
return this.parms;
}
@Override
public String getQueryParameterString() {
return this.queryParameterString;
}
@Override
public final String getUri() {
return this.uri;
}
/**
* Deduce body length in bytes. Either from "content-length" header or
* read bytes.
*/
public long getBodySize() {
if (this.headers.containsKey("content-length")) {
return Long.parseLong(this.headers.get("content-length"));
} else if (this.splitbyte < this.rlen) {
return this.rlen - this.splitbyte;
}
return 0;
}
@Override
public String getRemoteIpAddress() {
return this.remoteIp;
}
@Override
public String getRemoteHostName() {
return this.remoteHostname;
}
}
/**
* The runnable that will be used for the main listening thread.
*/
public class ServerRunnable implements Runnable {
private final int timeout;
private IOException bindException;
private boolean hasBinded = false;
public ServerRunnable(int timeout) {
this.timeout = timeout;
}
@Override
public void run() {
try {
myServerSocket.bind(hostname != null ? new InetSocketAddress(hostname, myPort) : new InetSocketAddress(myPort));
hasBinded = true;
} catch (IOException e) {
this.bindException = e;
return;
}
do {
try {
@SuppressWarnings("resource")
final Socket finalAccept = NanoHTTPD.this.myServerSocket.accept();
if (this.timeout > 0) {
finalAccept.setSoTimeout(this.timeout);
}
@SuppressWarnings("resource")
final InputStream inputStream = finalAccept.getInputStream();
NanoHTTPD.this.asyncRunner.exec(createClientHandler(finalAccept, inputStream));
} catch (IOException e) {
NanoHTTPD.LOG.log(Level.FINE, "Communication with the client broken", e);
}
} while (!NanoHTTPD.this.myServerSocket.isClosed());
}
}
/**
* Factory to create ServerSocketFactories.
*/
public interface ServerSocketFactory {
public ServerSocket create() throws IOException;
}
/**
* Maximum time to wait on Socket.getInputStream().read() (in milliseconds)
* This is required as the Keep-Alive HTTP connections would otherwise block
* the socket reading thread forever (or as long the browser is open).
*/
public static final int SOCKET_READ_TIMEOUT = 5000;
/**
* Common MIME type for dynamic content: plain text
*/
public static final String MIME_PLAINTEXT = "text/plain";
/**
* Common MIME type for dynamic content: html
*/
public static final String MIME_HTML = "text/html";
/**
* logger to log to.
*/
static final Logger LOG = Logger.getLogger(NanoHTTPD.class.getName());
static final void safeClose(Object closeable) {
try {
if (closeable != null) {
if (closeable instanceof Closeable) {
((Closeable) closeable).close();
} else if (closeable instanceof Socket) {
((Socket) closeable).close();
} else if (closeable instanceof ServerSocket) {
((ServerSocket) closeable).close();
} else {
throw new IllegalArgumentException("Unknown object to close");
}
}
} catch (IOException e) {
NanoHTTPD.LOG.log(Level.SEVERE, "Could not close", e);
}
}
private final String hostname;
private final int myPort;
private volatile ServerSocket myServerSocket;
private ServerSocketFactory serverSocketFactory = new DefaultServerSocketFactory();
private Thread myThread;
/**
* Pluggable strategy for asynchronously executing requests.
*/
protected AsyncRunner asyncRunner;
/**
* Constructs an HTTP server on given port.
*/
public NanoHTTPD(int port) {
this(null, port);
}
// -------------------------------------------------------------------------------
// //
//
// Threading Strategy.
//
// -------------------------------------------------------------------------------
// //
/**
* Constructs an HTTP server on given hostname and port.
*/
public NanoHTTPD(String hostname, int port) {
this.hostname = hostname;
this.myPort = port;
setAsyncRunner(new DefaultAsyncRunner());
}
/**
* Forcibly closes all connections that are open.
*/
public synchronized void closeAllConnections() {
stop();
}
/**
* create a instance of the client handler, subclasses can return a subclass
* of the ClientHandler.
*
* @param finalAccept
* the socket the cleint is connected to
* @param inputStream
* the input stream
* @return the client handler
*/
protected ClientHandler createClientHandler(final Socket finalAccept, final InputStream inputStream) {
return new ClientHandler(inputStream, finalAccept);
}
/**
* Instantiate the server runnable, can be overwritten by subclasses to
* provide a subclass of the ServerRunnable.
*
* @param timeout
* the socet timeout to use.
* @return the server runnable.
*/
protected ServerRunnable createServerRunnable(final int timeout) {
return new ServerRunnable(timeout);
}
/**
* Decode percent encoded <code>String</code> values.
*
* @param str
* the percent encoded <code>String</code>
* @return expanded form of the input, for example "foo%20bar" becomes
* "foo bar"
*/
private static String decodePercent(String str) {
String decoded = null;
try {
decoded = URLDecoder.decode(str, "UTF8");
} catch (UnsupportedEncodingException ignored) {
NanoHTTPD.LOG.log(Level.WARNING, "Encoding not supported, ignored", ignored);
}
return decoded;
}
public final int getListeningPort() {
return this.myServerSocket == null ? -1 : this.myServerSocket.getLocalPort();
}
public final boolean isAlive() {
return wasStarted() && !this.myServerSocket.isClosed() && this.myThread.isAlive();
}
public ServerSocketFactory getServerSocketFactory() {
return serverSocketFactory;
}
public void setServerSocketFactory(ServerSocketFactory serverSocketFactory) {
this.serverSocketFactory = serverSocketFactory;
}
public String getHostname() {
return hostname;
}
/**
* Override this to customize the server.
* <p/>
* <p/>
* (By default, this returns a 404 "Not Found" plain text error response.)
*
* @param session
* The HTTP session
* @return HTTP response, see class Response for details
*/
public Response serve(IHTTPSession session) {
return Response.newFixedLength(Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, "Not Found");
}
/**
* Pluggable strategy for asynchronously executing requests.
*
* @param asyncRunner
* new strategy for handling threads.
*/
public void setAsyncRunner(AsyncRunner asyncRunner) {
this.asyncRunner = asyncRunner;
}
/**
* Start the server.
*
* @throws IOException
* if the socket is in use.
*/
public void start() throws IOException {
start(NanoHTTPD.SOCKET_READ_TIMEOUT);
}
/**
* Starts the server (in setDaemon(true) mode).
*/
public void start(final int timeout) throws IOException {
start(timeout, true);
}
/**
* Start the server.
*
* @param timeout
* timeout to use for socket connections.
* @param daemon
* start the thread daemon or not.
* @throws IOException
* if the socket is in use.
*/
public void start(final int timeout, boolean daemon) throws IOException {
this.myServerSocket = this.getServerSocketFactory().create();
this.myServerSocket.setReuseAddress(true);
ServerRunnable serverRunnable = createServerRunnable(timeout);
this.myThread = new Thread(serverRunnable);
this.myThread.setDaemon(daemon);
this.myThread.setName("NanoHttpd Main Listener");
this.myThread.start();
while (!serverRunnable.hasBinded && serverRunnable.bindException == null) {
try {
Thread.sleep(10L);
} catch (Throwable e) {
// on android this may not be allowed, that's why we
// catch throwable the wait should be very short because we are
// just waiting for the bind of the socket
}
}
if (serverRunnable.bindException != null) {
throw serverRunnable.bindException;
}
}
/**
* Stop the server.
*/
public void stop() {
try {
safeClose(this.myServerSocket);
this.asyncRunner.closeAll();
if (this.myThread != null) {
this.myThread.join();
}
} catch (Exception e) {
NanoHTTPD.LOG.log(Level.SEVERE, "Could not stop all connections", e);
}
}
public final boolean wasStarted() {
return this.myServerSocket != null && this.myThread != null;
}
}

View file

@ -0,0 +1,316 @@
package moe.yushi.authlibinjector.internal.fi.iki.elonen;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import java.util.Map.Entry;
import java.util.logging.Level;
/**
* HTTP response. Return one of these from serve().
*/
public class Response implements Closeable {
/**
* HTTP status code after processing, e.g. "200 OK", Status.OK
*/
private IStatus status;
/**
* MIME type of content, e.g. "text/html"
*/
private String mimeType;
/**
* Data of the response, may be null.
*/
private InputStream data;
private long contentLength;
/**
* Headers for the HTTP response. Use addHeader() to add lines. the
* lowercase map is automatically kept up to date.
*/
private final Map<String, String> header = new HashMap<String, String>() {
@Override
public String put(String key, String value) {
lowerCaseHeader.put(key == null ? key : key.toLowerCase(), value);
return super.put(key, value);
};
};
/**
* copy of the header map with all the keys lowercase for faster
* searching.
*/
private final Map<String, String> lowerCaseHeader = new HashMap<>();
/**
* The request method that spawned this response.
*/
private String requestMethod;
/**
* Use chunkedTransfer
*/
private boolean chunkedTransfer;
private boolean keepAlive;
/**
* Creates a fixed length response if totalBytes>=0, otherwise chunked.
*/
protected Response(IStatus status, String mimeType, InputStream data, long totalBytes) {
this.status = status;
this.mimeType = mimeType;
if (data == null) {
this.data = new ByteArrayInputStream(new byte[0]);
this.contentLength = 0L;
} else {
this.data = data;
this.contentLength = totalBytes;
}
this.chunkedTransfer = this.contentLength < 0;
keepAlive = true;
}
@Override
public void close() throws IOException {
if (this.data != null) {
this.data.close();
}
}
/**
* Adds given line to the header.
*/
public void addHeader(String name, String value) {
this.header.put(name, value);
}
/**
* Indicate to close the connection after the Response has been sent.
*
* @param close
* {@code true} to hint connection closing, {@code false} to
* let connection be closed by client.
*/
public void closeConnection(boolean close) {
if (close)
this.header.put("connection", "close");
else
this.header.remove("connection");
}
/**
* @return {@code true} if connection is to be closed after this
* Response has been sent.
*/
public boolean isCloseConnection() {
return "close".equals(getHeader("connection"));
}
public InputStream getData() {
return this.data;
}
public String getHeader(String name) {
return this.lowerCaseHeader.get(name.toLowerCase());
}
public String getMimeType() {
return this.mimeType;
}
public String getRequestMethod() {
return this.requestMethod;
}
public IStatus getStatus() {
return this.status;
}
public void setKeepAlive(boolean useKeepAlive) {
this.keepAlive = useKeepAlive;
}
/**
* Sends given response to the socket.
*/
protected void send(OutputStream outputStream) {
SimpleDateFormat gmtFrmt = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT"));
try {
if (this.status == null) {
throw new Error("sendResponse(): Status can't be null.");
}
PrintWriter pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(outputStream, new ContentType(this.mimeType).getEncoding())), false);
pw.append("HTTP/1.1 ").append(this.status.getDescription()).append(" \r\n");
if (this.mimeType != null) {
printHeader(pw, "Content-Type", this.mimeType);
}
if (getHeader("date") == null) {
printHeader(pw, "Date", gmtFrmt.format(new Date()));
}
for (Entry<String, String> entry : this.header.entrySet()) {
printHeader(pw, entry.getKey(), entry.getValue());
}
if (getHeader("connection") == null) {
printHeader(pw, "Connection", (this.keepAlive ? "keep-alive" : "close"));
}
long pending = this.data != null ? this.contentLength : 0;
if (!"HEAD".equals(this.requestMethod) && this.chunkedTransfer) {
printHeader(pw, "Transfer-Encoding", "chunked");
} else {
pending = sendContentLengthHeaderIfNotAlreadyPresent(pw, pending);
}
pw.append("\r\n");
pw.flush();
sendBodyWithCorrectTransferAndEncoding(outputStream, pending);
outputStream.flush();
NanoHTTPD.safeClose(this.data);
} catch (IOException ioe) {
NanoHTTPD.LOG.log(Level.SEVERE, "Could not send response to the client", ioe);
}
}
protected void printHeader(PrintWriter pw, String key, String value) {
pw.append(key).append(": ").append(value).append("\r\n");
}
protected long sendContentLengthHeaderIfNotAlreadyPresent(PrintWriter pw, long defaultSize) {
String contentLengthString = getHeader("content-length");
long size = defaultSize;
if (contentLengthString != null) {
try {
size = Long.parseLong(contentLengthString);
} catch (NumberFormatException ex) {
NanoHTTPD.LOG.severe("content-length was no number " + contentLengthString);
}
}
pw.print("Content-Length: " + size + "\r\n");
return size;
}
private void sendBodyWithCorrectTransferAndEncoding(OutputStream outputStream, long pending) throws IOException {
if (!"HEAD".equals(this.requestMethod) && this.chunkedTransfer) {
@SuppressWarnings("resource")
ChunkedOutputStream chunkedOutputStream = new ChunkedOutputStream(outputStream);
sendBody(chunkedOutputStream, -1);
chunkedOutputStream.finish();
} else {
sendBody(outputStream, pending);
}
}
/**
* Sends the body to the specified OutputStream. The pending parameter
* limits the maximum amounts of bytes sent unless it is -1, in which
* case everything is sent.
*
* @param outputStream
* the OutputStream to send data to
* @param pending
* -1 to send everything, otherwise sets a max limit to the
* number of bytes sent
* @throws IOException
* if something goes wrong while sending the data.
*/
private void sendBody(OutputStream outputStream, long pending) throws IOException {
long BUFFER_SIZE = 16 * 1024;
byte[] buff = new byte[(int) BUFFER_SIZE];
boolean sendEverything = pending == -1;
while (pending > 0 || sendEverything) {
long bytesToRead = sendEverything ? BUFFER_SIZE : Math.min(pending, BUFFER_SIZE);
int read = this.data.read(buff, 0, (int) bytesToRead);
if (read <= 0) {
break;
}
outputStream.write(buff, 0, read);
if (!sendEverything) {
pending -= read;
}
}
}
public void setChunkedTransfer(boolean chunkedTransfer) {
this.chunkedTransfer = chunkedTransfer;
}
public void setData(InputStream data) {
this.data = data;
}
public void setMimeType(String mimeType) {
this.mimeType = mimeType;
}
public void setRequestMethod(String requestMethod) {
this.requestMethod = requestMethod;
}
public void setStatus(IStatus status) {
this.status = status;
}
/**
* Create a text response with known length.
*/
public static Response newFixedLength(String msg) {
return newFixedLength(Status.OK, NanoHTTPD.MIME_HTML, msg);
}
/**
* Create a text response with known length.
*/
public static Response newFixedLength(IStatus status, String mimeType, String txt) {
ContentType contentType = new ContentType(mimeType);
if (txt == null) {
return newFixedLength(status, mimeType, new ByteArrayInputStream(new byte[0]), 0);
} else {
byte[] bytes;
try {
CharsetEncoder newEncoder = Charset.forName(contentType.getEncoding()).newEncoder();
if (!newEncoder.canEncode(txt)) {
contentType = contentType.tryUTF8();
}
bytes = txt.getBytes(contentType.getEncoding());
} catch (UnsupportedEncodingException e) {
NanoHTTPD.LOG.log(Level.SEVERE, "encoding problem, responding nothing", e);
bytes = new byte[0];
}
return newFixedLength(status, contentType.getContentTypeHeader(), new ByteArrayInputStream(bytes), bytes.length);
}
}
/**
* Create a response with known length.
*/
public static Response newFixedLength(IStatus status, String mimeType, InputStream data, long totalBytes) {
return new Response(status, mimeType, data, totalBytes);
}
/**
* Create a response with unknown length (using HTTP 1.1 chunking).
*/
public static Response newChunked(IStatus status, String mimeType, InputStream data) {
return new Response(status, mimeType, data, -1);
}
}

View file

@ -0,0 +1,20 @@
package moe.yushi.authlibinjector.internal.fi.iki.elonen;
public class ResponseException extends Exception {
private final Status status;
public ResponseException(Status status, String message) {
super(message);
this.status = status;
}
public ResponseException(Status status, String message, Exception e) {
super(message, e);
this.status = status;
}
public Status getStatus() {
return this.status;
}
}

View file

@ -0,0 +1,79 @@
package moe.yushi.authlibinjector.internal.fi.iki.elonen;
/**
* Some HTTP response status codes
*/
public enum Status implements IStatus {
SWITCH_PROTOCOL(101, "Switching Protocols"),
OK(200, "OK"),
CREATED(201, "Created"),
ACCEPTED(202, "Accepted"),
NO_CONTENT(204, "No Content"),
PARTIAL_CONTENT(206, "Partial Content"),
MULTI_STATUS(207, "Multi-Status"),
REDIRECT(301, "Moved Permanently"),
/**
* Many user agents mishandle 302 in ways that violate the RFC1945
* spec (i.e., redirect a POST to a GET). 303 and 307 were added in
* RFC2616 to address this. You should prefer 303 and 307 unless the
* calling user agent does not support 303 and 307 functionality
*/
@Deprecated
FOUND(302, "Found"),
REDIRECT_SEE_OTHER(303, "See Other"),
NOT_MODIFIED(304, "Not Modified"),
TEMPORARY_REDIRECT(307, "Temporary Redirect"),
BAD_REQUEST(400, "Bad Request"),
UNAUTHORIZED(401, "Unauthorized"),
FORBIDDEN(403, "Forbidden"),
NOT_FOUND(404, "Not Found"),
METHOD_NOT_ALLOWED(405, "Method Not Allowed"),
NOT_ACCEPTABLE(406, "Not Acceptable"),
REQUEST_TIMEOUT(408, "Request Timeout"),
CONFLICT(409, "Conflict"),
GONE(410, "Gone"),
LENGTH_REQUIRED(411, "Length Required"),
PRECONDITION_FAILED(412, "Precondition Failed"),
PAYLOAD_TOO_LARGE(413, "Payload Too Large"),
UNSUPPORTED_MEDIA_TYPE(415, "Unsupported Media Type"),
RANGE_NOT_SATISFIABLE(416, "Requested Range Not Satisfiable"),
EXPECTATION_FAILED(417, "Expectation Failed"),
TOO_MANY_REQUESTS(429, "Too Many Requests"),
INTERNAL_ERROR(500, "Internal Server Error"),
NOT_IMPLEMENTED(501, "Not Implemented"),
BAD_GATEWAY(502, "Bad Gateway"),
SERVICE_UNAVAILABLE(503, "Service Unavailable"),
UNSUPPORTED_HTTP_VERSION(505, "HTTP Version Not Supported");
private final int requestStatus;
private final String description;
Status(int requestStatus, String description) {
this.requestStatus = requestStatus;
this.description = description;
}
public static Status lookup(int requestStatus) {
for (Status status : Status.values()) {
if (status.getRequestStatus() == requestStatus) {
return status;
}
}
return null;
}
@Override
public String getDescription() {
return "" + this.requestStatus + " " + this.description;
}
@Override
public int getRequestStatus() {
return this.requestStatus;
}
}

View file

@ -0,0 +1,6 @@
/**
* Modified <a href="https://github.com/NanoHttpd/nanohttpd">nanohttpd</a>.
* <p>
* See license in META-INF/licenses/nanohttpd.txt
*/
package moe.yushi.authlibinjector.internal.fi.iki.elonen;

View file

@ -3,8 +3,6 @@
package moe.yushi.authlibinjector.internal.org.json.simple.parser;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
class Yylex {
@ -279,17 +277,6 @@ class Yylex {
zzReader = in;
}
/**
* Creates a new scanner.
* There is also Reader version of this constructor.
*
* @param in
* the Inputstream to read input from.
*/
Yylex(InputStream in) {
this(new InputStreamReader(in));
}
/**
* Unpacks the compressed character translation table.
*

View file

@ -0,0 +1,24 @@
package moe.yushi.authlibinjector.transform;
import java.util.Optional;
import moe.yushi.authlibinjector.httpd.URLProcessor;
public class ConstantURLTransformUnit extends LdcTransformUnit {
private URLProcessor urlProcessor;
public ConstantURLTransformUnit(URLProcessor urlProcessor) {
this.urlProcessor = urlProcessor;
}
@Override
protected Optional<String> transformLdc(String input) {
return urlProcessor.transformURL(input);
}
@Override
public String toString() {
return "Constant URL Transformer";
}
}

View file

@ -1,38 +0,0 @@
package moe.yushi.authlibinjector.transform;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public abstract class DomainBasedTransformUnit extends LdcTransformUnit {
private static final Pattern URL_REGEX = Pattern.compile("^https?:\\/\\/(?<domain>[^\\/]+)(?<path>\\/.*)$");
private Map<String, String> domainMapping = new ConcurrentHashMap<>();
public Map<String, String> getDomainMapping() {
return domainMapping;
}
@Override
public Optional<String> transformLdc(String input) {
Matcher matcher = URL_REGEX.matcher(input);
if (!matcher.find()) {
return Optional.empty();
}
String domain = matcher.group("domain");
String subdirectory = domainMapping.get(domain);
if (subdirectory == null) {
return Optional.empty();
}
String path = matcher.group("path");
return Optional.of(getApiRoot() + subdirectory + path);
}
protected abstract String getApiRoot();
}

View file

@ -4,7 +4,6 @@ import static org.objectweb.asm.Opcodes.ASM6;
import java.util.Optional;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import moe.yushi.authlibinjector.util.Logging;
public abstract class LdcTransformUnit implements TransformUnit {
@ -22,7 +21,6 @@ public abstract class LdcTransformUnit implements TransformUnit {
Optional<String> transformed = transformLdc((String) cst);
if (transformed.isPresent() && !transformed.get().equals(cst)) {
modifiedCallback.run();
Logging.TRANSFORM.fine("Transformed string [" + cst + "] to [" + transformed.get() + "]");
super.visitLdcInsn(transformed.get());
} else {
super.visitLdcInsn(cst);

View file

@ -1,34 +0,0 @@
package moe.yushi.authlibinjector.transform;
import java.util.Map;
import moe.yushi.authlibinjector.YggdrasilConfiguration;
import moe.yushi.authlibinjector.httpd.LocalYggdrasilHandle;
import moe.yushi.authlibinjector.util.Logging;
public class LocalYggdrasilApiTransformUnit extends DomainBasedTransformUnit {
private LocalYggdrasilHandle handle;
public LocalYggdrasilApiTransformUnit(YggdrasilConfiguration config) {
handle = new LocalYggdrasilHandle(config);
Map<String, String> mapping = getDomainMapping();
if (Boolean.TRUE.equals(config.getMeta().get("feature.legacy_skin_api"))) {
Logging.CONFIG.info("Disabled local redirect for legacy skin API, as the remote Yggdrasil server supports it");
} else {
mapping.put("skins.minecraft.net", "skins");
}
}
@Override
protected String getApiRoot() {
handle.ensureStarted();
return "http://127.0.0.1:" + handle.getLocalApiPort() + "/";
}
@Override
public String toString() {
return "Local Yggdrasil API Transformer";
}
}

View file

@ -1,28 +0,0 @@
package moe.yushi.authlibinjector.transform;
import java.util.Map;
public class RemoteYggdrasilTransformUnit extends DomainBasedTransformUnit {
private String apiRoot;
public RemoteYggdrasilTransformUnit(String apiRoot) {
this.apiRoot = apiRoot;
Map<String, String> mapping = getDomainMapping();
mapping.put("api.mojang.com", "api");
mapping.put("authserver.mojang.com", "authserver");
mapping.put("sessionserver.mojang.com", "sessionserver");
mapping.put("skins.minecraft.net", "skins");
}
@Override
protected String getApiRoot() {
return apiRoot;
}
@Override
public String toString() {
return "Yggdrasil API Transformer";
}
}

View file

@ -1,25 +1,40 @@
package moe.yushi.authlibinjector.transform;
import static org.objectweb.asm.Opcodes.ACC_PRIVATE;
import static org.objectweb.asm.Opcodes.ACC_STATIC;
import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC;
import static org.objectweb.asm.Opcodes.ALOAD;
import static org.objectweb.asm.Opcodes.ASM6;
import static org.objectweb.asm.Opcodes.BASTORE;
import static org.objectweb.asm.Opcodes.BIPUSH;
import static org.objectweb.asm.Opcodes.DUP;
import static org.objectweb.asm.Opcodes.ASTORE;
import static org.objectweb.asm.Opcodes.CHECKCAST;
import static org.objectweb.asm.Opcodes.F_APPEND;
import static org.objectweb.asm.Opcodes.F_CHOP;
import static org.objectweb.asm.Opcodes.F_SAME;
import static org.objectweb.asm.Opcodes.GOTO;
import static org.objectweb.asm.Opcodes.ICONST_0;
import static org.objectweb.asm.Opcodes.ICONST_1;
import static org.objectweb.asm.Opcodes.IFEQ;
import static org.objectweb.asm.Opcodes.INVOKEINTERFACE;
import static org.objectweb.asm.Opcodes.INVOKESTATIC;
import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
import static org.objectweb.asm.Opcodes.NEWARRAY;
import static org.objectweb.asm.Opcodes.SIPUSH;
import static org.objectweb.asm.Opcodes.T_BYTE;
import static org.objectweb.asm.Opcodes.IRETURN;
import java.security.PublicKey;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CopyOnWriteArrayList;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
public class YggdrasilKeyTransformUnit implements TransformUnit {
private byte[] publicKey;
private static final List<PublicKey> PUBLIC_KEYS = new CopyOnWriteArrayList<>();
public YggdrasilKeyTransformUnit(byte[] publicKey) {
this.publicKey = publicKey;
public static List<PublicKey> getPublicKeys() {
return PUBLIC_KEYS;
}
@Override
@ -27,50 +42,76 @@ public class YggdrasilKeyTransformUnit implements TransformUnit {
if ("com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService".equals(className)) {
return Optional.of(new ClassVisitor(ASM6, writer) {
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
MethodVisitor mv = super.visitMethod(ACC_PRIVATE | ACC_STATIC | ACC_SYNTHETIC,
"authlib_injector_isSignatureValid",
"(Lcom/mojang/authlib/properties/Property;Ljava/security/PublicKey;)Z",
null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ALOAD, 1);
mv.visitMethodInsn(INVOKEVIRTUAL, "com/mojang/authlib/properties/Property", "isSignatureValid", "(Ljava/security/PublicKey;)Z", false);
Label l0 = new Label();
mv.visitJumpInsn(IFEQ, l0);
mv.visitInsn(ICONST_1);
mv.visitInsn(IRETURN);
mv.visitLabel(l0);
mv.visitFrame(F_SAME, 0, null, 0, null);
mv.visitMethodInsn(INVOKESTATIC, Type.getInternalName(YggdrasilKeyTransformUnit.class), "getPublicKeys", "()Ljava/util/List;", false);
mv.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "iterator", "()Ljava/util/Iterator;", true);
mv.visitVarInsn(ASTORE, 2);
Label l1 = new Label();
mv.visitLabel(l1);
mv.visitFrame(F_APPEND, 1, new Object[] { "java/util/Iterator" }, 0, null);
mv.visitVarInsn(ALOAD, 2);
mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Iterator", "hasNext", "()Z", true);
Label l2 = new Label();
mv.visitJumpInsn(IFEQ, l2);
mv.visitVarInsn(ALOAD, 2);
mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Iterator", "next", "()Ljava/lang/Object;", true);
mv.visitTypeInsn(CHECKCAST, "java/security/PublicKey");
mv.visitVarInsn(ASTORE, 3);
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ALOAD, 3);
mv.visitMethodInsn(INVOKEVIRTUAL, "com/mojang/authlib/properties/Property", "isSignatureValid", "(Ljava/security/PublicKey;)Z", false);
Label l3 = new Label();
mv.visitJumpInsn(IFEQ, l3);
mv.visitInsn(ICONST_1);
mv.visitInsn(IRETURN);
mv.visitLabel(l3);
mv.visitFrame(F_SAME, 0, null, 0, null);
mv.visitJumpInsn(GOTO, l1);
mv.visitLabel(l2);
mv.visitFrame(F_CHOP, 1, null, 0, null);
mv.visitInsn(ICONST_0);
mv.visitInsn(IRETURN);
mv.visitMaxs(2, 4);
mv.visitEnd();
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
if ("<init>".equals(name)) {
return new MethodVisitor(ASM6, super.visitMethod(access, name, desc, signature, exceptions)) {
int state = 0;
@Override
public void visitLdcInsn(Object cst) {
if (state == 0 && cst instanceof Type && ((Type) cst).getInternalName().equals("com/mojang/authlib/yggdrasil/YggdrasilMinecraftSessionService")) {
state++;
} else if (state == 1 && "/yggdrasil_session_pubkey.der".equals(cst)) {
state++;
} else {
super.visitLdcInsn(cst);
}
return new MethodVisitor(ASM6, super.visitMethod(access, name, desc, signature, exceptions)) {
@Override
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
if (opcode == INVOKEVIRTUAL
&& "com/mojang/authlib/properties/Property".equals(owner)
&& "isSignatureValid".equals(name)
&& "(Ljava/security/PublicKey;)Z".equals(descriptor)) {
modifiedCallback.run();
super.visitMethodInsn(INVOKESTATIC,
"com/mojang/authlib/yggdrasil/YggdrasilMinecraftSessionService",
"authlib_injector_isSignatureValid",
"(Lcom/mojang/authlib/properties/Property;Ljava/security/PublicKey;)Z",
false);
} else {
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}
@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
if (state == 2 && opcode == INVOKEVIRTUAL && "java/lang/Class".equals(owner) && "getResourceAsStream".equals(name) && "(Ljava/lang/String;)Ljava/io/InputStream;".equals(desc)) {
state++;
} else if (state == 3 && opcode == INVOKESTATIC && "org/apache/commons/io/IOUtils".equals(owner) && "toByteArray".equals(name) && "(Ljava/io/InputStream;)[B".equals(desc)) {
state++;
if (state == 4) {
modifiedCallback.run();
super.visitIntInsn(SIPUSH, publicKey.length);
super.visitIntInsn(NEWARRAY, T_BYTE);
for (int i = 0; i < publicKey.length; i++) {
super.visitInsn(DUP);
super.visitIntInsn(SIPUSH, i);
super.visitIntInsn(BIPUSH, publicKey[i]);
super.visitInsn(BASTORE);
}
}
} else {
super.visitMethodInsn(opcode, owner, name, desc, itf);
}
}
};
} else {
return super.visitMethod(access, name, desc, signature, exceptions);
}
}
};
}
});
@ -83,5 +124,4 @@ public class YggdrasilKeyTransformUnit implements TransformUnit {
public String toString() {
return "Yggdrasil Public Key Transformer";
}
}

View file

@ -0,0 +1,64 @@
package moe.yushi.authlibinjector.transform.support;
import static org.objectweb.asm.Opcodes.ALOAD;
import static org.objectweb.asm.Opcodes.ASM6;
import static org.objectweb.asm.Opcodes.F_SAME;
import static org.objectweb.asm.Opcodes.GETFIELD;
import static org.objectweb.asm.Opcodes.IFEQ;
import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
import static org.objectweb.asm.Opcodes.RETURN;
import java.util.Optional;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import moe.yushi.authlibinjector.transform.TransformUnit;
/**
* Support for Citizens2
*
* In <https://github.com/CitizensDev/Citizens2/commit/28b0c4fdc3b343d4dc14f2a45cff37c0b75ced1d>,
* the profile-url that Citizens use became configurable. This class is used to make Citizens ignore
* the config property and use authlib-injector's url.
*/
public class CitizensTransformer implements TransformUnit {
@Override
public Optional<ClassVisitor> transform(ClassLoader classLoader, String className, ClassVisitor writer, Runnable modifiedCallback) {
if ("net.citizensnpcs.Settings$Setting".equals(className)) {
return Optional.of(new ClassVisitor(ASM6, writer) {
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
if (("loadFromKey".equals(name) || "setAtKey".equals(name))
&& "(Lnet/citizensnpcs/api/util/DataKey;)V".equals(descriptor)) {
return new MethodVisitor(ASM6, super.visitMethod(access, name, descriptor, signature, exceptions)) {
@Override
public void visitCode() {
super.visitCode();
super.visitLdcInsn("general.authlib.profile-url");
super.visitVarInsn(ALOAD, 0);
super.visitFieldInsn(GETFIELD, "net/citizensnpcs/Settings$Setting", "path", "Ljava/lang/String;");
super.visitMethodInsn(INVOKEVIRTUAL, "java/lang/String", "equals", "(Ljava/lang/Object;)Z", false);
Label lbl = new Label();
super.visitJumpInsn(IFEQ, lbl);
super.visitInsn(RETURN);
super.visitLabel(lbl);
super.visitFrame(F_SAME, 0, null, 0, null);
modifiedCallback.run();
}
};
}
return super.visitMethod(access, name, descriptor, signature, exceptions);
}
});
}
return Optional.empty();
}
@Override
public String toString() {
return "Citizens2 Support";
}
}

View file

@ -11,6 +11,8 @@ import java.net.URL;
public final class IOUtils {
public static final String CONTENT_TYPE_JSON = "application/json; charset=utf-8";
public static byte[] getURL(String url) throws IOException {
try (InputStream in = new URL(url).openStream()) {
return asBytes(in);
@ -23,27 +25,26 @@ public final class IOUtils {
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();
try (OutputStream out = conn.getOutputStream()) {
out.write(payload);
}
try (InputStream in = conn.getInputStream()) {
return asBytes(in);
}
}
public static byte[] asBytes(InputStream in) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
transfer(in, out);
return out.toByteArray();
}
public static void transfer(InputStream from, OutputStream to) throws IOException {
byte[] buf = new byte[8192];
int read;
while ((read = in.read(buf)) != -1) {
out.write(buf, 0, read);
while ((read = from.read(buf)) != -1) {
to.write(buf, 0, read);
}
return out.toByteArray();
}
public static String asString(byte[] bytes) {

View file

@ -0,0 +1,20 @@
package moe.yushi.authlibinjector.util;
import java.util.UUID;
public final class UUIDUtils {
public static String toUnsignedUUID(UUID uuid) {
return uuid.toString().replace("-", "");
}
public static UUID fromUnsignedUUID(String uuid) {
if (uuid.length() == 32) {
return UUID.fromString(uuid.substring(0, 8) + "-" + uuid.substring(8, 12) + "-" + uuid.substring(12, 16) + "-" + uuid.substring(16, 20) + "-" + uuid.substring(20, 32));
} else {
throw new IllegalArgumentException("Invalid UUID: " + uuid);
}
}
private UUIDUtils() {
}
}

View file

@ -0,0 +1,31 @@
package moe.yushi.authlibinjector.yggdrasil;
import static moe.yushi.authlibinjector.util.UUIDUtils.toUnsignedUUID;
import java.util.UUID;
import moe.yushi.authlibinjector.YggdrasilConfiguration;
public class CustomYggdrasilAPIProvider implements YggdrasilAPIProvider {
private String apiRoot;
public CustomYggdrasilAPIProvider(YggdrasilConfiguration configuration) {
this.apiRoot = configuration.getApiRoot();
}
@Override
public String queryUUIDsByNames() {
return apiRoot + "api/profiles/minecraft";
}
@Override
public String queryProfile(UUID uuid) {
return apiRoot + "sessionserver/session/minecraft/profile/" + toUnsignedUUID(uuid);
}
@Override
public String toString() {
return apiRoot;
}
}

View file

@ -0,0 +1,16 @@
package moe.yushi.authlibinjector.yggdrasil;
import java.util.Map;
import java.util.UUID;
public class GameProfile {
public static class PropertyValue {
public String value;
public String signature;
}
public UUID id;
public String name;
public Map<String, PropertyValue> properties;
}

View file

@ -0,0 +1,23 @@
package moe.yushi.authlibinjector.yggdrasil;
import static moe.yushi.authlibinjector.util.UUIDUtils.toUnsignedUUID;
import java.util.UUID;
public class MojangYggdrasilAPIProvider implements YggdrasilAPIProvider {
@Override
public String queryUUIDsByNames() {
return "https://api.mojang.com/profiles/minecraft";
}
@Override
public String queryProfile(UUID uuid) {
return "https://sessionserver.mojang.com/session/minecraft/profile/" + toUnsignedUUID(uuid);
}
@Override
public String toString() {
return "Mojang";
}
}

View file

@ -0,0 +1,8 @@
package moe.yushi.authlibinjector.yggdrasil;
import java.util.UUID;
public interface YggdrasilAPIProvider {
String queryUUIDsByNames();
String queryProfile(UUID uuid);
}

View file

@ -0,0 +1,106 @@
package moe.yushi.authlibinjector.yggdrasil;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Collections.singleton;
import static moe.yushi.authlibinjector.util.IOUtils.CONTENT_TYPE_JSON;
import static moe.yushi.authlibinjector.util.IOUtils.asString;
import static moe.yushi.authlibinjector.util.IOUtils.getURL;
import static moe.yushi.authlibinjector.util.IOUtils.newUncheckedIOException;
import static moe.yushi.authlibinjector.util.IOUtils.postURL;
import static moe.yushi.authlibinjector.util.JsonUtils.asJsonArray;
import static moe.yushi.authlibinjector.util.JsonUtils.asJsonObject;
import static moe.yushi.authlibinjector.util.JsonUtils.asJsonString;
import static moe.yushi.authlibinjector.util.JsonUtils.parseJson;
import static moe.yushi.authlibinjector.util.UUIDUtils.fromUnsignedUUID;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import moe.yushi.authlibinjector.internal.org.json.simple.JSONArray;
import moe.yushi.authlibinjector.internal.org.json.simple.JSONObject;
import moe.yushi.authlibinjector.util.Logging;
import moe.yushi.authlibinjector.yggdrasil.GameProfile.PropertyValue;
public class YggdrasilClient {
private YggdrasilAPIProvider apiProvider;
public YggdrasilClient(YggdrasilAPIProvider apiProvider) {
this.apiProvider = apiProvider;
}
public Map<String, UUID> queryUUIDs(Set<String> names) throws UncheckedIOException {
String responseText;
try {
responseText = asString(postURL(
apiProvider.queryUUIDsByNames(), CONTENT_TYPE_JSON,
JSONArray.toJSONString(names).getBytes(UTF_8)));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
Logging.HTTPD.fine("Query UUIDs of " + names + " at [" + apiProvider + "], response: " + responseText);
Map<String, UUID> result = new LinkedHashMap<>();
for (Object rawProfile : asJsonArray(parseJson(responseText))) {
JSONObject profile = asJsonObject(rawProfile);
result.put(
asJsonString(profile.get("name")),
parseUnsignedUUID(asJsonString(profile.get("id"))));
}
return result;
}
public Optional<UUID> queryUUID(String name) throws UncheckedIOException {
return Optional.ofNullable(queryUUIDs(singleton(name)).get(name));
}
public Optional<GameProfile> queryProfile(UUID uuid, boolean withSignature) throws UncheckedIOException {
String url = apiProvider.queryProfile(uuid);
if (withSignature) {
url += "?unsigned=false";
}
String responseText;
try {
responseText = asString(getURL(url));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
if (responseText.isEmpty()) {
Logging.HTTPD.fine("Query profile of [" + uuid + "] at [" + apiProvider + "], not found");
return Optional.empty();
}
Logging.HTTPD.fine("Query profile of [" + uuid + "] at [" + apiProvider + "], response: " + responseText);
return Optional.of(parseGameProfile(asJsonObject(parseJson(responseText))));
}
private GameProfile parseGameProfile(JSONObject json) {
GameProfile profile = new GameProfile();
profile.id = parseUnsignedUUID(asJsonString(json.get("id")));
profile.name = asJsonString(json.get("name"));
profile.properties = new LinkedHashMap<>();
for (Object rawProperty : asJsonArray(json.get("properties"))) {
JSONObject property = (JSONObject) rawProperty;
PropertyValue entry = new PropertyValue();
entry.value = asJsonString(property.get("value"));
if (property.containsKey("signature")) {
entry.signature = asJsonString(property.get("signature"));
}
profile.properties.put(asJsonString(property.get("name")), entry);
}
return profile;
}
private UUID parseUnsignedUUID(String uuid) throws UncheckedIOException {
try {
return fromUnsignedUUID(uuid);
} catch (IllegalArgumentException e) {
throw newUncheckedIOException(e.getMessage());
}
}
}

View file

@ -0,0 +1,45 @@
package moe.yushi.authlibinjector.yggdrasil;
import static moe.yushi.authlibinjector.util.UUIDUtils.toUnsignedUUID;
import java.util.Map;
import java.util.UUID;
import moe.yushi.authlibinjector.internal.org.json.simple.JSONArray;
import moe.yushi.authlibinjector.internal.org.json.simple.JSONObject;
public final class YggdrasilResponseBuilder {
private YggdrasilResponseBuilder() {
}
public static String queryUUIDs(Map<String, UUID> result) {
JSONArray response = new JSONArray();
result.forEach((name, uuid) -> {
JSONObject entry = new JSONObject();
entry.put("id", toUnsignedUUID(uuid));
entry.put("name", name);
response.add(entry);
});
return response.toJSONString();
}
public static String queryProfile(GameProfile profile, boolean withSignature) {
JSONObject response = new JSONObject();
response.put("id", toUnsignedUUID(profile.id));
response.put("name", profile.name);
JSONArray properties = new JSONArray();
profile.properties.forEach((name, value) -> {
JSONObject entry = new JSONObject();
entry.put("name", name);
entry.put("value", value.value);
if (withSignature && value.signature != null) {
entry.put("signature", value.signature);
}
properties.add(entry);
});
response.put("properties", properties);
return response.toJSONString();
}
}

View file

@ -1,12 +1,26 @@
Copyright (c) 2012-2013 by Paul S. Hawke, 2001,2005-2013 by Jarno Elonen, 2010 by Konstantinos Togias
All rights reserved.
Copyright (c) 2012 - 2016, nanohttpd
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the NanoHttpd organization nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
3. Neither the name of the nanohttpd nor the names of its contributors
may be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,158 @@
package moe.yushi.authlibinjector.internal.fi.iki.elonen;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static moe.yushi.authlibinjector.util.IOUtils.asBytes;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import org.junit.Test;
@SuppressWarnings("resource")
public class ChunkedInputStreamTest {
@Test
public void testRead1() throws IOException {
byte[] data = ("4\r\nWiki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
assertArrayEquals(("Wikipedia in\r\n\r\nchunks.").getBytes(US_ASCII), asBytes(in));
assertEquals(underlying.read(), -1);
}
@Test
public void testRead2() throws IOException {
byte[] data = ("4\r\nWiki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n.").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
assertArrayEquals(("Wikipedia in\r\n\r\nchunks.").getBytes(US_ASCII), asBytes(in));
assertEquals(underlying.read(), '.');
}
@Test
public void testRead3() throws IOException {
byte[] data = ("25\r\nThis is the data in the first chunk\r\n\r\n1c\r\nand this is the second one\r\n\r\n3\r\ncon\r\n8\r\nsequence\r\n0\r\n\r\n").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
assertArrayEquals(("This is the data in the first chunk\r\nand this is the second one\r\nconsequence").getBytes(US_ASCII), asBytes(in));
assertEquals(underlying.read(), -1);
}
@Test
public void testRead4() throws IOException {
byte[] data = ("25\r\nThis is the data in the first chunk\r\n\r\n1C\r\nand this is the second one\r\n\r\n3\r\ncon\r\n8\r\nsequence\r\n0\r\n\r\n.").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
assertArrayEquals(("This is the data in the first chunk\r\nand this is the second one\r\nconsequence").getBytes(US_ASCII), asBytes(in));
assertEquals(underlying.read(), '.');
}
@Test
public void testRead5() throws IOException {
byte[] data = ("0\r\n\r\n").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
assertArrayEquals(new byte[0], asBytes(in));
assertEquals(underlying.read(), -1);
}
@Test(expected = EOFException.class)
public void testReadEOF1() throws IOException {
byte[] data = ("a").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
asBytes(in);
}
@Test(expected = EOFException.class)
public void testReadEOF2() throws IOException {
byte[] data = ("a\r").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
asBytes(in);
}
@Test(expected = EOFException.class)
public void testReadEOF3() throws IOException {
byte[] data = ("a\r\n").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
asBytes(in);
}
@Test(expected = EOFException.class)
public void testReadEOF4() throws IOException {
byte[] data = ("a\r\nabc").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
asBytes(in);
}
@Test(expected = EOFException.class)
public void testReadEOF5() throws IOException {
byte[] data = ("a\r\n123456789a\r").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
asBytes(in);
}
@Test(expected = EOFException.class)
public void testReadEOF6() throws IOException {
byte[] data = ("a\r\n123456789a\r\n").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
asBytes(in);
}
@Test(expected = EOFException.class)
public void testReadEOF7() throws IOException {
byte[] data = ("a\r\n123456789a\r\n0\r\n\r").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
asBytes(in);
}
@Test(expected = IOException.class)
public void testBadIn1() throws IOException {
byte[] data = ("-1").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
asBytes(in);
}
@Test(expected = IOException.class)
public void testBadIn2() throws IOException {
byte[] data = ("a\ra").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
asBytes(in);
}
@Test(expected = IOException.class)
public void testBadIn3() throws IOException {
byte[] data = ("a\r\n123456789aa").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
asBytes(in);
}
@Test(expected = IOException.class)
public void testBadIn4() throws IOException {
byte[] data = ("a\r\n123456789a\ra").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
asBytes(in);
}
@Test(expected = IOException.class)
public void testBadIn5() throws IOException {
byte[] data = ("a\r\n123456789a\r\n0\r\n\r-").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
asBytes(in);
}
}

View file

@ -0,0 +1,51 @@
package moe.yushi.authlibinjector.internal.fi.iki.elonen;
import static moe.yushi.authlibinjector.util.IOUtils.asBytes;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import org.junit.Test;
@SuppressWarnings("resource")
public class FixedLengthInputStreamTest {
@Test
public void testRead1() throws IOException {
byte[] data = new byte[] { 0x11, 0x22, 0x33, 0x44, 0x55 };
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new FixedLengthInputStream(underlying, 5);
assertArrayEquals(data, asBytes(in));
assertEquals(underlying.read(), -1);
}
@Test
public void testRead2() throws IOException {
byte[] data = new byte[] { 0x11, 0x22, 0x33, 0x44, 0x55 };
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new FixedLengthInputStream(underlying, 4);
assertArrayEquals(Arrays.copyOf(data, 4), asBytes(in));
assertEquals(underlying.read(), 0x55);
}
@Test
public void testRead3() throws IOException {
byte[] data = new byte[] { 0x11 };
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new FixedLengthInputStream(underlying, 0);
assertArrayEquals(new byte[0], asBytes(in));
assertEquals(underlying.read(), 0x11);
}
@Test(expected = EOFException.class)
public void testReadEOF() throws IOException {
byte[] data = new byte[] { 0x11, 0x22, 0x33, 0x44, 0x55 };
InputStream in = new FixedLengthInputStream(new ByteArrayInputStream(data), 6);
asBytes(in);
}
}

View file

@ -0,0 +1,90 @@
package moe.yushi.authlibinjector.test;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static org.junit.Assert.assertEquals;
import java.util.Optional;
import org.junit.Test;
import moe.yushi.authlibinjector.YggdrasilConfiguration;
import moe.yushi.authlibinjector.httpd.DefaultURLRedirector;
public class DefaultURLRedirectorTest {
private String apiRoot = "https://yggdrasil.example.com/";
private DefaultURLRedirector redirector = new DefaultURLRedirector(new YggdrasilConfiguration(apiRoot, emptyList(), emptyMap(), Optional.empty()));
private void testTransform(String domain, String path, String output) {
assertEquals(redirector.redirect(domain, path).get(), output);
}
@Test
public void testReplace() {
testTransform(
// from: [com.mojang:authlib:1.5.24]/com.mojang.authlib.yggdrasil.YggdrasilGameProfileRepository
"api.mojang.com", "/profiles/",
"https://yggdrasil.example.com/api/profiles/");
testTransform(
// from: [com.mojang:authlib:1.5.24]/com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService
"sessionserver.mojang.com", "/session/minecraft/join",
"https://yggdrasil.example.com/sessionserver/session/minecraft/join");
testTransform(
// from: [com.mojang:authlib:1.5.24]/com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService
"sessionserver.mojang.com", "/session/minecraft/hasJoined",
"https://yggdrasil.example.com/sessionserver/session/minecraft/hasJoined");
testTransform(
// from: [com.mojang:authlib:1.5.24]/com.mojang.authlib.yggdrasil.YggdrasilUserAuthentication
"authserver.mojang.com", "/authenticate",
"https://yggdrasil.example.com/authserver/authenticate");
testTransform(
// from: [com.mojang:authlib:1.5.24]/com.mojang.authlib.yggdrasil.YggdrasilUserAuthentication
"authserver.mojang.com", "/refresh",
"https://yggdrasil.example.com/authserver/refresh");
testTransform(
// from: [com.mojang:authlib:1.5.24]/com.mojang.authlib.yggdrasil.YggdrasilUserAuthentication
"authserver.mojang.com", "/validate",
"https://yggdrasil.example.com/authserver/validate");
testTransform(
// from: [com.mojang:authlib:1.5.24]/com.mojang.authlib.yggdrasil.YggdrasilUserAuthentication
"authserver.mojang.com", "/invalidate",
"https://yggdrasil.example.com/authserver/invalidate");
testTransform(
// from: [com.mojang:authlib:1.5.24]/com.mojang.authlib.yggdrasil.YggdrasilUserAuthentication
"authserver.mojang.com", "/signout",
"https://yggdrasil.example.com/authserver/signout");
testTransform(
// from: [mcp940]/net.minecraft.client.entity.AbstractClientPlayer
// issue: yushijinhun/authlib-injector#7 <https://github.com/yushijinhun/authlib-injector/issues/7>
"skins.minecraft.net", "/MinecraftSkins/%s.png",
"https://yggdrasil.example.com/skins/MinecraftSkins/%s.png");
testTransform(
// from: [bungeecord@806a6dfacaadb7538860889f8a50612bb496a2d3]/net.md_5.bungee.connection.InitialHandler
// url: https://github.com/SpigotMC/BungeeCord/blob/806a6dfacaadb7538860889f8a50612bb496a2d3/proxy/src/main/java/net/md_5/bungee/connection/InitialHandler.java#L409
"sessionserver.mojang.com", "/session/minecraft/hasJoined?username=",
"https://yggdrasil.example.com/sessionserver/session/minecraft/hasJoined?username=");
testTransform(
// from: [wiki.vg]/Mojang_API/Username -> UUID at time
// url: http://wiki.vg/Mojang_API#Username_-.3E_UUID_at_time
// issue: yushijinhun/authlib-injector#6 <https://github.com/yushijinhun/authlib-injector/issues/6>
"api.mojang.com", "/users/profiles/minecraft/",
"https://yggdrasil.example.com/api/users/profiles/minecraft/");
}
@Test
public void testEmpty() {
assertEquals(redirector.redirect("example.com", "/path"), Optional.empty());
}
}

View file

@ -1,97 +0,0 @@
package moe.yushi.authlibinjector.test;
import static org.junit.Assert.assertEquals;
import java.util.Arrays;
import java.util.Collection;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
import moe.yushi.authlibinjector.transform.RemoteYggdrasilTransformUnit;
@RunWith(Parameterized.class)
public class UrlReplaceTest {
private static final String apiRoot = "https://yggdrasil.example.com/";
@Parameters
public static Collection<Object[]> data() {
// @formatter:off
return Arrays.asList(new Object[][] {
{
// from: [com.mojang:authlib:1.5.24]/com.mojang.authlib.yggdrasil.YggdrasilGameProfileRepository
"https://api.mojang.com/profiles/",
"https://yggdrasil.example.com/api/profiles/"
},
{
// from: [com.mojang:authlib:1.5.24]/com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService
"https://sessionserver.mojang.com/session/minecraft/join",
"https://yggdrasil.example.com/sessionserver/session/minecraft/join"
},
{
// from: [com.mojang:authlib:1.5.24]/com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService
"https://sessionserver.mojang.com/session/minecraft/hasJoined",
"https://yggdrasil.example.com/sessionserver/session/minecraft/hasJoined"
},
{
// from: [com.mojang:authlib:1.5.24]/com.mojang.authlib.yggdrasil.YggdrasilUserAuthentication
"https://authserver.mojang.com/authenticate",
"https://yggdrasil.example.com/authserver/authenticate"
},
{
// from: [com.mojang:authlib:1.5.24]/com.mojang.authlib.yggdrasil.YggdrasilUserAuthentication
"https://authserver.mojang.com/refresh",
"https://yggdrasil.example.com/authserver/refresh"
},
{
// from: [com.mojang:authlib:1.5.24]/com.mojang.authlib.yggdrasil.YggdrasilUserAuthentication
"https://authserver.mojang.com/validate",
"https://yggdrasil.example.com/authserver/validate"
},
{
// from: [com.mojang:authlib:1.5.24]/com.mojang.authlib.yggdrasil.YggdrasilUserAuthentication
"https://authserver.mojang.com/invalidate",
"https://yggdrasil.example.com/authserver/invalidate"
},
{
// from: [com.mojang:authlib:1.5.24]/com.mojang.authlib.yggdrasil.YggdrasilUserAuthentication
"https://authserver.mojang.com/signout",
"https://yggdrasil.example.com/authserver/signout"
},
{
// from: [mcp940]/net.minecraft.client.entity.AbstractClientPlayer
// issue: yushijinhun/authlib-injector#7 <https://github.com/yushijinhun/authlib-injector/issues/7>
"http://skins.minecraft.net/MinecraftSkins/%s.png",
"https://yggdrasil.example.com/skins/MinecraftSkins/%s.png"
},
{
// from: [bungeecord@806a6dfacaadb7538860889f8a50612bb496a2d3]/net.md_5.bungee.connection.InitialHandler
// url: https://github.com/SpigotMC/BungeeCord/blob/806a6dfacaadb7538860889f8a50612bb496a2d3/proxy/src/main/java/net/md_5/bungee/connection/InitialHandler.java#L409
"https://sessionserver.mojang.com/session/minecraft/hasJoined?username=",
"https://yggdrasil.example.com/sessionserver/session/minecraft/hasJoined?username="
},
{
// from: [wiki.vg]/Mojang_API/Username -> UUID at time
// url: http://wiki.vg/Mojang_API#Username_-.3E_UUID_at_time
// issue: yushijinhun/authlib-injector#6 <https://github.com/yushijinhun/authlib-injector/issues/6>
"https://api.mojang.com/users/profiles/minecraft/",
"https://yggdrasil.example.com/api/users/profiles/minecraft/"
}
});
// @formatter:on
}
@Parameter(0)
public String input;
@Parameter(1)
public String output;
@Test
public void test() {
RemoteYggdrasilTransformUnit transformer = new RemoteYggdrasilTransformUnit(apiRoot);
assertEquals(output, transformer.transformLdc(input).get());
}
}