forked from MirrorHub/authlib-injector
Merge pull request #28 from yushijinhun/citizens-support
支持Citizens2及@mojang后缀
This commit is contained in:
commit
ead8866a40
43 changed files with 2967 additions and 492 deletions
|
@ -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'
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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")));
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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")));
|
||||
}
|
||||
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
30
src/main/java/moe/yushi/authlibinjector/httpd/URLFilter.java
Normal file
30
src/main/java/moe/yushi/authlibinjector/httpd/URLFilter.java
Normal 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;
|
||||
}
|
215
src/main/java/moe/yushi/authlibinjector/httpd/URLProcessor.java
Normal file
215
src/main/java/moe/yushi/authlibinjector/httpd/URLProcessor.java
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package moe.yushi.authlibinjector.internal.fi.iki.elonen;
|
||||
|
||||
public interface IStatus {
|
||||
|
||||
String getDescription();
|
||||
|
||||
int getRequestStatus();
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
20
src/main/java/moe/yushi/authlibinjector/util/UUIDUtils.java
Normal file
20
src/main/java/moe/yushi/authlibinjector/util/UUIDUtils.java
Normal 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() {
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package moe.yushi.authlibinjector.yggdrasil;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public interface YggdrasilAPIProvider {
|
||||
String queryUUIDsByNames();
|
||||
String queryProfile(UUID uuid);
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue