From ffd2a94b3d624c19b0d336ba4ece938346f404a8 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Sun, 23 Dec 2018 15:24:07 +0800 Subject: [PATCH 01/16] Support multi signature keys --- .../authlibinjector/AuthlibInjector.java | 4 +- .../transform/YggdrasilKeyTransformUnit.java | 142 +++++++++++------- 2 files changed, 93 insertions(+), 53 deletions(-) diff --git a/src/main/java/moe/yushi/authlibinjector/AuthlibInjector.java b/src/main/java/moe/yushi/authlibinjector/AuthlibInjector.java index 4100c32..de8333f 100644 --- a/src/main/java/moe/yushi/authlibinjector/AuthlibInjector.java +++ b/src/main/java/moe/yushi/authlibinjector/AuthlibInjector.java @@ -290,8 +290,8 @@ public final class AuthlibInjector { 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; } diff --git a/src/main/java/moe/yushi/authlibinjector/transform/YggdrasilKeyTransformUnit.java b/src/main/java/moe/yushi/authlibinjector/transform/YggdrasilKeyTransformUnit.java index f8aebf8..77f0007 100644 --- a/src/main/java/moe/yushi/authlibinjector/transform/YggdrasilKeyTransformUnit.java +++ b/src/main/java/moe/yushi/authlibinjector/transform/YggdrasilKeyTransformUnit.java @@ -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 PUBLIC_KEYS = new CopyOnWriteArrayList<>(); - public YggdrasilKeyTransformUnit(byte[] publicKey) { - this.publicKey = publicKey; + public static List 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 ("".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"; } - } From 15f766ab29a916a71e00c085aa11e94cab8cdf7e Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Sat, 29 Dec 2018 20:39:49 +0800 Subject: [PATCH 02/16] Refactor URL replacing --- .../authlibinjector/AuthlibInjector.java | 37 +++--- .../httpd/DefaultURLRedirector.java | 37 ++++++ ...silHttpd.java => LegacySkinAPIFilter.java} | 32 +++--- .../httpd/LocalYggdrasilHandle.java | 44 ------- .../authlibinjector/httpd/URLFilter.java | 13 +++ .../authlibinjector/httpd/URLProcessor.java | 108 ++++++++++++++++++ .../authlibinjector/httpd/URLRedirector.java | 7 ++ .../transform/ConstantURLTransformUnit.java | 24 ++++ .../transform/DomainBasedTransformUnit.java | 38 ------ .../LocalYggdrasilApiTransformUnit.java | 34 ------ .../RemoteYggdrasilTransformUnit.java | 28 ----- .../yushi/authlibinjector/util/IOUtils.java | 2 + .../test/DefaultURLRedirectorTest.java | 90 +++++++++++++++ .../authlibinjector/test/UrlReplaceTest.java | 97 ---------------- 14 files changed, 322 insertions(+), 269 deletions(-) create mode 100644 src/main/java/moe/yushi/authlibinjector/httpd/DefaultURLRedirector.java rename src/main/java/moe/yushi/authlibinjector/httpd/{LocalYggdrasilHttpd.java => LegacySkinAPIFilter.java} (86%) delete mode 100644 src/main/java/moe/yushi/authlibinjector/httpd/LocalYggdrasilHandle.java create mode 100644 src/main/java/moe/yushi/authlibinjector/httpd/URLFilter.java create mode 100644 src/main/java/moe/yushi/authlibinjector/httpd/URLProcessor.java create mode 100644 src/main/java/moe/yushi/authlibinjector/httpd/URLRedirector.java create mode 100644 src/main/java/moe/yushi/authlibinjector/transform/ConstantURLTransformUnit.java delete mode 100644 src/main/java/moe/yushi/authlibinjector/transform/DomainBasedTransformUnit.java delete mode 100644 src/main/java/moe/yushi/authlibinjector/transform/LocalYggdrasilApiTransformUnit.java delete mode 100644 src/main/java/moe/yushi/authlibinjector/transform/RemoteYggdrasilTransformUnit.java create mode 100644 src/test/java/moe/yushi/authlibinjector/test/DefaultURLRedirectorTest.java delete mode 100644 src/test/java/moe/yushi/authlibinjector/test/UrlReplaceTest.java diff --git a/src/main/java/moe/yushi/authlibinjector/AuthlibInjector.java b/src/main/java/moe/yushi/authlibinjector/AuthlibInjector.java index de8333f..664b125 100644 --- a/src/main/java/moe/yushi/authlibinjector/AuthlibInjector.java +++ b/src/main/java/moe/yushi/authlibinjector/AuthlibInjector.java @@ -14,17 +14,22 @@ 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.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.util.Logging; @@ -54,11 +59,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. */ @@ -267,9 +267,22 @@ public final class AuthlibInjector { } } - private static ClassTransformer createTransformer(YggdrasilConfiguration config) { - ClassTransformer transformer = new ClassTransformer(); + private static URLProcessor createURLProcessor(YggdrasilConfiguration config) { + List filters = new ArrayList<>(); + 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(config)); + } + + return new URLProcessor(filters, new DefaultURLRedirector(config)); + } + + private static ClassTransformer createTransformer(YggdrasilConfiguration config) { + URLProcessor urlProcessor = createURLProcessor(config); + + ClassTransformer transformer = new ClassTransformer(); for (String ignore : nonTransformablePackages) { transformer.ignores.add(ignore); } @@ -282,11 +295,7 @@ 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 SkinWhitelistTransformUnit(config.getSkinDomains().toArray(new String[0]))); diff --git a/src/main/java/moe/yushi/authlibinjector/httpd/DefaultURLRedirector.java b/src/main/java/moe/yushi/authlibinjector/httpd/DefaultURLRedirector.java new file mode 100644 index 0000000..f7be300 --- /dev/null +++ b/src/main/java/moe/yushi/authlibinjector/httpd/DefaultURLRedirector.java @@ -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 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 redirect(String domain, String path) { + String subdirectory = domainMapping.get(domain); + if (subdirectory == null) { + return Optional.empty(); + } + + return Optional.of(apiRoot + subdirectory + path); + } + +} diff --git a/src/main/java/moe/yushi/authlibinjector/httpd/LocalYggdrasilHttpd.java b/src/main/java/moe/yushi/authlibinjector/httpd/LegacySkinAPIFilter.java similarity index 86% rename from src/main/java/moe/yushi/authlibinjector/httpd/LocalYggdrasilHttpd.java rename to src/main/java/moe/yushi/authlibinjector/httpd/LegacySkinAPIFilter.java index 0793a89..32a15c7 100644 --- a/src/main/java/moe/yushi/authlibinjector/httpd/LocalYggdrasilHttpd.java +++ b/src/main/java/moe/yushi/authlibinjector/httpd/LegacySkinAPIFilter.java @@ -1,10 +1,12 @@ package moe.yushi.authlibinjector.httpd; +import static fi.iki.elonen.NanoHTTPD.newFixedLengthResponse; 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.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; @@ -13,6 +15,7 @@ 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; @@ -21,7 +24,9 @@ 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.IHTTPSession; +import fi.iki.elonen.NanoHTTPD.Response; import fi.iki.elonen.NanoHTTPD.Response.Status; import moe.yushi.authlibinjector.YggdrasilConfiguration; import moe.yushi.authlibinjector.internal.org.json.simple.JSONArray; @@ -29,28 +34,28 @@ 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 class LegacySkinAPIFilter implements URLFilter { - public static final String CONTENT_TYPE_JSON = "application/json; charset=utf-8"; - - private static final Pattern URL_SKINS = Pattern.compile("^/skins/MinecraftSkins/(?[^/]+)\\.png$"); + private static final Pattern PATH_SKINS = Pattern.compile("^/MinecraftSkins/(?[^/]+)\\.png$"); private YggdrasilConfiguration configuration; - public LocalYggdrasilHttpd(int port, YggdrasilConfiguration configuration) { - super("127.0.0.1", port); + public LegacySkinAPIFilter(YggdrasilConfiguration configuration) { this.configuration = configuration; } @Override - public Response serve(IHTTPSession session) { - return processAsSkin(session) - .orElseGet(() -> super.serve(session)); + public boolean canHandle(String domain, String path) { + return domain.equals("skins.minecraft.net"); } - private Optional processAsSkin(IHTTPSession session) { - Matcher matcher = URL_SKINS.matcher(session.getUri()); - if (!matcher.find()) return empty(); + @Override + public Optional 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 skinUrl; @@ -138,5 +143,4 @@ public class LocalYggdrasilHttpd extends NanoHTTPD { .map(JsonUtils::asJsonString) .orElseThrow(() -> newUncheckedIOException("Invalid JSON: Missing texture url"))); } - } diff --git a/src/main/java/moe/yushi/authlibinjector/httpd/LocalYggdrasilHandle.java b/src/main/java/moe/yushi/authlibinjector/httpd/LocalYggdrasilHandle.java deleted file mode 100644 index c50393f..0000000 --- a/src/main/java/moe/yushi/authlibinjector/httpd/LocalYggdrasilHandle.java +++ /dev/null @@ -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(); - } - -} diff --git a/src/main/java/moe/yushi/authlibinjector/httpd/URLFilter.java b/src/main/java/moe/yushi/authlibinjector/httpd/URLFilter.java new file mode 100644 index 0000000..d6ae9b2 --- /dev/null +++ b/src/main/java/moe/yushi/authlibinjector/httpd/URLFilter.java @@ -0,0 +1,13 @@ +package moe.yushi.authlibinjector.httpd; + +import java.util.Optional; + +import fi.iki.elonen.NanoHTTPD.IHTTPSession; +import fi.iki.elonen.NanoHTTPD.Response; + +public interface URLFilter { + + boolean canHandle(String domain, String path); + + Optional handle(String domain, String path, IHTTPSession session); +} diff --git a/src/main/java/moe/yushi/authlibinjector/httpd/URLProcessor.java b/src/main/java/moe/yushi/authlibinjector/httpd/URLProcessor.java new file mode 100644 index 0000000..7e63c8f --- /dev/null +++ b/src/main/java/moe/yushi/authlibinjector/httpd/URLProcessor.java @@ -0,0 +1,108 @@ +package moe.yushi.authlibinjector.httpd; + +import static fi.iki.elonen.NanoHTTPD.Response.Status.INTERNAL_ERROR; +import static fi.iki.elonen.NanoHTTPD.Response.Status.NOT_FOUND; +import java.io.IOException; +import java.util.List; +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 moe.yushi.authlibinjector.util.Logging; + +public class URLProcessor { + + private static final Pattern URL_REGEX = Pattern.compile("^https?:\\/\\/(?[^\\/]+)(?\\/.*)$"); + private static final Pattern LOCAL_URL_REGEX = Pattern.compile("^/(?[^\\/]+)(?\\/.*)$"); + + private List filters; + private URLRedirector redirector; + + public URLProcessor(List filters, URLRedirector redirector) { + this.filters = filters; + this.redirector = redirector; + } + + public Optional transformURL(String inputUrl) { + Matcher matcher = URL_REGEX.matcher(inputUrl); + if (!matcher.find()) { + return Optional.empty(); + } + String domain = matcher.group("domain"); + String path = matcher.group("path"); + + Optional result = transform(domain, path); + if (result.isPresent()) { + Logging.TRANSFORM.fine("Transformed url [" + inputUrl + "] to [" + result.get() + "]"); + } + return result; + } + + private Optional transform(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() + "/" + 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 domain = matcher.group("domain"); + String path = matcher.group("path"); + for (URLFilter filter : filters) { + if (filter.canHandle(domain, path)) { + Optional 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 newFixedLengthResponse(INTERNAL_ERROR, null, null); + } + + if (result.isPresent()) { + Logging.HTTPD.fine("Request to [" + session.getUri() + "] is handled by [" + filter + "]"); + return result.get(); + } + } + } + } + + Logging.HTTPD.fine("No handler is found for [" + session.getUri() + "]"); + return newFixedLengthResponse(NOT_FOUND, MIME_PLAINTEXT, "Not Found"); + } + }; + } +} diff --git a/src/main/java/moe/yushi/authlibinjector/httpd/URLRedirector.java b/src/main/java/moe/yushi/authlibinjector/httpd/URLRedirector.java new file mode 100644 index 0000000..6688f64 --- /dev/null +++ b/src/main/java/moe/yushi/authlibinjector/httpd/URLRedirector.java @@ -0,0 +1,7 @@ +package moe.yushi.authlibinjector.httpd; + +import java.util.Optional; + +public interface URLRedirector { + Optional redirect(String domain, String path); +} diff --git a/src/main/java/moe/yushi/authlibinjector/transform/ConstantURLTransformUnit.java b/src/main/java/moe/yushi/authlibinjector/transform/ConstantURLTransformUnit.java new file mode 100644 index 0000000..9a824aa --- /dev/null +++ b/src/main/java/moe/yushi/authlibinjector/transform/ConstantURLTransformUnit.java @@ -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 transformLdc(String input) { + return urlProcessor.transformURL(input); + } + + @Override + public String toString() { + return "Constant URL Transformer"; + } +} diff --git a/src/main/java/moe/yushi/authlibinjector/transform/DomainBasedTransformUnit.java b/src/main/java/moe/yushi/authlibinjector/transform/DomainBasedTransformUnit.java deleted file mode 100644 index 704ca36..0000000 --- a/src/main/java/moe/yushi/authlibinjector/transform/DomainBasedTransformUnit.java +++ /dev/null @@ -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?:\\/\\/(?[^\\/]+)(?\\/.*)$"); - - private Map domainMapping = new ConcurrentHashMap<>(); - - public Map getDomainMapping() { - return domainMapping; - } - - @Override - public Optional 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(); -} diff --git a/src/main/java/moe/yushi/authlibinjector/transform/LocalYggdrasilApiTransformUnit.java b/src/main/java/moe/yushi/authlibinjector/transform/LocalYggdrasilApiTransformUnit.java deleted file mode 100644 index f2dc9c5..0000000 --- a/src/main/java/moe/yushi/authlibinjector/transform/LocalYggdrasilApiTransformUnit.java +++ /dev/null @@ -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 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"; - } -} diff --git a/src/main/java/moe/yushi/authlibinjector/transform/RemoteYggdrasilTransformUnit.java b/src/main/java/moe/yushi/authlibinjector/transform/RemoteYggdrasilTransformUnit.java deleted file mode 100644 index e4efdc0..0000000 --- a/src/main/java/moe/yushi/authlibinjector/transform/RemoteYggdrasilTransformUnit.java +++ /dev/null @@ -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 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"; - } -} diff --git a/src/main/java/moe/yushi/authlibinjector/util/IOUtils.java b/src/main/java/moe/yushi/authlibinjector/util/IOUtils.java index 52407de..b8ed853 100644 --- a/src/main/java/moe/yushi/authlibinjector/util/IOUtils.java +++ b/src/main/java/moe/yushi/authlibinjector/util/IOUtils.java @@ -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); diff --git a/src/test/java/moe/yushi/authlibinjector/test/DefaultURLRedirectorTest.java b/src/test/java/moe/yushi/authlibinjector/test/DefaultURLRedirectorTest.java new file mode 100644 index 0000000..25b8f14 --- /dev/null +++ b/src/test/java/moe/yushi/authlibinjector/test/DefaultURLRedirectorTest.java @@ -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 + "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 + "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()); + } + +} diff --git a/src/test/java/moe/yushi/authlibinjector/test/UrlReplaceTest.java b/src/test/java/moe/yushi/authlibinjector/test/UrlReplaceTest.java deleted file mode 100644 index b7c42bd..0000000 --- a/src/test/java/moe/yushi/authlibinjector/test/UrlReplaceTest.java +++ /dev/null @@ -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 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 - "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://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()); - } - -} From 5531c8a06e15606f70fa7428013bcdc4e721e13e Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Sun, 30 Dec 2018 00:40:49 +0800 Subject: [PATCH 03/16] Support Citizens2 --- .../authlibinjector/AuthlibInjector.java | 2 + .../support/CitizensTransformer.java | 64 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 src/main/java/moe/yushi/authlibinjector/transform/support/CitizensTransformer.java diff --git a/src/main/java/moe/yushi/authlibinjector/AuthlibInjector.java b/src/main/java/moe/yushi/authlibinjector/AuthlibInjector.java index 664b125..af2445d 100644 --- a/src/main/java/moe/yushi/authlibinjector/AuthlibInjector.java +++ b/src/main/java/moe/yushi/authlibinjector/AuthlibInjector.java @@ -31,6 +31,7 @@ import moe.yushi.authlibinjector.transform.ConstantURLTransformUnit; import moe.yushi.authlibinjector.transform.DumpClassListener; import moe.yushi.authlibinjector.transform.SkinWhitelistTransformUnit; import moe.yushi.authlibinjector.transform.YggdrasilKeyTransformUnit; +import moe.yushi.authlibinjector.transform.support.CitizensTransformer; import moe.yushi.authlibinjector.util.Logging; public final class AuthlibInjector { @@ -296,6 +297,7 @@ public final class AuthlibInjector { } transformer.units.add(new ConstantURLTransformUnit(urlProcessor)); + transformer.units.add(new CitizensTransformer()); transformer.units.add(new SkinWhitelistTransformUnit(config.getSkinDomains().toArray(new String[0]))); diff --git a/src/main/java/moe/yushi/authlibinjector/transform/support/CitizensTransformer.java b/src/main/java/moe/yushi/authlibinjector/transform/support/CitizensTransformer.java new file mode 100644 index 0000000..acdc26a --- /dev/null +++ b/src/main/java/moe/yushi/authlibinjector/transform/support/CitizensTransformer.java @@ -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 , + * 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 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"; + } +} From 3572ff8eb5e78d688669281d549caecbad7f8cb2 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Sun, 30 Dec 2018 14:25:12 +0800 Subject: [PATCH 04/16] Add YggdrasilClient --- .../httpd/LegacySkinAPIFilter.java | 72 ++---------- .../yushi/authlibinjector/util/UUIDUtils.java | 20 ++++ .../yggdrasil/CustomYggdrasilAPIProvider.java | 31 +++++ .../yggdrasil/GameProfile.java | 16 +++ .../yggdrasil/MojangYggdrasilAPIProvider.java | 23 ++++ .../yggdrasil/YggdrasilAPIProvider.java | 8 ++ .../yggdrasil/YggdrasilClient.java | 106 ++++++++++++++++++ 7 files changed, 214 insertions(+), 62 deletions(-) create mode 100644 src/main/java/moe/yushi/authlibinjector/util/UUIDUtils.java create mode 100644 src/main/java/moe/yushi/authlibinjector/yggdrasil/CustomYggdrasilAPIProvider.java create mode 100644 src/main/java/moe/yushi/authlibinjector/yggdrasil/GameProfile.java create mode 100644 src/main/java/moe/yushi/authlibinjector/yggdrasil/MojangYggdrasilAPIProvider.java create mode 100644 src/main/java/moe/yushi/authlibinjector/yggdrasil/YggdrasilAPIProvider.java create mode 100644 src/main/java/moe/yushi/authlibinjector/yggdrasil/YggdrasilClient.java diff --git a/src/main/java/moe/yushi/authlibinjector/httpd/LegacySkinAPIFilter.java b/src/main/java/moe/yushi/authlibinjector/httpd/LegacySkinAPIFilter.java index 32a15c7..bbfbe29 100644 --- a/src/main/java/moe/yushi/authlibinjector/httpd/LegacySkinAPIFilter.java +++ b/src/main/java/moe/yushi/authlibinjector/httpd/LegacySkinAPIFilter.java @@ -1,19 +1,13 @@ package moe.yushi.authlibinjector.httpd; import static fi.iki.elonen.NanoHTTPD.newFixedLengthResponse; -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.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 java.io.ByteArrayInputStream; @@ -21,7 +15,6 @@ 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; @@ -29,19 +22,20 @@ import fi.iki.elonen.NanoHTTPD.IHTTPSession; import fi.iki.elonen.NanoHTTPD.Response; 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; +import moe.yushi.authlibinjector.yggdrasil.CustomYggdrasilAPIProvider; +import moe.yushi.authlibinjector.yggdrasil.YggdrasilClient; public class LegacySkinAPIFilter implements URLFilter { private static final Pattern PATH_SKINS = Pattern.compile("^/MinecraftSkins/(?[^/]+)\\.png$"); - private YggdrasilConfiguration configuration; + private YggdrasilClient upstream; public LegacySkinAPIFilter(YggdrasilConfiguration configuration) { - this.configuration = configuration; + this.upstream = new YggdrasilClient(new CustomYggdrasilAPIProvider(configuration)); } @Override @@ -60,13 +54,13 @@ public class LegacySkinAPIFilter implements URLFilter { Optional skinUrl; try { - skinUrl = queryCharacterUUID(username) - .flatMap(uuid -> queryCharacterProperty(uuid, "textures")) - .map(encoded -> asString(Base64.getDecoder().decode(encoded))) + 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) { - Logging.HTTPD.log(Level.WARNING, "Failed to fetch skin for " + username, e); - return of(newFixedLengthResponse(Status.INTERNAL_ERROR, null, null)); + throw newUncheckedIOException("Failed to fetch skin metadata for " + username, e); } if (skinUrl.isPresent()) { @@ -76,8 +70,7 @@ public class LegacySkinAPIFilter implements URLFilter { 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)); + throw newUncheckedIOException("Failed to retrieve skin from " + url, e); } Logging.HTTPD.info("Retrieved skin for " + username + " from " + url + ", " + data.length + " bytes"); return of(newFixedLengthResponse(Status.OK, "image/png", new ByteArrayInputStream(data), data.length)); @@ -88,51 +81,6 @@ public class LegacySkinAPIFilter implements URLFilter { } } - private Optional 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 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 obtainTextureUrl(String texturesPayload, String textureType) throws UncheckedIOException { JSONObject payload = asJsonObject(parseJson(texturesPayload)); JSONObject textures = asJsonObject(payload.get("textures")); diff --git a/src/main/java/moe/yushi/authlibinjector/util/UUIDUtils.java b/src/main/java/moe/yushi/authlibinjector/util/UUIDUtils.java new file mode 100644 index 0000000..7d52c76 --- /dev/null +++ b/src/main/java/moe/yushi/authlibinjector/util/UUIDUtils.java @@ -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() { + } +} diff --git a/src/main/java/moe/yushi/authlibinjector/yggdrasil/CustomYggdrasilAPIProvider.java b/src/main/java/moe/yushi/authlibinjector/yggdrasil/CustomYggdrasilAPIProvider.java new file mode 100644 index 0000000..08ba67c --- /dev/null +++ b/src/main/java/moe/yushi/authlibinjector/yggdrasil/CustomYggdrasilAPIProvider.java @@ -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; + } +} diff --git a/src/main/java/moe/yushi/authlibinjector/yggdrasil/GameProfile.java b/src/main/java/moe/yushi/authlibinjector/yggdrasil/GameProfile.java new file mode 100644 index 0000000..515c8a9 --- /dev/null +++ b/src/main/java/moe/yushi/authlibinjector/yggdrasil/GameProfile.java @@ -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 properties; +} diff --git a/src/main/java/moe/yushi/authlibinjector/yggdrasil/MojangYggdrasilAPIProvider.java b/src/main/java/moe/yushi/authlibinjector/yggdrasil/MojangYggdrasilAPIProvider.java new file mode 100644 index 0000000..eebdc09 --- /dev/null +++ b/src/main/java/moe/yushi/authlibinjector/yggdrasil/MojangYggdrasilAPIProvider.java @@ -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"; + } +} diff --git a/src/main/java/moe/yushi/authlibinjector/yggdrasil/YggdrasilAPIProvider.java b/src/main/java/moe/yushi/authlibinjector/yggdrasil/YggdrasilAPIProvider.java new file mode 100644 index 0000000..814343d --- /dev/null +++ b/src/main/java/moe/yushi/authlibinjector/yggdrasil/YggdrasilAPIProvider.java @@ -0,0 +1,8 @@ +package moe.yushi.authlibinjector.yggdrasil; + +import java.util.UUID; + +public interface YggdrasilAPIProvider { + String queryUUIDsByNames(); + String queryProfile(UUID uuid); +} diff --git a/src/main/java/moe/yushi/authlibinjector/yggdrasil/YggdrasilClient.java b/src/main/java/moe/yushi/authlibinjector/yggdrasil/YggdrasilClient.java new file mode 100644 index 0000000..c509bcc --- /dev/null +++ b/src/main/java/moe/yushi/authlibinjector/yggdrasil/YggdrasilClient.java @@ -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 queryUUIDs(Set 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 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 queryUUID(String name) throws UncheckedIOException { + return Optional.ofNullable(queryUUIDs(singleton(name)).get(name)); + } + + public Optional 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()); + } + } +} From 6dccd97269d43704defbd7a8a9cfb415e28ef020 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Sun, 30 Dec 2018 16:31:08 +0800 Subject: [PATCH 05/16] Import nanohttpd 2.3.1 source --- build.gradle | 7 - .../httpd/LegacySkinAPIFilter.java | 8 +- .../authlibinjector/httpd/URLFilter.java | 4 +- .../authlibinjector/httpd/URLProcessor.java | 7 +- .../internal/fi/iki/elonen/NanoHTTPD.java | 2358 +++++++++++++++++ .../internal/fi/iki/elonen/package-info.java | 6 + .../resources/META-INF/licenses/nanohttpd.txt | 28 +- .../nanohttpd/default-mimetypes.properties | 30 + 8 files changed, 2425 insertions(+), 23 deletions(-) create mode 100644 src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/NanoHTTPD.java create mode 100644 src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/package-info.java create mode 100644 src/main/resources/META-INF/nanohttpd/default-mimetypes.properties diff --git a/build.gradle b/build.gradle index 420dca6..1859ac7 100644 --- a/build.gradle +++ b/build.gradle @@ -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' diff --git a/src/main/java/moe/yushi/authlibinjector/httpd/LegacySkinAPIFilter.java b/src/main/java/moe/yushi/authlibinjector/httpd/LegacySkinAPIFilter.java index bbfbe29..1d8f244 100644 --- a/src/main/java/moe/yushi/authlibinjector/httpd/LegacySkinAPIFilter.java +++ b/src/main/java/moe/yushi/authlibinjector/httpd/LegacySkinAPIFilter.java @@ -1,9 +1,9 @@ package moe.yushi.authlibinjector.httpd; -import static fi.iki.elonen.NanoHTTPD.newFixedLengthResponse; import static java.util.Optional.empty; import static java.util.Optional.of; import static java.util.Optional.ofNullable; +import static moe.yushi.authlibinjector.internal.fi.iki.elonen.NanoHTTPD.newFixedLengthResponse; import static moe.yushi.authlibinjector.util.IOUtils.asString; import static moe.yushi.authlibinjector.util.IOUtils.getURL; import static moe.yushi.authlibinjector.util.IOUtils.newUncheckedIOException; @@ -18,10 +18,10 @@ import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; -import fi.iki.elonen.NanoHTTPD.IHTTPSession; -import fi.iki.elonen.NanoHTTPD.Response; -import fi.iki.elonen.NanoHTTPD.Response.Status; import moe.yushi.authlibinjector.YggdrasilConfiguration; +import moe.yushi.authlibinjector.internal.fi.iki.elonen.NanoHTTPD.IHTTPSession; +import moe.yushi.authlibinjector.internal.fi.iki.elonen.NanoHTTPD.Response; +import moe.yushi.authlibinjector.internal.fi.iki.elonen.NanoHTTPD.Response.Status; import moe.yushi.authlibinjector.internal.org.json.simple.JSONObject; import moe.yushi.authlibinjector.util.JsonUtils; import moe.yushi.authlibinjector.util.Logging; diff --git a/src/main/java/moe/yushi/authlibinjector/httpd/URLFilter.java b/src/main/java/moe/yushi/authlibinjector/httpd/URLFilter.java index d6ae9b2..6f1d32a 100644 --- a/src/main/java/moe/yushi/authlibinjector/httpd/URLFilter.java +++ b/src/main/java/moe/yushi/authlibinjector/httpd/URLFilter.java @@ -2,8 +2,8 @@ package moe.yushi.authlibinjector.httpd; import java.util.Optional; -import fi.iki.elonen.NanoHTTPD.IHTTPSession; -import fi.iki.elonen.NanoHTTPD.Response; +import moe.yushi.authlibinjector.internal.fi.iki.elonen.NanoHTTPD.IHTTPSession; +import moe.yushi.authlibinjector.internal.fi.iki.elonen.NanoHTTPD.Response; public interface URLFilter { diff --git a/src/main/java/moe/yushi/authlibinjector/httpd/URLProcessor.java b/src/main/java/moe/yushi/authlibinjector/httpd/URLProcessor.java index 7e63c8f..0a4821b 100644 --- a/src/main/java/moe/yushi/authlibinjector/httpd/URLProcessor.java +++ b/src/main/java/moe/yushi/authlibinjector/httpd/URLProcessor.java @@ -1,7 +1,8 @@ package moe.yushi.authlibinjector.httpd; -import static fi.iki.elonen.NanoHTTPD.Response.Status.INTERNAL_ERROR; -import static fi.iki.elonen.NanoHTTPD.Response.Status.NOT_FOUND; +import static moe.yushi.authlibinjector.internal.fi.iki.elonen.NanoHTTPD.Response.Status.INTERNAL_ERROR; +import static moe.yushi.authlibinjector.internal.fi.iki.elonen.NanoHTTPD.Response.Status.NOT_FOUND; + import java.io.IOException; import java.util.List; import java.util.Optional; @@ -9,7 +10,7 @@ import java.util.logging.Level; import java.util.regex.Matcher; import java.util.regex.Pattern; -import fi.iki.elonen.NanoHTTPD; +import moe.yushi.authlibinjector.internal.fi.iki.elonen.NanoHTTPD; import moe.yushi.authlibinjector.util.Logging; public class URLProcessor { diff --git a/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/NanoHTTPD.java b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/NanoHTTPD.java new file mode 100644 index 0000000..ecb7761 --- /dev/null +++ b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/NanoHTTPD.java @@ -0,0 +1,2358 @@ +package moe.yushi.authlibinjector.internal.fi.iki.elonen; + +/* + * #%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.BufferedWriter; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.DataOutput; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.RandomAccessFile; +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.URL; +import java.net.URLDecoder; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; +import java.security.KeyStore; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.StringTokenizer; +import java.util.TimeZone; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.zip.GZIPOutputStream; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLServerSocket; +import javax.net.ssl.SSLServerSocketFactory; +import javax.net.ssl.TrustManagerFactory; + +import moe.yushi.authlibinjector.internal.fi.iki.elonen.NanoHTTPD.Response.IStatus; +import moe.yushi.authlibinjector.internal.fi.iki.elonen.NanoHTTPD.Response.Status; + +/** + * A simple, tiny, nicely embeddable HTTP server in Java + *

+ *

+ * NanoHTTPD + *

+ * Copyright (c) 2012-2013 by Paul S. Hawke, 2001,2005-2013 by Jarno Elonen, + * 2010 by Konstantinos Togias + *

+ *

+ *

+ * Features + limitations: + *

    + *

    + *

  • Only one Java file
  • + *
  • Java 5 compatible
  • + *
  • Released as open source, Modified BSD licence
  • + *
  • No fixed config files, logging, authorization etc. (Implement yourself if + * you need them.)
  • + *
  • Supports parameter parsing of GET and POST methods (+ rudimentary PUT + * support in 1.25)
  • + *
  • Supports both dynamic content and file serving
  • + *
  • Supports file upload (since version 1.2, 2010)
  • + *
  • Supports partial content (streaming)
  • + *
  • Supports ETags
  • + *
  • Never caches anything
  • + *
  • Doesn't limit bandwidth, request time or simultaneous connections
  • + *
  • Default code serves files and shows all HTTP parameters and headers
  • + *
  • File server supports directory listing, index.html and index.htm
  • + *
  • File server supports partial content (streaming)
  • + *
  • File server supports ETags
  • + *
  • File server does the 301 redirection trick for directories without '/'
  • + *
  • File server supports simple skipping for files (continue download)
  • + *
  • File server serves also very long files without memory overhead
  • + *
  • Contains a built-in list of most common MIME types
  • + *
  • All header names are converted to lower case so they don't vary between + * browsers/clients
  • + *

    + *

+ *

+ *

+ * How to use: + *

    + *

    + *

  • Subclass and implement serve() and embed to your own program
  • + *

    + *

+ *

+ * See the separate "LICENSE.md" 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(); + TempFileManager tempFileManager = NanoHTTPD.this.tempFileManagerFactory.create(); + HTTPSession session = new HTTPSession(tempFileManager, 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); + } + } + } + + public static class Cookie { + + public static String getHTTPTime(int days) { + Calendar calendar = Calendar.getInstance(); + SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); + dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); + calendar.add(Calendar.DAY_OF_MONTH, days); + return dateFormat.format(calendar.getTime()); + } + + private final String n, v, e; + + public Cookie(String name, String value) { + this(name, value, 30); + } + + public Cookie(String name, String value, int numDays) { + this.n = name; + this.v = value; + this.e = getHTTPTime(numDays); + } + + public Cookie(String name, String value, String expires) { + this.n = name; + this.v = value; + this.e = expires; + } + + public String getHTTPHeader() { + String fmt = "%s=%s; expires=%s"; + return String.format(fmt, this.n, this.v, this.e); + } + } + + /** + * Provides rudimentary support for cookies. Doesn't support 'path', + * 'secure' nor 'httpOnly'. Feel free to improve it and/or add unsupported + * features. + * + * @author LordFokas + */ + public class CookieHandler implements Iterable { + + private final HashMap cookies = new HashMap(); + + private final ArrayList queue = new ArrayList(); + + public CookieHandler(Map httpHeaders) { + String raw = httpHeaders.get("cookie"); + if (raw != null) { + String[] tokens = raw.split(";"); + for (String token : tokens) { + String[] data = token.trim().split("="); + if (data.length == 2) { + this.cookies.put(data[0], data[1]); + } + } + } + } + + /** + * Set a cookie with an expiration date from a month ago, effectively + * deleting it on the client side. + * + * @param name + * The cookie name. + */ + public void delete(String name) { + set(name, "-delete-", -30); + } + + @Override + public Iterator iterator() { + return this.cookies.keySet().iterator(); + } + + /** + * Read a cookie from the HTTP Headers. + * + * @param name + * The cookie's name. + * @return The cookie's value if it exists, null otherwise. + */ + public String read(String name) { + return this.cookies.get(name); + } + + public void set(Cookie cookie) { + this.queue.add(cookie); + } + + /** + * Sets a cookie. + * + * @param name + * The cookie's name. + * @param value + * The cookie's value. + * @param expires + * How many days until the cookie expires. + */ + public void set(String name, String value, int expires) { + this.queue.add(new Cookie(name, value, Cookie.getHTTPTime(expires))); + } + + /** + * Internally used by the webserver to add all queued cookies into the + * Response's HTTP Headers. + * + * @param response + * The Response object to which headers the queued cookies + * will be added. + */ + public void unloadQueue(Response response) { + for (Cookie cookie : this.queue) { + response.addHeader("Set-Cookie", cookie.getHTTPHeader()); + } + } + } + + /** + * Default threading strategy for NanoHTTPD. + *

+ *

+ * By default, the server spawns a new Thread for every incoming request. + * These are set to daemon status, and named according to the request + * number. The name is useful when profiling the application. + *

+ */ + public static class DefaultAsyncRunner implements AsyncRunner { + + private long requestCount; + + private final List running = Collections.synchronizedList(new ArrayList()); + + /** + * @return a list with currently running clients. + */ + public List 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(); + } + } + + /** + * Default strategy for creating and cleaning up temporary files. + *

+ *

+ * By default, files are created by File.createTempFile() in + * the directory specified. + *

+ */ + public static class DefaultTempFile implements TempFile { + + private final File file; + + private final OutputStream fstream; + + public DefaultTempFile(File tempdir) throws IOException { + this.file = File.createTempFile("NanoHTTPD-", "", tempdir); + this.fstream = new FileOutputStream(this.file); + } + + @Override + public void delete() throws Exception { + safeClose(this.fstream); + if (!this.file.delete()) { + throw new Exception("could not delete temporary file: " + this.file.getAbsolutePath()); + } + } + + @Override + public String getName() { + return this.file.getAbsolutePath(); + } + + @Override + public OutputStream open() throws Exception { + return this.fstream; + } + } + + /** + * Default strategy for creating and cleaning up temporary files. + *

+ *

+ * This class stores its files in the standard location (that is, wherever + * java.io.tmpdir points to). Files are added to an internal + * list, and deleted when no longer needed (that is, when + * clear() is invoked at the end of processing a request). + *

+ */ + public static class DefaultTempFileManager implements TempFileManager { + + private final File tmpdir; + + private final List tempFiles; + + public DefaultTempFileManager() { + this.tmpdir = new File(System.getProperty("java.io.tmpdir")); + if (!tmpdir.exists()) { + tmpdir.mkdirs(); + } + this.tempFiles = new ArrayList(); + } + + @Override + public void clear() { + for (TempFile file : this.tempFiles) { + try { + file.delete(); + } catch (Exception ignored) { + NanoHTTPD.LOG.log(Level.WARNING, "could not delete file ", ignored); + } + } + this.tempFiles.clear(); + } + + @Override + public TempFile createTempFile(String filename_hint) throws Exception { + DefaultTempFile tempFile = new DefaultTempFile(this.tmpdir); + this.tempFiles.add(tempFile); + return tempFile; + } + } + + /** + * Default strategy for creating and cleaning up temporary files. + */ + private class DefaultTempFileManagerFactory implements TempFileManagerFactory { + + @Override + public TempFileManager create() { + return new DefaultTempFileManager(); + } + } + + /** + * Creates a normal ServerSocket for TCP connections + */ + public static class DefaultServerSocketFactory implements ServerSocketFactory { + + @Override + public ServerSocket create() throws IOException { + return new ServerSocket(); + } + + } + + /** + * Creates a new SSLServerSocket + */ + public static class SecureServerSocketFactory implements ServerSocketFactory { + + private SSLServerSocketFactory sslServerSocketFactory; + + private String[] sslProtocols; + + public SecureServerSocketFactory(SSLServerSocketFactory sslServerSocketFactory, String[] sslProtocols) { + this.sslServerSocketFactory = sslServerSocketFactory; + this.sslProtocols = sslProtocols; + } + + @Override + public ServerSocket create() throws IOException { + SSLServerSocket ss = null; + ss = (SSLServerSocket) this.sslServerSocketFactory.createServerSocket(); + if (this.sslProtocols != null) { + ss.setEnabledProtocols(this.sslProtocols); + } else { + ss.setEnabledProtocols(ss.getSupportedProtocols()); + } + ss.setUseClientMode(false); + ss.setWantClientAuth(false); + ss.setNeedClientAuth(false); + return ss; + } + + } + + private static final String CONTENT_DISPOSITION_REGEX = "([ |\t]*Content-Disposition[ |\t]*:)(.*)"; + + private static final Pattern CONTENT_DISPOSITION_PATTERN = Pattern.compile(CONTENT_DISPOSITION_REGEX, Pattern.CASE_INSENSITIVE); + + private static final String CONTENT_TYPE_REGEX = "([ |\t]*content-type[ |\t]*:)(.*)"; + + private static final Pattern CONTENT_TYPE_PATTERN = Pattern.compile(CONTENT_TYPE_REGEX, Pattern.CASE_INSENSITIVE); + + private static final String CONTENT_DISPOSITION_ATTRIBUTE_REGEX = "[ |\t]*([a-zA-Z]*)[ |\t]*=[ |\t]*['|\"]([^\"^']*)['|\"]"; + + private static final Pattern CONTENT_DISPOSITION_ATTRIBUTE_PATTERN = Pattern.compile(CONTENT_DISPOSITION_ATTRIBUTE_REGEX); + + protected static 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; + } + } + + protected class HTTPSession implements IHTTPSession { + + private static final int REQUEST_BUFFER_LEN = 512; + + private static final int MEMORY_STORE_LIMIT = 1024; + + public static final int BUFSIZE = 8192; + + public static final int MAX_HEADER_SIZE = 1024; + + private final TempFileManager tempFileManager; + + private final OutputStream outputStream; + + private final BufferedInputStream inputStream; + + private int splitbyte; + + private int rlen; + + private String uri; + + private Method method; + + private Map> parms; + + private Map headers; + + private CookieHandler cookies; + + private String queryParameterString; + + private String remoteIp; + + private String remoteHostname; + + private String protocolVersion; + + public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream) { + this.tempFileManager = tempFileManager; + this.inputStream = new BufferedInputStream(inputStream, HTTPSession.BUFSIZE); + this.outputStream = outputStream; + } + + public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream, InetAddress inetAddress) { + this.tempFileManager = tempFileManager; + this.inputStream = new BufferedInputStream(inputStream, HTTPSession.BUFSIZE); + this.outputStream = outputStream; + this.remoteIp = inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() ? "127.0.0.1" : inetAddress.getHostAddress().toString(); + this.remoteHostname = inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() ? "localhost" : inetAddress.getHostName().toString(); + this.headers = new HashMap(); + } + + /** + * Decodes the sent headers and loads the data into Key/value pairs + */ + private void decodeHeader(BufferedReader in, Map pre, Map> parms, Map 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(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html"); + } + + pre.put("method", st.nextToken()); + + if (!st.hasMoreTokens()) { + throw new ResponseException(Response.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(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe); + } + } + + /** + * Decodes the Multipart Body data and put it into Key/Value pairs. + */ + private void decodeMultipartFormData(ContentType contentType, ByteBuffer fbuf, Map> parms, Map files) throws ResponseException { + int pcount = 0; + try { + int[] boundaryIdxs = getBoundaryPositions(fbuf, contentType.getBoundary().getBytes()); + if (boundaryIdxs.length < 2) { + throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but contains less than two boundary strings."); + } + + byte[] partHeaderBuff = new byte[MAX_HEADER_SIZE]; + for (int boundaryIdx = 0; boundaryIdx < boundaryIdxs.length - 1; boundaryIdx++) { + fbuf.position(boundaryIdxs[boundaryIdx]); + int len = (fbuf.remaining() < MAX_HEADER_SIZE) ? fbuf.remaining() : MAX_HEADER_SIZE; + fbuf.get(partHeaderBuff, 0, len); + BufferedReader in = + new BufferedReader(new InputStreamReader(new ByteArrayInputStream(partHeaderBuff, 0, len), Charset.forName(contentType.getEncoding())), len); + + int headerLines = 0; + // First line is boundary string + String mpline = in.readLine(); + headerLines++; + if (mpline == null || !mpline.contains(contentType.getBoundary())) { + throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but chunk does not start with boundary."); + } + + String partName = null, fileName = null, partContentType = null; + // Parse the reset of the header lines + mpline = in.readLine(); + headerLines++; + while (mpline != null && mpline.trim().length() > 0) { + Matcher matcher = CONTENT_DISPOSITION_PATTERN.matcher(mpline); + if (matcher.matches()) { + String attributeString = matcher.group(2); + matcher = CONTENT_DISPOSITION_ATTRIBUTE_PATTERN.matcher(attributeString); + while (matcher.find()) { + String key = matcher.group(1); + if ("name".equalsIgnoreCase(key)) { + partName = matcher.group(2); + } else if ("filename".equalsIgnoreCase(key)) { + fileName = matcher.group(2); + // add these two line to support multiple + // files uploaded using the same field Id + if (!fileName.isEmpty()) { + if (pcount > 0) + partName = partName + String.valueOf(pcount++); + else + pcount++; + } + } + } + } + matcher = CONTENT_TYPE_PATTERN.matcher(mpline); + if (matcher.matches()) { + partContentType = matcher.group(2).trim(); + } + mpline = in.readLine(); + headerLines++; + } + int partHeaderLength = 0; + while (headerLines-- > 0) { + partHeaderLength = scipOverNewLine(partHeaderBuff, partHeaderLength); + } + // Read the part data + if (partHeaderLength >= len - 4) { + throw new ResponseException(Response.Status.INTERNAL_ERROR, "Multipart header size exceeds MAX_HEADER_SIZE."); + } + int partDataStart = boundaryIdxs[boundaryIdx] + partHeaderLength; + int partDataEnd = boundaryIdxs[boundaryIdx + 1] - 4; + + fbuf.position(partDataStart); + + List values = parms.get(partName); + if (values == null) { + values = new ArrayList(); + parms.put(partName, values); + } + + if (partContentType == null) { + // Read the part into a string + byte[] data_bytes = new byte[partDataEnd - partDataStart]; + fbuf.get(data_bytes); + + values.add(new String(data_bytes, contentType.getEncoding())); + } else { + // Read it into a file + String path = saveTmpFile(fbuf, partDataStart, partDataEnd - partDataStart, fileName); + if (!files.containsKey(partName)) { + files.put(partName, path); + } else { + int count = 2; + while (files.containsKey(partName + count)) { + count++; + } + files.put(partName + count, path); + } + values.add(fileName); + } + } + } catch (ResponseException re) { + throw re; + } catch (Exception e) { + throw new ResponseException(Response.Status.INTERNAL_ERROR, e.toString()); + } + } + + private int scipOverNewLine(byte[] partHeaderBuff, int index) { + while (partHeaderBuff[index] != '\n') { + index++; + } + return ++index; + } + + /** + * 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> 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 values = p.get(key); + if (values == null) { + values = new ArrayList(); + p.put(key, values); + } + + values.add(value); + } + } + + @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 (SSLException e) { + throw e; + } 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))); + + // Decode the header into parms and header java properties + Map pre = new HashMap(); + decodeHeader(hin, pre, this.parms, this.headers); + + if (null != this.remoteIp) { + this.headers.put("remote-addr", this.remoteIp); + this.headers.put("http-client-ip", this.remoteIp); + } + + this.method = Method.lookup(pre.get("method")); + if (this.method == null) { + throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error. HTTP verb " + pre.get("method") + " unhandled."); + } + + this.uri = pre.get("uri"); + + this.cookies = new CookieHandler(this.headers); + + String connection = this.headers.get("connection"); + boolean keepAlive = "HTTP/1.1".equals(protocolVersion) && (connection == null || !connection.matches("(?i).*close.*")); + + // Ok, now do the serve() + + // TODO: long body_size = getBodySize(); + // TODO: long pos_before_serve = this.inputStream.totalRead() + // (requires implementation for totalRead()) + r = serve(this); + // TODO: this.inputStream.skip(body_size - + // (this.inputStream.totalRead() - pos_before_serve)) + + if (r == null) { + throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response."); + } else { + String acceptEncoding = this.headers.get("accept-encoding"); + this.cookies.unloadQueue(r); + r.setRequestMethod(this.method); + r.setGzipEncoding(useGzipWhenAccepted(r) && acceptEncoding != null && acceptEncoding.contains("gzip")); + 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 (SSLException ssle) { + Response resp = newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SSL PROTOCOL FAILURE: " + ssle.getMessage()); + resp.send(this.outputStream); + safeClose(this.outputStream); + } catch (IOException ioe) { + Response resp = newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); + resp.send(this.outputStream); + safeClose(this.outputStream); + } catch (ResponseException re) { + Response resp = newFixedLengthResponse(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage()); + resp.send(this.outputStream); + safeClose(this.outputStream); + } finally { + safeClose(r); + this.tempFileManager.clear(); + } + } + + /** + * 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; + } + + /** + * Find the byte positions where multipart boundaries start. This reads + * a large block at a time and uses a temporary buffer to optimize + * (memory mapped) file access. + */ + private int[] getBoundaryPositions(ByteBuffer b, byte[] boundary) { + int[] res = new int[0]; + if (b.remaining() < boundary.length) { + return res; + } + + int search_window_pos = 0; + byte[] search_window = new byte[4 * 1024 + boundary.length]; + + int first_fill = (b.remaining() < search_window.length) ? b.remaining() : search_window.length; + b.get(search_window, 0, first_fill); + int new_bytes = first_fill - boundary.length; + + do { + // Search the search_window + for (int j = 0; j < new_bytes; j++) { + for (int i = 0; i < boundary.length; i++) { + if (search_window[j + i] != boundary[i]) + break; + if (i == boundary.length - 1) { + // Match found, add it to results + int[] new_res = new int[res.length + 1]; + System.arraycopy(res, 0, new_res, 0, res.length); + new_res[res.length] = search_window_pos + j; + res = new_res; + } + } + } + search_window_pos += new_bytes; + + // Copy the end of the buffer to the start + System.arraycopy(search_window, search_window.length - boundary.length, search_window, 0, boundary.length); + + // Refill search_window + new_bytes = search_window.length - boundary.length; + new_bytes = (b.remaining() < new_bytes) ? b.remaining() : new_bytes; + b.get(search_window, boundary.length, new_bytes); + } while (new_bytes > 0); + return res; + } + + @Override + public CookieHandler getCookies() { + return this.cookies; + } + + @Override + public final Map getHeaders() { + return this.headers; + } + + @Override + public final InputStream getInputStream() { + return this.inputStream; + } + + @Override + public final Method getMethod() { + return this.method; + } + + /** + * @deprecated use {@link #getParameters()} instead. + */ + @Override + @Deprecated + public final Map getParms() { + Map result = new HashMap(); + for (String key : this.parms.keySet()) { + result.put(key, this.parms.get(key).get(0)); + } + + return result; + } + + @Override + public final Map> getParameters() { + return this.parms; + } + + @Override + public String getQueryParameterString() { + return this.queryParameterString; + } + + private RandomAccessFile getTmpBucket() { + try { + TempFile tempFile = this.tempFileManager.createTempFile(null); + return new RandomAccessFile(tempFile.getName(), "rw"); + } catch (Exception e) { + throw new Error(e); // we won't recover, so throw an error + } + } + + @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 void parseBody(Map files) throws IOException, ResponseException { + RandomAccessFile randomAccessFile = null; + try { + long size = getBodySize(); + ByteArrayOutputStream baos = null; + DataOutput requestDataOutput = null; + + // Store the request in memory or a file, depending on size + if (size < MEMORY_STORE_LIMIT) { + baos = new ByteArrayOutputStream(); + requestDataOutput = new DataOutputStream(baos); + } else { + randomAccessFile = getTmpBucket(); + requestDataOutput = randomAccessFile; + } + + // Read all the body and write it to request_data_output + byte[] buf = new byte[REQUEST_BUFFER_LEN]; + while (this.rlen >= 0 && size > 0) { + this.rlen = this.inputStream.read(buf, 0, (int) Math.min(size, REQUEST_BUFFER_LEN)); + size -= this.rlen; + if (this.rlen > 0) { + requestDataOutput.write(buf, 0, this.rlen); + } + } + + ByteBuffer fbuf = null; + if (baos != null) { + fbuf = ByteBuffer.wrap(baos.toByteArray(), 0, baos.size()); + } else { + fbuf = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, randomAccessFile.length()); + randomAccessFile.seek(0); + } + + // If the method is POST, there may be parameters + // in data section, too, read it: + if (Method.POST.equals(this.method)) { + ContentType contentType = new ContentType(this.headers.get("content-type")); + if (contentType.isMultipart()) { + String boundary = contentType.getBoundary(); + if (boundary == null) { + throw new ResponseException(Response.Status.BAD_REQUEST, + "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html"); + } + decodeMultipartFormData(contentType, fbuf, this.parms, files); + } else { + byte[] postBytes = new byte[fbuf.remaining()]; + fbuf.get(postBytes); + String postLine = new String(postBytes, contentType.getEncoding()).trim(); + // Handle application/x-www-form-urlencoded + if ("application/x-www-form-urlencoded".equalsIgnoreCase(contentType.getContentType())) { + decodeParms(postLine, this.parms); + } else if (postLine.length() != 0) { + // Special case for raw POST data => create a + // special files entry "postData" with raw content + // data + files.put("postData", postLine); + } + } + } else if (Method.PUT.equals(this.method)) { + files.put("content", saveTmpFile(fbuf, 0, fbuf.limit(), null)); + } + } finally { + safeClose(randomAccessFile); + } + } + + /** + * Retrieves the content of a sent file and saves it to a temporary + * file. The full path to the saved file is returned. + */ + private String saveTmpFile(ByteBuffer b, int offset, int len, String filename_hint) { + String path = ""; + if (len > 0) { + FileOutputStream fileOutputStream = null; + try { + TempFile tempFile = this.tempFileManager.createTempFile(filename_hint); + ByteBuffer src = b.duplicate(); + fileOutputStream = new FileOutputStream(tempFile.getName()); + FileChannel dest = fileOutputStream.getChannel(); + src.position(offset).limit(offset + len); + dest.write(src.slice()); + path = tempFile.getName(); + } catch (Exception e) { // Catch exception if any + throw new Error(e); // we won't recover, so throw an error + } finally { + safeClose(fileOutputStream); + } + } + return path; + } + + @Override + public String getRemoteIpAddress() { + return this.remoteIp; + } + + @Override + public String getRemoteHostName() { + return this.remoteHostname; + } + } + + /** + * Handles one session, i.e. parses the HTTP request and returns the + * response. + */ + public interface IHTTPSession { + + void execute() throws IOException; + + CookieHandler getCookies(); + + Map getHeaders(); + + InputStream getInputStream(); + + Method getMethod(); + + /** + * This method will only return the first value for a given parameter. + * You will want to use getParameters if you expect multiple values for + * a given key. + * + * @deprecated use {@link #getParameters()} instead. + */ + @Deprecated + Map getParms(); + + Map> getParameters(); + + String getQueryParameterString(); + + /** + * @return the path part of the URL. + */ + String getUri(); + + /** + * Adds the files in the request body to the files map. + * + * @param files + * map to modify + */ + void parseBody(Map files) throws IOException, ResponseException; + + /** + * 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(); + } + + /** + * HTTP Request methods, with the ability to decode a String + * back to its enum value. + */ + public enum Method { + GET, + PUT, + POST, + DELETE, + HEAD, + OPTIONS, + TRACE, + CONNECT, + PATCH, + PROPFIND, + PROPPATCH, + MKCOL, + MOVE, + COPY, + LOCK, + UNLOCK; + + static Method lookup(String method) { + if (method == null) + return null; + + try { + return valueOf(method); + } catch (IllegalArgumentException e) { + // TODO: Log it? + return null; + } + } + } + + /** + * HTTP response. Return one of these from serve(). + */ + public static class Response implements Closeable { + + public interface IStatus { + + String getDescription(); + + int getRequestStatus(); + } + + /** + * 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"), + 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; + } + + } + + /** + * 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 + */ + private static 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()); + out.write(b, off, len); + out.write("\r\n".getBytes()); + } + + public void finish() throws IOException { + out.write("0\r\n\r\n".getBytes()); + } + + } + + /** + * 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. + */ + @SuppressWarnings("serial") + private final Map header = new HashMap() { + + 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 lowerCaseHeader = new HashMap(); + + /** + * The request method that spawned this response. + */ + private Method requestMethod; + + /** + * Use chunkedTransfer + */ + private boolean chunkedTransfer; + + private boolean encodeAsGzip; + + 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 Method getRequestMethod() { + return this.requestMethod; + } + + public IStatus getStatus() { + return this.status; + } + + public void setGzipEncoding(boolean encodeAsGzip) { + this.encodeAsGzip = encodeAsGzip; + } + + 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 entry : this.header.entrySet()) { + printHeader(pw, entry.getKey(), entry.getValue()); + } + if (getHeader("connection") == null) { + printHeader(pw, "Connection", (this.keepAlive ? "keep-alive" : "close")); + } + if (getHeader("content-length") != null) { + encodeAsGzip = false; + } + if (encodeAsGzip) { + printHeader(pw, "Content-Encoding", "gzip"); + setChunkedTransfer(true); + } + long pending = this.data != null ? this.contentLength : 0; + if (this.requestMethod != Method.HEAD && this.chunkedTransfer) { + printHeader(pw, "Transfer-Encoding", "chunked"); + } else if (!encodeAsGzip) { + pending = sendContentLengthHeaderIfNotAlreadyPresent(pw, pending); + } + pw.append("\r\n"); + pw.flush(); + sendBodyWithCorrectTransferAndEncoding(outputStream, pending); + outputStream.flush(); + safeClose(this.data); + } catch (IOException ioe) { + NanoHTTPD.LOG.log(Level.SEVERE, "Could not send response to the client", ioe); + } + } + + @SuppressWarnings("static-method") + 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) { + 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 (this.requestMethod != Method.HEAD && this.chunkedTransfer) { + ChunkedOutputStream chunkedOutputStream = new ChunkedOutputStream(outputStream); + sendBodyWithCorrectEncoding(chunkedOutputStream, -1); + chunkedOutputStream.finish(); + } else { + sendBodyWithCorrectEncoding(outputStream, pending); + } + } + + private void sendBodyWithCorrectEncoding(OutputStream outputStream, long pending) throws IOException { + if (encodeAsGzip) { + GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream); + sendBody(gzipOutputStream, -1); + gzipOutputStream.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(Method requestMethod) { + this.requestMethod = requestMethod; + } + + public void setStatus(IStatus status) { + this.status = status; + } + } + + public static final class ResponseException extends Exception { + + private static final long serialVersionUID = 6569838532917408380L; + + private final Response.Status status; + + public ResponseException(Response.Status status, String message) { + super(message); + this.status = status; + } + + public ResponseException(Response.Status status, String message, Exception e) { + super(message, e); + this.status = status; + } + + public Response.Status getStatus() { + return this.status; + } + } + + /** + * 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 { + final Socket finalAccept = NanoHTTPD.this.myServerSocket.accept(); + if (this.timeout > 0) { + finalAccept.setSoTimeout(this.timeout); + } + 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()); + } + } + + /** + * A temp file. + *

+ *

+ * Temp files are responsible for managing the actual temporary storage and + * cleaning themselves up when no longer needed. + *

+ */ + public interface TempFile { + + public void delete() throws Exception; + + public String getName(); + + public OutputStream open() throws Exception; + } + + /** + * Temp file manager. + *

+ *

+ * Temp file managers are created 1-to-1 with incoming requests, to create + * and cleanup temporary files created as a result of handling the request. + *

+ */ + public interface TempFileManager { + + void clear(); + + public TempFile createTempFile(String filename_hint) throws Exception; + } + + /** + * Factory to create temp file managers. + */ + public interface TempFileManagerFactory { + + public TempFileManager create(); + } + + /** + * 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"; + + /** + * Pseudo-Parameter to use to store the actual query string in the + * parameters map for later re-processing. + */ + private static final String QUERY_STRING_PARAMETER = "NanoHttpd.QUERY_STRING"; + + /** + * logger to log to. + */ + private static final Logger LOG = Logger.getLogger(NanoHTTPD.class.getName()); + + /** + * Hashtable mapping (String)FILENAME_EXTENSION -> (String)MIME_TYPE + */ + protected static Map MIME_TYPES; + + public static Map mimeTypes() { + if (MIME_TYPES == null) { + MIME_TYPES = new HashMap(); + loadMimeTypes(MIME_TYPES, "META-INF/nanohttpd/default-mimetypes.properties"); + loadMimeTypes(MIME_TYPES, "META-INF/nanohttpd/mimetypes.properties"); + if (MIME_TYPES.isEmpty()) { + LOG.log(Level.WARNING, "no mime types found in the classpath! please provide mimetypes.properties"); + } + } + return MIME_TYPES; + } + + @SuppressWarnings({ + "unchecked", + "rawtypes" + }) + private static void loadMimeTypes(Map result, String resourceName) { + try { + Enumeration resources = NanoHTTPD.class.getClassLoader().getResources(resourceName); + while (resources.hasMoreElements()) { + URL url = (URL) resources.nextElement(); + Properties properties = new Properties(); + InputStream stream = null; + try { + stream = url.openStream(); + properties.load(stream); + } catch (IOException e) { + LOG.log(Level.SEVERE, "could not load mimetypes from " + url, e); + } finally { + safeClose(stream); + } + result.putAll((Map) properties); + } + } catch (IOException e) { + LOG.log(Level.INFO, "no mime types available at " + resourceName); + } + }; + + /** + * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and an + * array of loaded KeyManagers. These objects must properly + * loaded/initialized by the caller. + */ + public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManager[] keyManagers) throws IOException { + SSLServerSocketFactory res = null; + try { + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(loadedKeyStore); + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init(keyManagers, trustManagerFactory.getTrustManagers(), null); + res = ctx.getServerSocketFactory(); + } catch (Exception e) { + throw new IOException(e.getMessage()); + } + return res; + } + + /** + * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and a + * loaded KeyManagerFactory. These objects must properly loaded/initialized + * by the caller. + */ + public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManagerFactory loadedKeyFactory) throws IOException { + try { + return makeSSLSocketFactory(loadedKeyStore, loadedKeyFactory.getKeyManagers()); + } catch (Exception e) { + throw new IOException(e.getMessage()); + } + } + + /** + * Creates an SSLSocketFactory for HTTPS. Pass a KeyStore resource with your + * certificate and passphrase + */ + public static SSLServerSocketFactory makeSSLSocketFactory(String keyAndTrustStoreClasspathPath, char[] passphrase) throws IOException { + try { + KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); + InputStream keystoreStream = NanoHTTPD.class.getResourceAsStream(keyAndTrustStoreClasspathPath); + + if (keystoreStream == null) { + throw new IOException("Unable to load keystore from classpath: " + keyAndTrustStoreClasspathPath); + } + + keystore.load(keystoreStream, passphrase); + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(keystore, passphrase); + return makeSSLSocketFactory(keystore, keyManagerFactory); + } catch (Exception e) { + throw new IOException(e.getMessage()); + } + } + + /** + * Get MIME type from file name extension, if possible + * + * @param uri + * the string representing a file + * @return the connected mime/type + */ + public static String getMimeTypeForFile(String uri) { + int dot = uri.lastIndexOf('.'); + String mime = null; + if (dot >= 0) { + mime = mimeTypes().get(uri.substring(dot + 1).toLowerCase()); + } + return mime == null ? "application/octet-stream" : mime; + } + + private 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; + + /** + * Pluggable strategy for creating and cleaning up temporary files. + */ + private TempFileManagerFactory tempFileManagerFactory; + + /** + * 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; + setTempFileManagerFactory(new DefaultTempFileManagerFactory()); + 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 parameters from a URL, handing the case where a single parameter + * name might have been supplied several times, by return lists of values. + * In general these lists will contain a single element. + * + * @param parms + * original NanoHTTPD parameters values, as passed to the + * serve() method. + * @return a map of String (parameter name) to + * List<String> (a list of the values supplied). + */ + protected static Map> decodeParameters(Map parms) { + return decodeParameters(parms.get(NanoHTTPD.QUERY_STRING_PARAMETER)); + } + + // ------------------------------------------------------------------------------- + // // + + /** + * Decode parameters from a URL, handing the case where a single parameter + * name might have been supplied several times, by return lists of values. + * In general these lists will contain a single element. + * + * @param queryString + * a query string pulled from the URL. + * @return a map of String (parameter name) to + * List<String> (a list of the values supplied). + */ + protected static Map> decodeParameters(String queryString) { + Map> parms = new HashMap>(); + if (queryString != null) { + StringTokenizer st = new StringTokenizer(queryString, "&"); + while (st.hasMoreTokens()) { + String e = st.nextToken(); + int sep = e.indexOf('='); + String propertyName = sep >= 0 ? decodePercent(e.substring(0, sep)).trim() : decodePercent(e).trim(); + if (!parms.containsKey(propertyName)) { + parms.put(propertyName, new ArrayList()); + } + String propertyValue = sep >= 0 ? decodePercent(e.substring(sep + 1)) : null; + if (propertyValue != null) { + parms.get(propertyName).add(propertyValue); + } + } + } + return parms; + } + + /** + * Decode percent encoded String values. + * + * @param str + * the percent encoded String + * @return expanded form of the input, for example "foo%20bar" becomes + * "foo bar" + */ + protected 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; + } + + /** + * @return true if the gzip compression should be used if the client + * accespts it. Default this option is on for text content and off + * for everything. Override this for custom semantics. + */ + @SuppressWarnings("static-method") + protected boolean useGzipWhenAccepted(Response r) { + return r.getMimeType() != null && (r.getMimeType().toLowerCase().contains("text/") || r.getMimeType().toLowerCase().contains("/json")); + } + + 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; + } + + public TempFileManagerFactory getTempFileManagerFactory() { + return tempFileManagerFactory; + } + + /** + * Call before start() to serve over HTTPS instead of HTTP + */ + public void makeSecure(SSLServerSocketFactory sslServerSocketFactory, String[] sslProtocols) { + this.serverSocketFactory = new SecureServerSocketFactory(sslServerSocketFactory, sslProtocols); + } + + /** + * Create a response with unknown length (using HTTP 1.1 chunking). + */ + public static Response newChunkedResponse(IStatus status, String mimeType, InputStream data) { + return new Response(status, mimeType, data, -1); + } + + /** + * Create a response with known length. + */ + public static Response newFixedLengthResponse(IStatus status, String mimeType, InputStream data, long totalBytes) { + return new Response(status, mimeType, data, totalBytes); + } + + /** + * Create a text response with known length. + */ + public static Response newFixedLengthResponse(IStatus status, String mimeType, String txt) { + ContentType contentType = new ContentType(mimeType); + if (txt == null) { + return newFixedLengthResponse(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 newFixedLengthResponse(status, contentType.getContentTypeHeader(), new ByteArrayInputStream(bytes), bytes.length); + } + } + + /** + * Create a text response with known length. + */ + public static Response newFixedLengthResponse(String msg) { + return newFixedLengthResponse(Status.OK, NanoHTTPD.MIME_HTML, msg); + } + + /** + * Override this to customize the server. + *

+ *

+ * (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) { + Map files = new HashMap(); + Method method = session.getMethod(); + if (Method.PUT.equals(method) || Method.POST.equals(method)) { + try { + session.parseBody(files); + } catch (IOException ioe) { + return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); + } catch (ResponseException re) { + return newFixedLengthResponse(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage()); + } + } + + Map parms = session.getParms(); + parms.put(NanoHTTPD.QUERY_STRING_PARAMETER, session.getQueryParameterString()); + return serve(session.getUri(), method, session.getHeaders(), parms, files); + } + + /** + * Override this to customize the server. + *

+ *

+ * (By default, this returns a 404 "Not Found" plain text error response.) + * + * @param uri + * Percent-decoded URI without parameters, for example + * "/index.cgi" + * @param method + * "GET", "POST" etc. + * @param parms + * Parsed, percent decoded parameters from URI and, in case of + * POST, data. + * @param headers + * Header entries, percent decoded + * @return HTTP response, see class Response for details + */ + @Deprecated + public Response serve(String uri, Method method, Map headers, Map parms, Map files) { + return newFixedLengthResponse(Response.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; + } + + /** + * Pluggable strategy for creating and cleaning up temporary files. + * + * @param tempFileManagerFactory + * new strategy for handling temp files. + */ + public void setTempFileManagerFactory(TempFileManagerFactory tempFileManagerFactory) { + this.tempFileManagerFactory = tempFileManagerFactory; + } + + /** + * 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; + } +} diff --git a/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/package-info.java b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/package-info.java new file mode 100644 index 0000000..fea1ff6 --- /dev/null +++ b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/package-info.java @@ -0,0 +1,6 @@ +/** + * Modified nanohttpd. + *

+ * See license in META-INF/licenses/nanohttpd.txt + */ +package moe.yushi.authlibinjector.internal.fi.iki.elonen; diff --git a/src/main/resources/META-INF/licenses/nanohttpd.txt b/src/main/resources/META-INF/licenses/nanohttpd.txt index 8dc4ca7..a715254 100644 --- a/src/main/resources/META-INF/licenses/nanohttpd.txt +++ b/src/main/resources/META-INF/licenses/nanohttpd.txt @@ -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. diff --git a/src/main/resources/META-INF/nanohttpd/default-mimetypes.properties b/src/main/resources/META-INF/nanohttpd/default-mimetypes.properties new file mode 100644 index 0000000..3fb242f --- /dev/null +++ b/src/main/resources/META-INF/nanohttpd/default-mimetypes.properties @@ -0,0 +1,30 @@ +#default mime types for nanohttpd, use META-INF/mimetypes.properties for user defined mimetypes +css=text/css +htm=text/html +html=text/html +xml=text/xml +java=text/x-java-source, text/java +md=text/plain +txt=text/plain +asc=text/plain +gif=image/gif +jpg=image/jpeg +jpeg=image/jpeg +png=image/png +svg=image/svg+xml +mp3=audio/mpeg +m3u=audio/mpeg-url +mp4=video/mp4 +ogv=video/ogg +flv=video/x-flv +mov=video/quicktime +swf=application/x-shockwave-flash +js=application/javascript +pdf=application/pdf +doc=application/msword +ogg=application/x-ogg +zip=application/octet-stream +exe=application/octet-stream +class=application/octet-stream +m3u8=application/vnd.apple.mpegurl +ts=video/mp2t \ No newline at end of file From ffc96d0a05fa5e043a6820b74681efd20500bc6c Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Sun, 30 Dec 2018 16:59:09 +0800 Subject: [PATCH 06/16] [nanohttpd]code format --- .../internal/fi/iki/elonen/NanoHTTPD.java | 4339 ++++++++--------- 1 file changed, 2169 insertions(+), 2170 deletions(-) diff --git a/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/NanoHTTPD.java b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/NanoHTTPD.java index ecb7761..34b88dc 100644 --- a/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/NanoHTTPD.java +++ b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/NanoHTTPD.java @@ -8,18 +8,18 @@ package moe.yushi.authlibinjector.internal.fi.iki.elonen; * %% * 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. - * + * 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. - * + * 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. - * + * 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. @@ -150,2209 +150,2208 @@ import moe.yushi.authlibinjector.internal.fi.iki.elonen.NanoHTTPD.Response.Statu */ 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(); - TempFileManager tempFileManager = NanoHTTPD.this.tempFileManagerFactory.create(); - HTTPSession session = new HTTPSession(tempFileManager, 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); - } - } - } - - public static class Cookie { - - public static String getHTTPTime(int days) { - Calendar calendar = Calendar.getInstance(); - SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); - dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); - calendar.add(Calendar.DAY_OF_MONTH, days); - return dateFormat.format(calendar.getTime()); - } - - private final String n, v, e; - - public Cookie(String name, String value) { - this(name, value, 30); - } - - public Cookie(String name, String value, int numDays) { - this.n = name; - this.v = value; - this.e = getHTTPTime(numDays); - } - - public Cookie(String name, String value, String expires) { - this.n = name; - this.v = value; - this.e = expires; - } - - public String getHTTPHeader() { - String fmt = "%s=%s; expires=%s"; - return String.format(fmt, this.n, this.v, this.e); - } - } - - /** - * Provides rudimentary support for cookies. Doesn't support 'path', - * 'secure' nor 'httpOnly'. Feel free to improve it and/or add unsupported - * features. - * - * @author LordFokas - */ - public class CookieHandler implements Iterable { - - private final HashMap cookies = new HashMap(); - - private final ArrayList queue = new ArrayList(); - - public CookieHandler(Map httpHeaders) { - String raw = httpHeaders.get("cookie"); - if (raw != null) { - String[] tokens = raw.split(";"); - for (String token : tokens) { - String[] data = token.trim().split("="); - if (data.length == 2) { - this.cookies.put(data[0], data[1]); - } - } - } - } - - /** - * Set a cookie with an expiration date from a month ago, effectively - * deleting it on the client side. - * - * @param name - * The cookie name. - */ - public void delete(String name) { - set(name, "-delete-", -30); - } - - @Override - public Iterator iterator() { - return this.cookies.keySet().iterator(); - } - - /** - * Read a cookie from the HTTP Headers. - * - * @param name - * The cookie's name. - * @return The cookie's value if it exists, null otherwise. - */ - public String read(String name) { - return this.cookies.get(name); - } - - public void set(Cookie cookie) { - this.queue.add(cookie); - } - - /** - * Sets a cookie. - * - * @param name - * The cookie's name. - * @param value - * The cookie's value. - * @param expires - * How many days until the cookie expires. - */ - public void set(String name, String value, int expires) { - this.queue.add(new Cookie(name, value, Cookie.getHTTPTime(expires))); - } - - /** - * Internally used by the webserver to add all queued cookies into the - * Response's HTTP Headers. - * - * @param response - * The Response object to which headers the queued cookies - * will be added. - */ - public void unloadQueue(Response response) { - for (Cookie cookie : this.queue) { - response.addHeader("Set-Cookie", cookie.getHTTPHeader()); - } - } - } - - /** - * Default threading strategy for NanoHTTPD. - *

- *

- * By default, the server spawns a new Thread for every incoming request. - * These are set to daemon status, and named according to the request - * number. The name is useful when profiling the application. - *

- */ - public static class DefaultAsyncRunner implements AsyncRunner { - - private long requestCount; - - private final List running = Collections.synchronizedList(new ArrayList()); - - /** - * @return a list with currently running clients. - */ - public List 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(); - } - } - - /** - * Default strategy for creating and cleaning up temporary files. - *

- *

- * By default, files are created by File.createTempFile() in - * the directory specified. - *

- */ - public static class DefaultTempFile implements TempFile { - - private final File file; - - private final OutputStream fstream; - - public DefaultTempFile(File tempdir) throws IOException { - this.file = File.createTempFile("NanoHTTPD-", "", tempdir); - this.fstream = new FileOutputStream(this.file); - } - - @Override - public void delete() throws Exception { - safeClose(this.fstream); - if (!this.file.delete()) { - throw new Exception("could not delete temporary file: " + this.file.getAbsolutePath()); - } - } - - @Override - public String getName() { - return this.file.getAbsolutePath(); - } - - @Override - public OutputStream open() throws Exception { - return this.fstream; - } - } - - /** - * Default strategy for creating and cleaning up temporary files. - *

- *

- * This class stores its files in the standard location (that is, wherever - * java.io.tmpdir points to). Files are added to an internal - * list, and deleted when no longer needed (that is, when - * clear() is invoked at the end of processing a request). - *

- */ - public static class DefaultTempFileManager implements TempFileManager { - - private final File tmpdir; - - private final List tempFiles; - - public DefaultTempFileManager() { - this.tmpdir = new File(System.getProperty("java.io.tmpdir")); - if (!tmpdir.exists()) { - tmpdir.mkdirs(); - } - this.tempFiles = new ArrayList(); - } - - @Override - public void clear() { - for (TempFile file : this.tempFiles) { - try { - file.delete(); - } catch (Exception ignored) { - NanoHTTPD.LOG.log(Level.WARNING, "could not delete file ", ignored); - } - } - this.tempFiles.clear(); - } - - @Override - public TempFile createTempFile(String filename_hint) throws Exception { - DefaultTempFile tempFile = new DefaultTempFile(this.tmpdir); - this.tempFiles.add(tempFile); - return tempFile; - } - } - - /** - * Default strategy for creating and cleaning up temporary files. - */ - private class DefaultTempFileManagerFactory implements TempFileManagerFactory { - - @Override - public TempFileManager create() { - return new DefaultTempFileManager(); - } - } - - /** - * Creates a normal ServerSocket for TCP connections - */ - public static class DefaultServerSocketFactory implements ServerSocketFactory { - - @Override - public ServerSocket create() throws IOException { - return new ServerSocket(); - } - - } - - /** - * Creates a new SSLServerSocket - */ - public static class SecureServerSocketFactory implements ServerSocketFactory { - - private SSLServerSocketFactory sslServerSocketFactory; - - private String[] sslProtocols; - - public SecureServerSocketFactory(SSLServerSocketFactory sslServerSocketFactory, String[] sslProtocols) { - this.sslServerSocketFactory = sslServerSocketFactory; - this.sslProtocols = sslProtocols; - } - - @Override - public ServerSocket create() throws IOException { - SSLServerSocket ss = null; - ss = (SSLServerSocket) this.sslServerSocketFactory.createServerSocket(); - if (this.sslProtocols != null) { - ss.setEnabledProtocols(this.sslProtocols); - } else { - ss.setEnabledProtocols(ss.getSupportedProtocols()); - } - ss.setUseClientMode(false); - ss.setWantClientAuth(false); - ss.setNeedClientAuth(false); - return ss; - } + /** + * 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(); + TempFileManager tempFileManager = NanoHTTPD.this.tempFileManagerFactory.create(); + HTTPSession session = new HTTPSession(tempFileManager, 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); + } + } + } + + public static class Cookie { + + public static String getHTTPTime(int days) { + Calendar calendar = Calendar.getInstance(); + SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); + dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); + calendar.add(Calendar.DAY_OF_MONTH, days); + return dateFormat.format(calendar.getTime()); + } + + private final String n, v, e; + + public Cookie(String name, String value) { + this(name, value, 30); + } + + public Cookie(String name, String value, int numDays) { + this.n = name; + this.v = value; + this.e = getHTTPTime(numDays); + } + + public Cookie(String name, String value, String expires) { + this.n = name; + this.v = value; + this.e = expires; + } + + public String getHTTPHeader() { + String fmt = "%s=%s; expires=%s"; + return String.format(fmt, this.n, this.v, this.e); + } + } + + /** + * Provides rudimentary support for cookies. Doesn't support 'path', + * 'secure' nor 'httpOnly'. Feel free to improve it and/or add unsupported + * features. + * + * @author LordFokas + */ + public class CookieHandler implements Iterable { + + private final HashMap cookies = new HashMap<>(); + + private final ArrayList queue = new ArrayList<>(); + + public CookieHandler(Map httpHeaders) { + String raw = httpHeaders.get("cookie"); + if (raw != null) { + String[] tokens = raw.split(";"); + for (String token : tokens) { + String[] data = token.trim().split("="); + if (data.length == 2) { + this.cookies.put(data[0], data[1]); + } + } + } + } + + /** + * Set a cookie with an expiration date from a month ago, effectively + * deleting it on the client side. + * + * @param name + * The cookie name. + */ + public void delete(String name) { + set(name, "-delete-", -30); + } + + @Override + public Iterator iterator() { + return this.cookies.keySet().iterator(); + } + + /** + * Read a cookie from the HTTP Headers. + * + * @param name + * The cookie's name. + * @return The cookie's value if it exists, null otherwise. + */ + public String read(String name) { + return this.cookies.get(name); + } + + public void set(Cookie cookie) { + this.queue.add(cookie); + } + + /** + * Sets a cookie. + * + * @param name + * The cookie's name. + * @param value + * The cookie's value. + * @param expires + * How many days until the cookie expires. + */ + public void set(String name, String value, int expires) { + this.queue.add(new Cookie(name, value, Cookie.getHTTPTime(expires))); + } + + /** + * Internally used by the webserver to add all queued cookies into the + * Response's HTTP Headers. + * + * @param response + * The Response object to which headers the queued cookies + * will be added. + */ + public void unloadQueue(Response response) { + for (Cookie cookie : this.queue) { + response.addHeader("Set-Cookie", cookie.getHTTPHeader()); + } + } + } + + /** + * Default threading strategy for NanoHTTPD. + *

+ *

+ * By default, the server spawns a new Thread for every incoming request. + * These are set to daemon status, and named according to the request + * number. The name is useful when profiling the application. + *

+ */ + public static class DefaultAsyncRunner implements AsyncRunner { + + private long requestCount; + + private final List running = Collections.synchronizedList(new ArrayList()); + + /** + * @return a list with currently running clients. + */ + public List 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(); + } + } + + /** + * Default strategy for creating and cleaning up temporary files. + *

+ *

+ * By default, files are created by File.createTempFile() in + * the directory specified. + *

+ */ + public static class DefaultTempFile implements TempFile { + + private final File file; + + private final OutputStream fstream; + + public DefaultTempFile(File tempdir) throws IOException { + this.file = File.createTempFile("NanoHTTPD-", "", tempdir); + this.fstream = new FileOutputStream(this.file); + } + + @Override + public void delete() throws Exception { + safeClose(this.fstream); + if (!this.file.delete()) { + throw new Exception("could not delete temporary file: " + this.file.getAbsolutePath()); + } + } + + @Override + public String getName() { + return this.file.getAbsolutePath(); + } + + @Override + public OutputStream open() throws Exception { + return this.fstream; + } + } + + /** + * Default strategy for creating and cleaning up temporary files. + *

+ *

+ * This class stores its files in the standard location (that is, wherever + * java.io.tmpdir points to). Files are added to an internal + * list, and deleted when no longer needed (that is, when + * clear() is invoked at the end of processing a request). + *

+ */ + public static class DefaultTempFileManager implements TempFileManager { + + private final File tmpdir; + + private final List tempFiles; + + public DefaultTempFileManager() { + this.tmpdir = new File(System.getProperty("java.io.tmpdir")); + if (!tmpdir.exists()) { + tmpdir.mkdirs(); + } + this.tempFiles = new ArrayList<>(); + } + + @Override + public void clear() { + for (TempFile file : this.tempFiles) { + try { + file.delete(); + } catch (Exception ignored) { + NanoHTTPD.LOG.log(Level.WARNING, "could not delete file ", ignored); + } + } + this.tempFiles.clear(); + } + + @Override + public TempFile createTempFile(String filename_hint) throws Exception { + DefaultTempFile tempFile = new DefaultTempFile(this.tmpdir); + this.tempFiles.add(tempFile); + return tempFile; + } + } + + /** + * Default strategy for creating and cleaning up temporary files. + */ + private class DefaultTempFileManagerFactory implements TempFileManagerFactory { + + @Override + public TempFileManager create() { + return new DefaultTempFileManager(); + } + } + + /** + * Creates a normal ServerSocket for TCP connections + */ + public static class DefaultServerSocketFactory implements ServerSocketFactory { + + @Override + public ServerSocket create() throws IOException { + return new ServerSocket(); + } + + } + + /** + * Creates a new SSLServerSocket + */ + public static class SecureServerSocketFactory implements ServerSocketFactory { + + private SSLServerSocketFactory sslServerSocketFactory; + + private String[] sslProtocols; + + public SecureServerSocketFactory(SSLServerSocketFactory sslServerSocketFactory, String[] sslProtocols) { + this.sslServerSocketFactory = sslServerSocketFactory; + this.sslProtocols = sslProtocols; + } + + @Override + public ServerSocket create() throws IOException { + SSLServerSocket ss = null; + ss = (SSLServerSocket) this.sslServerSocketFactory.createServerSocket(); + if (this.sslProtocols != null) { + ss.setEnabledProtocols(this.sslProtocols); + } else { + ss.setEnabledProtocols(ss.getSupportedProtocols()); + } + ss.setUseClientMode(false); + ss.setWantClientAuth(false); + ss.setNeedClientAuth(false); + return ss; + } - } + } - private static final String CONTENT_DISPOSITION_REGEX = "([ |\t]*Content-Disposition[ |\t]*:)(.*)"; + private static final String CONTENT_DISPOSITION_REGEX = "([ |\t]*Content-Disposition[ |\t]*:)(.*)"; - private static final Pattern CONTENT_DISPOSITION_PATTERN = Pattern.compile(CONTENT_DISPOSITION_REGEX, Pattern.CASE_INSENSITIVE); + private static final Pattern CONTENT_DISPOSITION_PATTERN = Pattern.compile(CONTENT_DISPOSITION_REGEX, Pattern.CASE_INSENSITIVE); - private static final String CONTENT_TYPE_REGEX = "([ |\t]*content-type[ |\t]*:)(.*)"; + private static final String CONTENT_TYPE_REGEX = "([ |\t]*content-type[ |\t]*:)(.*)"; - private static final Pattern CONTENT_TYPE_PATTERN = Pattern.compile(CONTENT_TYPE_REGEX, Pattern.CASE_INSENSITIVE); + private static final Pattern CONTENT_TYPE_PATTERN = Pattern.compile(CONTENT_TYPE_REGEX, Pattern.CASE_INSENSITIVE); - private static final String CONTENT_DISPOSITION_ATTRIBUTE_REGEX = "[ |\t]*([a-zA-Z]*)[ |\t]*=[ |\t]*['|\"]([^\"^']*)['|\"]"; + private static final String CONTENT_DISPOSITION_ATTRIBUTE_REGEX = "[ |\t]*([a-zA-Z]*)[ |\t]*=[ |\t]*['|\"]([^\"^']*)['|\"]"; - private static final Pattern CONTENT_DISPOSITION_ATTRIBUTE_PATTERN = Pattern.compile(CONTENT_DISPOSITION_ATTRIBUTE_REGEX); + private static final Pattern CONTENT_DISPOSITION_ATTRIBUTE_PATTERN = Pattern.compile(CONTENT_DISPOSITION_ATTRIBUTE_REGEX); - protected static class ContentType { + protected static class ContentType { - private static final String ASCII_ENCODING = "US-ASCII"; + private static final String ASCII_ENCODING = "US-ASCII"; - private static final String MULTIPART_FORM_DATA_HEADER = "multipart/form-data"; + private static final String MULTIPART_FORM_DATA_HEADER = "multipart/form-data"; - private static final String CONTENT_REGEX = "[ |\t]*([^/^ ^;^,]+/[^ ^;^,]+)"; + private static final String CONTENT_REGEX = "[ |\t]*([^/^ ^;^,]+/[^ ^;^,]+)"; - private static final Pattern MIME_PATTERN = Pattern.compile(CONTENT_REGEX, Pattern.CASE_INSENSITIVE); + 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 String CHARSET_REGEX = "[ |\t]*(charset)[ |\t]*=[ |\t]*['|\"]?([^\"^'^;^,]*)['|\"]?"; - private static final Pattern CHARSET_PATTERN = Pattern.compile(CHARSET_REGEX, Pattern.CASE_INSENSITIVE); + 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 String BOUNDARY_REGEX = "[ |\t]*(boundary)[ |\t]*=[ |\t]*['|\"]?([^\"^'^;^,]*)['|\"]?"; - private static final Pattern BOUNDARY_PATTERN = Pattern.compile(BOUNDARY_REGEX, Pattern.CASE_INSENSITIVE); + private static final Pattern BOUNDARY_PATTERN = Pattern.compile(BOUNDARY_REGEX, Pattern.CASE_INSENSITIVE); - private final String contentTypeHeader; + private final String contentTypeHeader; - private final String contentType; + private final String contentType; - private final String encoding; + private final String encoding; - private final String boundary; + 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; - } - } + 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; - } + 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 getContentTypeHeader() { + return contentTypeHeader; + } - public String getContentType() { - return contentType; - } + public String getContentType() { + return contentType; + } - public String getEncoding() { - return encoding == null ? ASCII_ENCODING : encoding; - } + public String getEncoding() { + return encoding == null ? ASCII_ENCODING : encoding; + } - public String getBoundary() { - return boundary; - } + public String getBoundary() { + return boundary; + } - public boolean isMultipart() { - return MULTIPART_FORM_DATA_HEADER.equalsIgnoreCase(contentType); - } + 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; - } - } + public ContentType tryUTF8() { + if (encoding == null) { + return new ContentType(this.contentTypeHeader + "; charset=UTF-8"); + } + return this; + } + } - protected class HTTPSession implements IHTTPSession { + protected class HTTPSession implements IHTTPSession { - private static final int REQUEST_BUFFER_LEN = 512; + private static final int REQUEST_BUFFER_LEN = 512; - private static final int MEMORY_STORE_LIMIT = 1024; + private static final int MEMORY_STORE_LIMIT = 1024; - public static final int BUFSIZE = 8192; + public static final int BUFSIZE = 8192; - public static final int MAX_HEADER_SIZE = 1024; + public static final int MAX_HEADER_SIZE = 1024; - private final TempFileManager tempFileManager; + private final TempFileManager tempFileManager; - private final OutputStream outputStream; + private final OutputStream outputStream; - private final BufferedInputStream inputStream; + private final BufferedInputStream inputStream; - private int splitbyte; + private int splitbyte; - private int rlen; + private int rlen; - private String uri; + private String uri; - private Method method; + private Method method; - private Map> parms; + private Map> parms; - private Map headers; + private Map headers; - private CookieHandler cookies; + private CookieHandler cookies; - private String queryParameterString; - - private String remoteIp; - - private String remoteHostname; - - private String protocolVersion; - - public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream) { - this.tempFileManager = tempFileManager; - this.inputStream = new BufferedInputStream(inputStream, HTTPSession.BUFSIZE); - this.outputStream = outputStream; - } - - public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream, InetAddress inetAddress) { - this.tempFileManager = tempFileManager; - this.inputStream = new BufferedInputStream(inputStream, HTTPSession.BUFSIZE); - this.outputStream = outputStream; - this.remoteIp = inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() ? "127.0.0.1" : inetAddress.getHostAddress().toString(); - this.remoteHostname = inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() ? "localhost" : inetAddress.getHostName().toString(); - this.headers = new HashMap(); - } - - /** - * Decodes the sent headers and loads the data into Key/value pairs - */ - private void decodeHeader(BufferedReader in, Map pre, Map> parms, Map 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(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html"); - } - - pre.put("method", st.nextToken()); - - if (!st.hasMoreTokens()) { - throw new ResponseException(Response.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(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe); - } - } - - /** - * Decodes the Multipart Body data and put it into Key/Value pairs. - */ - private void decodeMultipartFormData(ContentType contentType, ByteBuffer fbuf, Map> parms, Map files) throws ResponseException { - int pcount = 0; - try { - int[] boundaryIdxs = getBoundaryPositions(fbuf, contentType.getBoundary().getBytes()); - if (boundaryIdxs.length < 2) { - throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but contains less than two boundary strings."); - } - - byte[] partHeaderBuff = new byte[MAX_HEADER_SIZE]; - for (int boundaryIdx = 0; boundaryIdx < boundaryIdxs.length - 1; boundaryIdx++) { - fbuf.position(boundaryIdxs[boundaryIdx]); - int len = (fbuf.remaining() < MAX_HEADER_SIZE) ? fbuf.remaining() : MAX_HEADER_SIZE; - fbuf.get(partHeaderBuff, 0, len); - BufferedReader in = - new BufferedReader(new InputStreamReader(new ByteArrayInputStream(partHeaderBuff, 0, len), Charset.forName(contentType.getEncoding())), len); - - int headerLines = 0; - // First line is boundary string - String mpline = in.readLine(); - headerLines++; - if (mpline == null || !mpline.contains(contentType.getBoundary())) { - throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but chunk does not start with boundary."); - } - - String partName = null, fileName = null, partContentType = null; - // Parse the reset of the header lines - mpline = in.readLine(); - headerLines++; - while (mpline != null && mpline.trim().length() > 0) { - Matcher matcher = CONTENT_DISPOSITION_PATTERN.matcher(mpline); - if (matcher.matches()) { - String attributeString = matcher.group(2); - matcher = CONTENT_DISPOSITION_ATTRIBUTE_PATTERN.matcher(attributeString); - while (matcher.find()) { - String key = matcher.group(1); - if ("name".equalsIgnoreCase(key)) { - partName = matcher.group(2); - } else if ("filename".equalsIgnoreCase(key)) { - fileName = matcher.group(2); - // add these two line to support multiple - // files uploaded using the same field Id - if (!fileName.isEmpty()) { - if (pcount > 0) - partName = partName + String.valueOf(pcount++); - else - pcount++; - } - } - } - } - matcher = CONTENT_TYPE_PATTERN.matcher(mpline); - if (matcher.matches()) { - partContentType = matcher.group(2).trim(); - } - mpline = in.readLine(); - headerLines++; - } - int partHeaderLength = 0; - while (headerLines-- > 0) { - partHeaderLength = scipOverNewLine(partHeaderBuff, partHeaderLength); - } - // Read the part data - if (partHeaderLength >= len - 4) { - throw new ResponseException(Response.Status.INTERNAL_ERROR, "Multipart header size exceeds MAX_HEADER_SIZE."); - } - int partDataStart = boundaryIdxs[boundaryIdx] + partHeaderLength; - int partDataEnd = boundaryIdxs[boundaryIdx + 1] - 4; - - fbuf.position(partDataStart); - - List values = parms.get(partName); - if (values == null) { - values = new ArrayList(); - parms.put(partName, values); - } - - if (partContentType == null) { - // Read the part into a string - byte[] data_bytes = new byte[partDataEnd - partDataStart]; - fbuf.get(data_bytes); - - values.add(new String(data_bytes, contentType.getEncoding())); - } else { - // Read it into a file - String path = saveTmpFile(fbuf, partDataStart, partDataEnd - partDataStart, fileName); - if (!files.containsKey(partName)) { - files.put(partName, path); - } else { - int count = 2; - while (files.containsKey(partName + count)) { - count++; - } - files.put(partName + count, path); - } - values.add(fileName); - } - } - } catch (ResponseException re) { - throw re; - } catch (Exception e) { - throw new ResponseException(Response.Status.INTERNAL_ERROR, e.toString()); - } - } - - private int scipOverNewLine(byte[] partHeaderBuff, int index) { - while (partHeaderBuff[index] != '\n') { - index++; - } - return ++index; - } - - /** - * 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> 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 values = p.get(key); - if (values == null) { - values = new ArrayList(); - p.put(key, values); - } - - values.add(value); - } - } - - @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 (SSLException e) { - throw e; - } 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))); - - // Decode the header into parms and header java properties - Map pre = new HashMap(); - decodeHeader(hin, pre, this.parms, this.headers); - - if (null != this.remoteIp) { - this.headers.put("remote-addr", this.remoteIp); - this.headers.put("http-client-ip", this.remoteIp); - } - - this.method = Method.lookup(pre.get("method")); - if (this.method == null) { - throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error. HTTP verb " + pre.get("method") + " unhandled."); - } - - this.uri = pre.get("uri"); - - this.cookies = new CookieHandler(this.headers); - - String connection = this.headers.get("connection"); - boolean keepAlive = "HTTP/1.1".equals(protocolVersion) && (connection == null || !connection.matches("(?i).*close.*")); - - // Ok, now do the serve() - - // TODO: long body_size = getBodySize(); - // TODO: long pos_before_serve = this.inputStream.totalRead() - // (requires implementation for totalRead()) - r = serve(this); - // TODO: this.inputStream.skip(body_size - - // (this.inputStream.totalRead() - pos_before_serve)) - - if (r == null) { - throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response."); - } else { - String acceptEncoding = this.headers.get("accept-encoding"); - this.cookies.unloadQueue(r); - r.setRequestMethod(this.method); - r.setGzipEncoding(useGzipWhenAccepted(r) && acceptEncoding != null && acceptEncoding.contains("gzip")); - 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 (SSLException ssle) { - Response resp = newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SSL PROTOCOL FAILURE: " + ssle.getMessage()); - resp.send(this.outputStream); - safeClose(this.outputStream); - } catch (IOException ioe) { - Response resp = newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); - resp.send(this.outputStream); - safeClose(this.outputStream); - } catch (ResponseException re) { - Response resp = newFixedLengthResponse(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage()); - resp.send(this.outputStream); - safeClose(this.outputStream); - } finally { - safeClose(r); - this.tempFileManager.clear(); - } - } - - /** - * 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; - } - - /** - * Find the byte positions where multipart boundaries start. This reads - * a large block at a time and uses a temporary buffer to optimize - * (memory mapped) file access. - */ - private int[] getBoundaryPositions(ByteBuffer b, byte[] boundary) { - int[] res = new int[0]; - if (b.remaining() < boundary.length) { - return res; - } - - int search_window_pos = 0; - byte[] search_window = new byte[4 * 1024 + boundary.length]; - - int first_fill = (b.remaining() < search_window.length) ? b.remaining() : search_window.length; - b.get(search_window, 0, first_fill); - int new_bytes = first_fill - boundary.length; - - do { - // Search the search_window - for (int j = 0; j < new_bytes; j++) { - for (int i = 0; i < boundary.length; i++) { - if (search_window[j + i] != boundary[i]) - break; - if (i == boundary.length - 1) { - // Match found, add it to results - int[] new_res = new int[res.length + 1]; - System.arraycopy(res, 0, new_res, 0, res.length); - new_res[res.length] = search_window_pos + j; - res = new_res; - } - } - } - search_window_pos += new_bytes; - - // Copy the end of the buffer to the start - System.arraycopy(search_window, search_window.length - boundary.length, search_window, 0, boundary.length); - - // Refill search_window - new_bytes = search_window.length - boundary.length; - new_bytes = (b.remaining() < new_bytes) ? b.remaining() : new_bytes; - b.get(search_window, boundary.length, new_bytes); - } while (new_bytes > 0); - return res; - } - - @Override - public CookieHandler getCookies() { - return this.cookies; - } - - @Override - public final Map getHeaders() { - return this.headers; - } - - @Override - public final InputStream getInputStream() { - return this.inputStream; - } - - @Override - public final Method getMethod() { - return this.method; - } - - /** - * @deprecated use {@link #getParameters()} instead. - */ - @Override - @Deprecated - public final Map getParms() { - Map result = new HashMap(); - for (String key : this.parms.keySet()) { - result.put(key, this.parms.get(key).get(0)); - } - - return result; - } - - @Override - public final Map> getParameters() { - return this.parms; - } - - @Override - public String getQueryParameterString() { - return this.queryParameterString; - } - - private RandomAccessFile getTmpBucket() { - try { - TempFile tempFile = this.tempFileManager.createTempFile(null); - return new RandomAccessFile(tempFile.getName(), "rw"); - } catch (Exception e) { - throw new Error(e); // we won't recover, so throw an error - } - } - - @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 void parseBody(Map files) throws IOException, ResponseException { - RandomAccessFile randomAccessFile = null; - try { - long size = getBodySize(); - ByteArrayOutputStream baos = null; - DataOutput requestDataOutput = null; - - // Store the request in memory or a file, depending on size - if (size < MEMORY_STORE_LIMIT) { - baos = new ByteArrayOutputStream(); - requestDataOutput = new DataOutputStream(baos); - } else { - randomAccessFile = getTmpBucket(); - requestDataOutput = randomAccessFile; - } - - // Read all the body and write it to request_data_output - byte[] buf = new byte[REQUEST_BUFFER_LEN]; - while (this.rlen >= 0 && size > 0) { - this.rlen = this.inputStream.read(buf, 0, (int) Math.min(size, REQUEST_BUFFER_LEN)); - size -= this.rlen; - if (this.rlen > 0) { - requestDataOutput.write(buf, 0, this.rlen); - } - } - - ByteBuffer fbuf = null; - if (baos != null) { - fbuf = ByteBuffer.wrap(baos.toByteArray(), 0, baos.size()); - } else { - fbuf = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, randomAccessFile.length()); - randomAccessFile.seek(0); - } - - // If the method is POST, there may be parameters - // in data section, too, read it: - if (Method.POST.equals(this.method)) { - ContentType contentType = new ContentType(this.headers.get("content-type")); - if (contentType.isMultipart()) { - String boundary = contentType.getBoundary(); - if (boundary == null) { - throw new ResponseException(Response.Status.BAD_REQUEST, - "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html"); - } - decodeMultipartFormData(contentType, fbuf, this.parms, files); - } else { - byte[] postBytes = new byte[fbuf.remaining()]; - fbuf.get(postBytes); - String postLine = new String(postBytes, contentType.getEncoding()).trim(); - // Handle application/x-www-form-urlencoded - if ("application/x-www-form-urlencoded".equalsIgnoreCase(contentType.getContentType())) { - decodeParms(postLine, this.parms); - } else if (postLine.length() != 0) { - // Special case for raw POST data => create a - // special files entry "postData" with raw content - // data - files.put("postData", postLine); - } - } - } else if (Method.PUT.equals(this.method)) { - files.put("content", saveTmpFile(fbuf, 0, fbuf.limit(), null)); - } - } finally { - safeClose(randomAccessFile); - } - } - - /** - * Retrieves the content of a sent file and saves it to a temporary - * file. The full path to the saved file is returned. - */ - private String saveTmpFile(ByteBuffer b, int offset, int len, String filename_hint) { - String path = ""; - if (len > 0) { - FileOutputStream fileOutputStream = null; - try { - TempFile tempFile = this.tempFileManager.createTempFile(filename_hint); - ByteBuffer src = b.duplicate(); - fileOutputStream = new FileOutputStream(tempFile.getName()); - FileChannel dest = fileOutputStream.getChannel(); - src.position(offset).limit(offset + len); - dest.write(src.slice()); - path = tempFile.getName(); - } catch (Exception e) { // Catch exception if any - throw new Error(e); // we won't recover, so throw an error - } finally { - safeClose(fileOutputStream); - } - } - return path; - } - - @Override - public String getRemoteIpAddress() { - return this.remoteIp; - } - - @Override - public String getRemoteHostName() { - return this.remoteHostname; - } - } - - /** - * Handles one session, i.e. parses the HTTP request and returns the - * response. - */ - public interface IHTTPSession { - - void execute() throws IOException; - - CookieHandler getCookies(); - - Map getHeaders(); - - InputStream getInputStream(); - - Method getMethod(); - - /** - * This method will only return the first value for a given parameter. - * You will want to use getParameters if you expect multiple values for - * a given key. - * - * @deprecated use {@link #getParameters()} instead. - */ - @Deprecated - Map getParms(); - - Map> getParameters(); - - String getQueryParameterString(); - - /** - * @return the path part of the URL. - */ - String getUri(); - - /** - * Adds the files in the request body to the files map. - * - * @param files - * map to modify - */ - void parseBody(Map files) throws IOException, ResponseException; - - /** - * 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(); - } - - /** - * HTTP Request methods, with the ability to decode a String - * back to its enum value. - */ - public enum Method { - GET, - PUT, - POST, - DELETE, - HEAD, - OPTIONS, - TRACE, - CONNECT, - PATCH, - PROPFIND, - PROPPATCH, - MKCOL, - MOVE, - COPY, - LOCK, - UNLOCK; - - static Method lookup(String method) { - if (method == null) - return null; - - try { - return valueOf(method); - } catch (IllegalArgumentException e) { - // TODO: Log it? - return null; - } - } - } - - /** - * HTTP response. Return one of these from serve(). - */ - public static class Response implements Closeable { - - public interface IStatus { - - String getDescription(); - - int getRequestStatus(); - } - - /** - * 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"), - 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; - } - - } - - /** - * 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 - */ - private static 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()); - out.write(b, off, len); - out.write("\r\n".getBytes()); - } - - public void finish() throws IOException { - out.write("0\r\n\r\n".getBytes()); - } - - } - - /** - * 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. - */ - @SuppressWarnings("serial") - private final Map header = new HashMap() { - - 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 lowerCaseHeader = new HashMap(); - - /** - * The request method that spawned this response. - */ - private Method requestMethod; - - /** - * Use chunkedTransfer - */ - private boolean chunkedTransfer; - - private boolean encodeAsGzip; - - 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 Method getRequestMethod() { - return this.requestMethod; - } - - public IStatus getStatus() { - return this.status; - } - - public void setGzipEncoding(boolean encodeAsGzip) { - this.encodeAsGzip = encodeAsGzip; - } - - 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 entry : this.header.entrySet()) { - printHeader(pw, entry.getKey(), entry.getValue()); - } - if (getHeader("connection") == null) { - printHeader(pw, "Connection", (this.keepAlive ? "keep-alive" : "close")); - } - if (getHeader("content-length") != null) { - encodeAsGzip = false; - } - if (encodeAsGzip) { - printHeader(pw, "Content-Encoding", "gzip"); - setChunkedTransfer(true); - } - long pending = this.data != null ? this.contentLength : 0; - if (this.requestMethod != Method.HEAD && this.chunkedTransfer) { - printHeader(pw, "Transfer-Encoding", "chunked"); - } else if (!encodeAsGzip) { - pending = sendContentLengthHeaderIfNotAlreadyPresent(pw, pending); - } - pw.append("\r\n"); - pw.flush(); - sendBodyWithCorrectTransferAndEncoding(outputStream, pending); - outputStream.flush(); - safeClose(this.data); - } catch (IOException ioe) { - NanoHTTPD.LOG.log(Level.SEVERE, "Could not send response to the client", ioe); - } - } - - @SuppressWarnings("static-method") - 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) { - 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 (this.requestMethod != Method.HEAD && this.chunkedTransfer) { - ChunkedOutputStream chunkedOutputStream = new ChunkedOutputStream(outputStream); - sendBodyWithCorrectEncoding(chunkedOutputStream, -1); - chunkedOutputStream.finish(); - } else { - sendBodyWithCorrectEncoding(outputStream, pending); - } - } - - private void sendBodyWithCorrectEncoding(OutputStream outputStream, long pending) throws IOException { - if (encodeAsGzip) { - GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream); - sendBody(gzipOutputStream, -1); - gzipOutputStream.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(Method requestMethod) { - this.requestMethod = requestMethod; - } - - public void setStatus(IStatus status) { - this.status = status; - } - } - - public static final class ResponseException extends Exception { - - private static final long serialVersionUID = 6569838532917408380L; - - private final Response.Status status; - - public ResponseException(Response.Status status, String message) { - super(message); - this.status = status; - } - - public ResponseException(Response.Status status, String message, Exception e) { - super(message, e); - this.status = status; - } - - public Response.Status getStatus() { - return this.status; - } - } - - /** - * 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 { - final Socket finalAccept = NanoHTTPD.this.myServerSocket.accept(); - if (this.timeout > 0) { - finalAccept.setSoTimeout(this.timeout); - } - 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()); - } - } - - /** - * A temp file. - *

- *

- * Temp files are responsible for managing the actual temporary storage and - * cleaning themselves up when no longer needed. - *

- */ - public interface TempFile { - - public void delete() throws Exception; - - public String getName(); - - public OutputStream open() throws Exception; - } - - /** - * Temp file manager. - *

- *

- * Temp file managers are created 1-to-1 with incoming requests, to create - * and cleanup temporary files created as a result of handling the request. - *

- */ - public interface TempFileManager { - - void clear(); - - public TempFile createTempFile(String filename_hint) throws Exception; - } - - /** - * Factory to create temp file managers. - */ - public interface TempFileManagerFactory { - - public TempFileManager create(); - } - - /** - * 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"; - - /** - * Pseudo-Parameter to use to store the actual query string in the - * parameters map for later re-processing. - */ - private static final String QUERY_STRING_PARAMETER = "NanoHttpd.QUERY_STRING"; - - /** - * logger to log to. - */ - private static final Logger LOG = Logger.getLogger(NanoHTTPD.class.getName()); - - /** - * Hashtable mapping (String)FILENAME_EXTENSION -> (String)MIME_TYPE - */ - protected static Map MIME_TYPES; - - public static Map mimeTypes() { - if (MIME_TYPES == null) { - MIME_TYPES = new HashMap(); - loadMimeTypes(MIME_TYPES, "META-INF/nanohttpd/default-mimetypes.properties"); - loadMimeTypes(MIME_TYPES, "META-INF/nanohttpd/mimetypes.properties"); - if (MIME_TYPES.isEmpty()) { - LOG.log(Level.WARNING, "no mime types found in the classpath! please provide mimetypes.properties"); - } - } - return MIME_TYPES; - } - - @SuppressWarnings({ - "unchecked", - "rawtypes" - }) - private static void loadMimeTypes(Map result, String resourceName) { - try { - Enumeration resources = NanoHTTPD.class.getClassLoader().getResources(resourceName); - while (resources.hasMoreElements()) { - URL url = (URL) resources.nextElement(); - Properties properties = new Properties(); - InputStream stream = null; - try { - stream = url.openStream(); - properties.load(stream); - } catch (IOException e) { - LOG.log(Level.SEVERE, "could not load mimetypes from " + url, e); - } finally { - safeClose(stream); - } - result.putAll((Map) properties); - } - } catch (IOException e) { - LOG.log(Level.INFO, "no mime types available at " + resourceName); - } - }; - - /** - * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and an - * array of loaded KeyManagers. These objects must properly - * loaded/initialized by the caller. - */ - public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManager[] keyManagers) throws IOException { - SSLServerSocketFactory res = null; - try { - TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init(loadedKeyStore); - SSLContext ctx = SSLContext.getInstance("TLS"); - ctx.init(keyManagers, trustManagerFactory.getTrustManagers(), null); - res = ctx.getServerSocketFactory(); - } catch (Exception e) { - throw new IOException(e.getMessage()); - } - return res; - } - - /** - * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and a - * loaded KeyManagerFactory. These objects must properly loaded/initialized - * by the caller. - */ - public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManagerFactory loadedKeyFactory) throws IOException { - try { - return makeSSLSocketFactory(loadedKeyStore, loadedKeyFactory.getKeyManagers()); - } catch (Exception e) { - throw new IOException(e.getMessage()); - } - } - - /** - * Creates an SSLSocketFactory for HTTPS. Pass a KeyStore resource with your - * certificate and passphrase - */ - public static SSLServerSocketFactory makeSSLSocketFactory(String keyAndTrustStoreClasspathPath, char[] passphrase) throws IOException { - try { - KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); - InputStream keystoreStream = NanoHTTPD.class.getResourceAsStream(keyAndTrustStoreClasspathPath); - - if (keystoreStream == null) { - throw new IOException("Unable to load keystore from classpath: " + keyAndTrustStoreClasspathPath); - } - - keystore.load(keystoreStream, passphrase); - KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); - keyManagerFactory.init(keystore, passphrase); - return makeSSLSocketFactory(keystore, keyManagerFactory); - } catch (Exception e) { - throw new IOException(e.getMessage()); - } - } - - /** - * Get MIME type from file name extension, if possible - * - * @param uri - * the string representing a file - * @return the connected mime/type - */ - public static String getMimeTypeForFile(String uri) { - int dot = uri.lastIndexOf('.'); - String mime = null; - if (dot >= 0) { - mime = mimeTypes().get(uri.substring(dot + 1).toLowerCase()); - } - return mime == null ? "application/octet-stream" : mime; - } - - private 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; - - /** - * Pluggable strategy for creating and cleaning up temporary files. - */ - private TempFileManagerFactory tempFileManagerFactory; - - /** - * 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; - setTempFileManagerFactory(new DefaultTempFileManagerFactory()); - 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 parameters from a URL, handing the case where a single parameter - * name might have been supplied several times, by return lists of values. - * In general these lists will contain a single element. - * - * @param parms - * original NanoHTTPD parameters values, as passed to the - * serve() method. - * @return a map of String (parameter name) to - * List<String> (a list of the values supplied). - */ - protected static Map> decodeParameters(Map parms) { - return decodeParameters(parms.get(NanoHTTPD.QUERY_STRING_PARAMETER)); - } - - // ------------------------------------------------------------------------------- - // // - - /** - * Decode parameters from a URL, handing the case where a single parameter - * name might have been supplied several times, by return lists of values. - * In general these lists will contain a single element. - * - * @param queryString - * a query string pulled from the URL. - * @return a map of String (parameter name) to - * List<String> (a list of the values supplied). - */ - protected static Map> decodeParameters(String queryString) { - Map> parms = new HashMap>(); - if (queryString != null) { - StringTokenizer st = new StringTokenizer(queryString, "&"); - while (st.hasMoreTokens()) { - String e = st.nextToken(); - int sep = e.indexOf('='); - String propertyName = sep >= 0 ? decodePercent(e.substring(0, sep)).trim() : decodePercent(e).trim(); - if (!parms.containsKey(propertyName)) { - parms.put(propertyName, new ArrayList()); - } - String propertyValue = sep >= 0 ? decodePercent(e.substring(sep + 1)) : null; - if (propertyValue != null) { - parms.get(propertyName).add(propertyValue); - } - } - } - return parms; - } - - /** - * Decode percent encoded String values. - * - * @param str - * the percent encoded String - * @return expanded form of the input, for example "foo%20bar" becomes - * "foo bar" - */ - protected 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; - } - - /** - * @return true if the gzip compression should be used if the client - * accespts it. Default this option is on for text content and off - * for everything. Override this for custom semantics. - */ - @SuppressWarnings("static-method") - protected boolean useGzipWhenAccepted(Response r) { - return r.getMimeType() != null && (r.getMimeType().toLowerCase().contains("text/") || r.getMimeType().toLowerCase().contains("/json")); - } - - 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; - } - - public TempFileManagerFactory getTempFileManagerFactory() { - return tempFileManagerFactory; - } - - /** - * Call before start() to serve over HTTPS instead of HTTP - */ - public void makeSecure(SSLServerSocketFactory sslServerSocketFactory, String[] sslProtocols) { - this.serverSocketFactory = new SecureServerSocketFactory(sslServerSocketFactory, sslProtocols); - } - - /** - * Create a response with unknown length (using HTTP 1.1 chunking). - */ - public static Response newChunkedResponse(IStatus status, String mimeType, InputStream data) { - return new Response(status, mimeType, data, -1); - } - - /** - * Create a response with known length. - */ - public static Response newFixedLengthResponse(IStatus status, String mimeType, InputStream data, long totalBytes) { - return new Response(status, mimeType, data, totalBytes); - } - - /** - * Create a text response with known length. - */ - public static Response newFixedLengthResponse(IStatus status, String mimeType, String txt) { - ContentType contentType = new ContentType(mimeType); - if (txt == null) { - return newFixedLengthResponse(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 newFixedLengthResponse(status, contentType.getContentTypeHeader(), new ByteArrayInputStream(bytes), bytes.length); - } - } - - /** - * Create a text response with known length. - */ - public static Response newFixedLengthResponse(String msg) { - return newFixedLengthResponse(Status.OK, NanoHTTPD.MIME_HTML, msg); - } - - /** - * Override this to customize the server. - *

- *

- * (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) { - Map files = new HashMap(); - Method method = session.getMethod(); - if (Method.PUT.equals(method) || Method.POST.equals(method)) { - try { - session.parseBody(files); - } catch (IOException ioe) { - return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); - } catch (ResponseException re) { - return newFixedLengthResponse(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage()); - } - } - - Map parms = session.getParms(); - parms.put(NanoHTTPD.QUERY_STRING_PARAMETER, session.getQueryParameterString()); - return serve(session.getUri(), method, session.getHeaders(), parms, files); - } - - /** - * Override this to customize the server. - *

- *

- * (By default, this returns a 404 "Not Found" plain text error response.) - * - * @param uri - * Percent-decoded URI without parameters, for example - * "/index.cgi" - * @param method - * "GET", "POST" etc. - * @param parms - * Parsed, percent decoded parameters from URI and, in case of - * POST, data. - * @param headers - * Header entries, percent decoded - * @return HTTP response, see class Response for details - */ - @Deprecated - public Response serve(String uri, Method method, Map headers, Map parms, Map files) { - return newFixedLengthResponse(Response.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; - } - - /** - * Pluggable strategy for creating and cleaning up temporary files. - * - * @param tempFileManagerFactory - * new strategy for handling temp files. - */ - public void setTempFileManagerFactory(TempFileManagerFactory tempFileManagerFactory) { - this.tempFileManagerFactory = tempFileManagerFactory; - } - - /** - * 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; - } + private String queryParameterString; + + private String remoteIp; + + private String remoteHostname; + + private String protocolVersion; + + public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream) { + this.tempFileManager = tempFileManager; + this.inputStream = new BufferedInputStream(inputStream, HTTPSession.BUFSIZE); + this.outputStream = outputStream; + } + + public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream, InetAddress inetAddress) { + this.tempFileManager = tempFileManager; + this.inputStream = new BufferedInputStream(inputStream, HTTPSession.BUFSIZE); + this.outputStream = outputStream; + this.remoteIp = inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() ? "127.0.0.1" : inetAddress.getHostAddress().toString(); + this.remoteHostname = inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() ? "localhost" : inetAddress.getHostName().toString(); + this.headers = new HashMap<>(); + } + + /** + * Decodes the sent headers and loads the data into Key/value pairs + */ + private void decodeHeader(BufferedReader in, Map pre, Map> parms, Map 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(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html"); + } + + pre.put("method", st.nextToken()); + + if (!st.hasMoreTokens()) { + throw new ResponseException(Response.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(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe); + } + } + + /** + * Decodes the Multipart Body data and put it into Key/Value pairs. + */ + private void decodeMultipartFormData(ContentType contentType, ByteBuffer fbuf, Map> parms, Map files) throws ResponseException { + int pcount = 0; + try { + int[] boundaryIdxs = getBoundaryPositions(fbuf, contentType.getBoundary().getBytes()); + if (boundaryIdxs.length < 2) { + throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but contains less than two boundary strings."); + } + + byte[] partHeaderBuff = new byte[MAX_HEADER_SIZE]; + for (int boundaryIdx = 0; boundaryIdx < boundaryIdxs.length - 1; boundaryIdx++) { + fbuf.position(boundaryIdxs[boundaryIdx]); + int len = (fbuf.remaining() < MAX_HEADER_SIZE) ? fbuf.remaining() : MAX_HEADER_SIZE; + fbuf.get(partHeaderBuff, 0, len); + BufferedReader in = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(partHeaderBuff, 0, len), Charset.forName(contentType.getEncoding())), len); + + int headerLines = 0; + // First line is boundary string + String mpline = in.readLine(); + headerLines++; + if (mpline == null || !mpline.contains(contentType.getBoundary())) { + throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but chunk does not start with boundary."); + } + + String partName = null, fileName = null, partContentType = null; + // Parse the reset of the header lines + mpline = in.readLine(); + headerLines++; + while (mpline != null && mpline.trim().length() > 0) { + Matcher matcher = CONTENT_DISPOSITION_PATTERN.matcher(mpline); + if (matcher.matches()) { + String attributeString = matcher.group(2); + matcher = CONTENT_DISPOSITION_ATTRIBUTE_PATTERN.matcher(attributeString); + while (matcher.find()) { + String key = matcher.group(1); + if ("name".equalsIgnoreCase(key)) { + partName = matcher.group(2); + } else if ("filename".equalsIgnoreCase(key)) { + fileName = matcher.group(2); + // add these two line to support multiple + // files uploaded using the same field Id + if (!fileName.isEmpty()) { + if (pcount > 0) + partName = partName + String.valueOf(pcount++); + else + pcount++; + } + } + } + } + matcher = CONTENT_TYPE_PATTERN.matcher(mpline); + if (matcher.matches()) { + partContentType = matcher.group(2).trim(); + } + mpline = in.readLine(); + headerLines++; + } + int partHeaderLength = 0; + while (headerLines-- > 0) { + partHeaderLength = scipOverNewLine(partHeaderBuff, partHeaderLength); + } + // Read the part data + if (partHeaderLength >= len - 4) { + throw new ResponseException(Response.Status.INTERNAL_ERROR, "Multipart header size exceeds MAX_HEADER_SIZE."); + } + int partDataStart = boundaryIdxs[boundaryIdx] + partHeaderLength; + int partDataEnd = boundaryIdxs[boundaryIdx + 1] - 4; + + fbuf.position(partDataStart); + + List values = parms.get(partName); + if (values == null) { + values = new ArrayList<>(); + parms.put(partName, values); + } + + if (partContentType == null) { + // Read the part into a string + byte[] data_bytes = new byte[partDataEnd - partDataStart]; + fbuf.get(data_bytes); + + values.add(new String(data_bytes, contentType.getEncoding())); + } else { + // Read it into a file + String path = saveTmpFile(fbuf, partDataStart, partDataEnd - partDataStart, fileName); + if (!files.containsKey(partName)) { + files.put(partName, path); + } else { + int count = 2; + while (files.containsKey(partName + count)) { + count++; + } + files.put(partName + count, path); + } + values.add(fileName); + } + } + } catch (ResponseException re) { + throw re; + } catch (Exception e) { + throw new ResponseException(Response.Status.INTERNAL_ERROR, e.toString()); + } + } + + private int scipOverNewLine(byte[] partHeaderBuff, int index) { + while (partHeaderBuff[index] != '\n') { + index++; + } + return ++index; + } + + /** + * 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> 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 values = p.get(key); + if (values == null) { + values = new ArrayList<>(); + p.put(key, values); + } + + values.add(value); + } + } + + @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 (SSLException e) { + throw e; + } 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))); + + // Decode the header into parms and header java properties + Map pre = new HashMap<>(); + decodeHeader(hin, pre, this.parms, this.headers); + + if (null != this.remoteIp) { + this.headers.put("remote-addr", this.remoteIp); + this.headers.put("http-client-ip", this.remoteIp); + } + + this.method = Method.lookup(pre.get("method")); + if (this.method == null) { + throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error. HTTP verb " + pre.get("method") + " unhandled."); + } + + this.uri = pre.get("uri"); + + this.cookies = new CookieHandler(this.headers); + + String connection = this.headers.get("connection"); + boolean keepAlive = "HTTP/1.1".equals(protocolVersion) && (connection == null || !connection.matches("(?i).*close.*")); + + // Ok, now do the serve() + + // TODO: long body_size = getBodySize(); + // TODO: long pos_before_serve = this.inputStream.totalRead() + // (requires implementation for totalRead()) + r = serve(this); + // TODO: this.inputStream.skip(body_size - + // (this.inputStream.totalRead() - pos_before_serve)) + + if (r == null) { + throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response."); + } else { + String acceptEncoding = this.headers.get("accept-encoding"); + this.cookies.unloadQueue(r); + r.setRequestMethod(this.method); + r.setGzipEncoding(useGzipWhenAccepted(r) && acceptEncoding != null && acceptEncoding.contains("gzip")); + 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 (SSLException ssle) { + Response resp = newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SSL PROTOCOL FAILURE: " + ssle.getMessage()); + resp.send(this.outputStream); + safeClose(this.outputStream); + } catch (IOException ioe) { + Response resp = newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); + resp.send(this.outputStream); + safeClose(this.outputStream); + } catch (ResponseException re) { + Response resp = newFixedLengthResponse(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage()); + resp.send(this.outputStream); + safeClose(this.outputStream); + } finally { + safeClose(r); + this.tempFileManager.clear(); + } + } + + /** + * 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; + } + + /** + * Find the byte positions where multipart boundaries start. This reads + * a large block at a time and uses a temporary buffer to optimize + * (memory mapped) file access. + */ + private int[] getBoundaryPositions(ByteBuffer b, byte[] boundary) { + int[] res = new int[0]; + if (b.remaining() < boundary.length) { + return res; + } + + int search_window_pos = 0; + byte[] search_window = new byte[4 * 1024 + boundary.length]; + + int first_fill = (b.remaining() < search_window.length) ? b.remaining() : search_window.length; + b.get(search_window, 0, first_fill); + int new_bytes = first_fill - boundary.length; + + do { + // Search the search_window + for (int j = 0; j < new_bytes; j++) { + for (int i = 0; i < boundary.length; i++) { + if (search_window[j + i] != boundary[i]) + break; + if (i == boundary.length - 1) { + // Match found, add it to results + int[] new_res = new int[res.length + 1]; + System.arraycopy(res, 0, new_res, 0, res.length); + new_res[res.length] = search_window_pos + j; + res = new_res; + } + } + } + search_window_pos += new_bytes; + + // Copy the end of the buffer to the start + System.arraycopy(search_window, search_window.length - boundary.length, search_window, 0, boundary.length); + + // Refill search_window + new_bytes = search_window.length - boundary.length; + new_bytes = (b.remaining() < new_bytes) ? b.remaining() : new_bytes; + b.get(search_window, boundary.length, new_bytes); + } while (new_bytes > 0); + return res; + } + + @Override + public CookieHandler getCookies() { + return this.cookies; + } + + @Override + public final Map getHeaders() { + return this.headers; + } + + @Override + public final InputStream getInputStream() { + return this.inputStream; + } + + @Override + public final Method getMethod() { + return this.method; + } + + /** + * @deprecated use {@link #getParameters()} instead. + */ + @Override + @Deprecated + public final Map getParms() { + Map result = new HashMap<>(); + for (String key : this.parms.keySet()) { + result.put(key, this.parms.get(key).get(0)); + } + + return result; + } + + @Override + public final Map> getParameters() { + return this.parms; + } + + @Override + public String getQueryParameterString() { + return this.queryParameterString; + } + + private RandomAccessFile getTmpBucket() { + try { + TempFile tempFile = this.tempFileManager.createTempFile(null); + return new RandomAccessFile(tempFile.getName(), "rw"); + } catch (Exception e) { + throw new Error(e); // we won't recover, so throw an error + } + } + + @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 void parseBody(Map files) throws IOException, ResponseException { + RandomAccessFile randomAccessFile = null; + try { + long size = getBodySize(); + ByteArrayOutputStream baos = null; + DataOutput requestDataOutput = null; + + // Store the request in memory or a file, depending on size + if (size < MEMORY_STORE_LIMIT) { + baos = new ByteArrayOutputStream(); + requestDataOutput = new DataOutputStream(baos); + } else { + randomAccessFile = getTmpBucket(); + requestDataOutput = randomAccessFile; + } + + // Read all the body and write it to request_data_output + byte[] buf = new byte[REQUEST_BUFFER_LEN]; + while (this.rlen >= 0 && size > 0) { + this.rlen = this.inputStream.read(buf, 0, (int) Math.min(size, REQUEST_BUFFER_LEN)); + size -= this.rlen; + if (this.rlen > 0) { + requestDataOutput.write(buf, 0, this.rlen); + } + } + + ByteBuffer fbuf = null; + if (baos != null) { + fbuf = ByteBuffer.wrap(baos.toByteArray(), 0, baos.size()); + } else { + fbuf = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, randomAccessFile.length()); + randomAccessFile.seek(0); + } + + // If the method is POST, there may be parameters + // in data section, too, read it: + if (Method.POST.equals(this.method)) { + ContentType contentType = new ContentType(this.headers.get("content-type")); + if (contentType.isMultipart()) { + String boundary = contentType.getBoundary(); + if (boundary == null) { + throw new ResponseException(Response.Status.BAD_REQUEST, + "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html"); + } + decodeMultipartFormData(contentType, fbuf, this.parms, files); + } else { + byte[] postBytes = new byte[fbuf.remaining()]; + fbuf.get(postBytes); + String postLine = new String(postBytes, contentType.getEncoding()).trim(); + // Handle application/x-www-form-urlencoded + if ("application/x-www-form-urlencoded".equalsIgnoreCase(contentType.getContentType())) { + decodeParms(postLine, this.parms); + } else if (postLine.length() != 0) { + // Special case for raw POST data => create a + // special files entry "postData" with raw content + // data + files.put("postData", postLine); + } + } + } else if (Method.PUT.equals(this.method)) { + files.put("content", saveTmpFile(fbuf, 0, fbuf.limit(), null)); + } + } finally { + safeClose(randomAccessFile); + } + } + + /** + * Retrieves the content of a sent file and saves it to a temporary + * file. The full path to the saved file is returned. + */ + private String saveTmpFile(ByteBuffer b, int offset, int len, String filename_hint) { + String path = ""; + if (len > 0) { + FileOutputStream fileOutputStream = null; + try { + TempFile tempFile = this.tempFileManager.createTempFile(filename_hint); + ByteBuffer src = b.duplicate(); + fileOutputStream = new FileOutputStream(tempFile.getName()); + FileChannel dest = fileOutputStream.getChannel(); + src.position(offset).limit(offset + len); + dest.write(src.slice()); + path = tempFile.getName(); + } catch (Exception e) { // Catch exception if any + throw new Error(e); // we won't recover, so throw an error + } finally { + safeClose(fileOutputStream); + } + } + return path; + } + + @Override + public String getRemoteIpAddress() { + return this.remoteIp; + } + + @Override + public String getRemoteHostName() { + return this.remoteHostname; + } + } + + /** + * Handles one session, i.e. parses the HTTP request and returns the + * response. + */ + public interface IHTTPSession { + + void execute() throws IOException; + + CookieHandler getCookies(); + + Map getHeaders(); + + InputStream getInputStream(); + + Method getMethod(); + + /** + * This method will only return the first value for a given parameter. + * You will want to use getParameters if you expect multiple values for + * a given key. + * + * @deprecated use {@link #getParameters()} instead. + */ + @Deprecated + Map getParms(); + + Map> getParameters(); + + String getQueryParameterString(); + + /** + * @return the path part of the URL. + */ + String getUri(); + + /** + * Adds the files in the request body to the files map. + * + * @param files + * map to modify + */ + void parseBody(Map files) throws IOException, ResponseException; + + /** + * 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(); + } + + /** + * HTTP Request methods, with the ability to decode a String + * back to its enum value. + */ + public enum Method { + GET, + PUT, + POST, + DELETE, + HEAD, + OPTIONS, + TRACE, + CONNECT, + PATCH, + PROPFIND, + PROPPATCH, + MKCOL, + MOVE, + COPY, + LOCK, + UNLOCK; + + static Method lookup(String method) { + if (method == null) + return null; + + try { + return valueOf(method); + } catch (IllegalArgumentException e) { + // TODO: Log it? + return null; + } + } + } + + /** + * HTTP response. Return one of these from serve(). + */ + public static class Response implements Closeable { + + public interface IStatus { + + String getDescription(); + + int getRequestStatus(); + } + + /** + * 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"), + 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; + } + + } + + /** + * 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 + */ + private static 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()); + out.write(b, off, len); + out.write("\r\n".getBytes()); + } + + public void finish() throws IOException { + out.write("0\r\n\r\n".getBytes()); + } + + } + + /** + * 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 header = new HashMap() { + + @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 lowerCaseHeader = new HashMap<>(); + + /** + * The request method that spawned this response. + */ + private Method requestMethod; + + /** + * Use chunkedTransfer + */ + private boolean chunkedTransfer; + + private boolean encodeAsGzip; + + 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 Method getRequestMethod() { + return this.requestMethod; + } + + public IStatus getStatus() { + return this.status; + } + + public void setGzipEncoding(boolean encodeAsGzip) { + this.encodeAsGzip = encodeAsGzip; + } + + 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 entry : this.header.entrySet()) { + printHeader(pw, entry.getKey(), entry.getValue()); + } + if (getHeader("connection") == null) { + printHeader(pw, "Connection", (this.keepAlive ? "keep-alive" : "close")); + } + if (getHeader("content-length") != null) { + encodeAsGzip = false; + } + if (encodeAsGzip) { + printHeader(pw, "Content-Encoding", "gzip"); + setChunkedTransfer(true); + } + long pending = this.data != null ? this.contentLength : 0; + if (this.requestMethod != Method.HEAD && this.chunkedTransfer) { + printHeader(pw, "Transfer-Encoding", "chunked"); + } else if (!encodeAsGzip) { + pending = sendContentLengthHeaderIfNotAlreadyPresent(pw, pending); + } + pw.append("\r\n"); + pw.flush(); + sendBodyWithCorrectTransferAndEncoding(outputStream, pending); + outputStream.flush(); + safeClose(this.data); + } catch (IOException ioe) { + NanoHTTPD.LOG.log(Level.SEVERE, "Could not send response to the client", ioe); + } + } + + @SuppressWarnings("static-method") + 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) { + 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 (this.requestMethod != Method.HEAD && this.chunkedTransfer) { + ChunkedOutputStream chunkedOutputStream = new ChunkedOutputStream(outputStream); + sendBodyWithCorrectEncoding(chunkedOutputStream, -1); + chunkedOutputStream.finish(); + } else { + sendBodyWithCorrectEncoding(outputStream, pending); + } + } + + private void sendBodyWithCorrectEncoding(OutputStream outputStream, long pending) throws IOException { + if (encodeAsGzip) { + GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream); + sendBody(gzipOutputStream, -1); + gzipOutputStream.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(Method requestMethod) { + this.requestMethod = requestMethod; + } + + public void setStatus(IStatus status) { + this.status = status; + } + } + + public static final class ResponseException extends Exception { + + private static final long serialVersionUID = 6569838532917408380L; + + private final Response.Status status; + + public ResponseException(Response.Status status, String message) { + super(message); + this.status = status; + } + + public ResponseException(Response.Status status, String message, Exception e) { + super(message, e); + this.status = status; + } + + public Response.Status getStatus() { + return this.status; + } + } + + /** + * 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 { + final Socket finalAccept = NanoHTTPD.this.myServerSocket.accept(); + if (this.timeout > 0) { + finalAccept.setSoTimeout(this.timeout); + } + 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()); + } + } + + /** + * A temp file. + *

+ *

+ * Temp files are responsible for managing the actual temporary storage and + * cleaning themselves up when no longer needed. + *

+ */ + public interface TempFile { + + public void delete() throws Exception; + + public String getName(); + + public OutputStream open() throws Exception; + } + + /** + * Temp file manager. + *

+ *

+ * Temp file managers are created 1-to-1 with incoming requests, to create + * and cleanup temporary files created as a result of handling the request. + *

+ */ + public interface TempFileManager { + + void clear(); + + public TempFile createTempFile(String filename_hint) throws Exception; + } + + /** + * Factory to create temp file managers. + */ + public interface TempFileManagerFactory { + + public TempFileManager create(); + } + + /** + * 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"; + + /** + * Pseudo-Parameter to use to store the actual query string in the + * parameters map for later re-processing. + */ + private static final String QUERY_STRING_PARAMETER = "NanoHttpd.QUERY_STRING"; + + /** + * logger to log to. + */ + private static final Logger LOG = Logger.getLogger(NanoHTTPD.class.getName()); + + /** + * Hashtable mapping (String)FILENAME_EXTENSION -> (String)MIME_TYPE + */ + protected static Map MIME_TYPES; + + public static Map mimeTypes() { + if (MIME_TYPES == null) { + MIME_TYPES = new HashMap<>(); + loadMimeTypes(MIME_TYPES, "META-INF/nanohttpd/default-mimetypes.properties"); + loadMimeTypes(MIME_TYPES, "META-INF/nanohttpd/mimetypes.properties"); + if (MIME_TYPES.isEmpty()) { + LOG.log(Level.WARNING, "no mime types found in the classpath! please provide mimetypes.properties"); + } + } + return MIME_TYPES; + } + + @SuppressWarnings({ + "unchecked", + "rawtypes" + }) + private static void loadMimeTypes(Map result, String resourceName) { + try { + Enumeration resources = NanoHTTPD.class.getClassLoader().getResources(resourceName); + while (resources.hasMoreElements()) { + URL url = resources.nextElement(); + Properties properties = new Properties(); + InputStream stream = null; + try { + stream = url.openStream(); + properties.load(stream); + } catch (IOException e) { + LOG.log(Level.SEVERE, "could not load mimetypes from " + url, e); + } finally { + safeClose(stream); + } + result.putAll((Map) properties); + } + } catch (IOException e) { + LOG.log(Level.INFO, "no mime types available at " + resourceName); + } + }; + + /** + * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and an + * array of loaded KeyManagers. These objects must properly + * loaded/initialized by the caller. + */ + public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManager[] keyManagers) throws IOException { + SSLServerSocketFactory res = null; + try { + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(loadedKeyStore); + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init(keyManagers, trustManagerFactory.getTrustManagers(), null); + res = ctx.getServerSocketFactory(); + } catch (Exception e) { + throw new IOException(e.getMessage()); + } + return res; + } + + /** + * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and a + * loaded KeyManagerFactory. These objects must properly loaded/initialized + * by the caller. + */ + public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManagerFactory loadedKeyFactory) throws IOException { + try { + return makeSSLSocketFactory(loadedKeyStore, loadedKeyFactory.getKeyManagers()); + } catch (Exception e) { + throw new IOException(e.getMessage()); + } + } + + /** + * Creates an SSLSocketFactory for HTTPS. Pass a KeyStore resource with your + * certificate and passphrase + */ + public static SSLServerSocketFactory makeSSLSocketFactory(String keyAndTrustStoreClasspathPath, char[] passphrase) throws IOException { + try { + KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); + InputStream keystoreStream = NanoHTTPD.class.getResourceAsStream(keyAndTrustStoreClasspathPath); + + if (keystoreStream == null) { + throw new IOException("Unable to load keystore from classpath: " + keyAndTrustStoreClasspathPath); + } + + keystore.load(keystoreStream, passphrase); + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(keystore, passphrase); + return makeSSLSocketFactory(keystore, keyManagerFactory); + } catch (Exception e) { + throw new IOException(e.getMessage()); + } + } + + /** + * Get MIME type from file name extension, if possible + * + * @param uri + * the string representing a file + * @return the connected mime/type + */ + public static String getMimeTypeForFile(String uri) { + int dot = uri.lastIndexOf('.'); + String mime = null; + if (dot >= 0) { + mime = mimeTypes().get(uri.substring(dot + 1).toLowerCase()); + } + return mime == null ? "application/octet-stream" : mime; + } + + private 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; + + /** + * Pluggable strategy for creating and cleaning up temporary files. + */ + private TempFileManagerFactory tempFileManagerFactory; + + /** + * 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; + setTempFileManagerFactory(new DefaultTempFileManagerFactory()); + 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 parameters from a URL, handing the case where a single parameter + * name might have been supplied several times, by return lists of values. + * In general these lists will contain a single element. + * + * @param parms + * original NanoHTTPD parameters values, as passed to the + * serve() method. + * @return a map of String (parameter name) to + * List<String> (a list of the values supplied). + */ + protected static Map> decodeParameters(Map parms) { + return decodeParameters(parms.get(NanoHTTPD.QUERY_STRING_PARAMETER)); + } + + // ------------------------------------------------------------------------------- + // // + + /** + * Decode parameters from a URL, handing the case where a single parameter + * name might have been supplied several times, by return lists of values. + * In general these lists will contain a single element. + * + * @param queryString + * a query string pulled from the URL. + * @return a map of String (parameter name) to + * List<String> (a list of the values supplied). + */ + protected static Map> decodeParameters(String queryString) { + Map> parms = new HashMap<>(); + if (queryString != null) { + StringTokenizer st = new StringTokenizer(queryString, "&"); + while (st.hasMoreTokens()) { + String e = st.nextToken(); + int sep = e.indexOf('='); + String propertyName = sep >= 0 ? decodePercent(e.substring(0, sep)).trim() : decodePercent(e).trim(); + if (!parms.containsKey(propertyName)) { + parms.put(propertyName, new ArrayList()); + } + String propertyValue = sep >= 0 ? decodePercent(e.substring(sep + 1)) : null; + if (propertyValue != null) { + parms.get(propertyName).add(propertyValue); + } + } + } + return parms; + } + + /** + * Decode percent encoded String values. + * + * @param str + * the percent encoded String + * @return expanded form of the input, for example "foo%20bar" becomes + * "foo bar" + */ + protected 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; + } + + /** + * @return true if the gzip compression should be used if the client + * accespts it. Default this option is on for text content and off + * for everything. Override this for custom semantics. + */ + @SuppressWarnings("static-method") + protected boolean useGzipWhenAccepted(Response r) { + return r.getMimeType() != null && (r.getMimeType().toLowerCase().contains("text/") || r.getMimeType().toLowerCase().contains("/json")); + } + + 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; + } + + public TempFileManagerFactory getTempFileManagerFactory() { + return tempFileManagerFactory; + } + + /** + * Call before start() to serve over HTTPS instead of HTTP + */ + public void makeSecure(SSLServerSocketFactory sslServerSocketFactory, String[] sslProtocols) { + this.serverSocketFactory = new SecureServerSocketFactory(sslServerSocketFactory, sslProtocols); + } + + /** + * Create a response with unknown length (using HTTP 1.1 chunking). + */ + public static Response newChunkedResponse(IStatus status, String mimeType, InputStream data) { + return new Response(status, mimeType, data, -1); + } + + /** + * Create a response with known length. + */ + public static Response newFixedLengthResponse(IStatus status, String mimeType, InputStream data, long totalBytes) { + return new Response(status, mimeType, data, totalBytes); + } + + /** + * Create a text response with known length. + */ + public static Response newFixedLengthResponse(IStatus status, String mimeType, String txt) { + ContentType contentType = new ContentType(mimeType); + if (txt == null) { + return newFixedLengthResponse(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 newFixedLengthResponse(status, contentType.getContentTypeHeader(), new ByteArrayInputStream(bytes), bytes.length); + } + } + + /** + * Create a text response with known length. + */ + public static Response newFixedLengthResponse(String msg) { + return newFixedLengthResponse(Status.OK, NanoHTTPD.MIME_HTML, msg); + } + + /** + * Override this to customize the server. + *

+ *

+ * (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) { + Map files = new HashMap<>(); + Method method = session.getMethod(); + if (Method.PUT.equals(method) || Method.POST.equals(method)) { + try { + session.parseBody(files); + } catch (IOException ioe) { + return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); + } catch (ResponseException re) { + return newFixedLengthResponse(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage()); + } + } + + Map parms = session.getParms(); + parms.put(NanoHTTPD.QUERY_STRING_PARAMETER, session.getQueryParameterString()); + return serve(session.getUri(), method, session.getHeaders(), parms, files); + } + + /** + * Override this to customize the server. + *

+ *

+ * (By default, this returns a 404 "Not Found" plain text error response.) + * + * @param uri + * Percent-decoded URI without parameters, for example + * "/index.cgi" + * @param method + * "GET", "POST" etc. + * @param parms + * Parsed, percent decoded parameters from URI and, in case of + * POST, data. + * @param headers + * Header entries, percent decoded + * @return HTTP response, see class Response for details + */ + @Deprecated + public Response serve(String uri, Method method, Map headers, Map parms, Map files) { + return newFixedLengthResponse(Response.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; + } + + /** + * Pluggable strategy for creating and cleaning up temporary files. + * + * @param tempFileManagerFactory + * new strategy for handling temp files. + */ + public void setTempFileManagerFactory(TempFileManagerFactory tempFileManagerFactory) { + this.tempFileManagerFactory = tempFileManagerFactory; + } + + /** + * 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; + } } From 352fb146672cd810951968e6ae797d4b62cee83a Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Sun, 30 Dec 2018 16:51:51 +0800 Subject: [PATCH 07/16] [nanohttpd]crop --- .../internal/fi/iki/elonen/NanoHTTPD.java | 982 +----------------- .../nanohttpd/default-mimetypes.properties | 30 - 2 files changed, 18 insertions(+), 994 deletions(-) delete mode 100644 src/main/resources/META-INF/nanohttpd/default-mimetypes.properties diff --git a/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/NanoHTTPD.java b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/NanoHTTPD.java index 34b88dc..da84d12 100644 --- a/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/NanoHTTPD.java +++ b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/NanoHTTPD.java @@ -37,12 +37,7 @@ import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.Closeable; -import java.io.DataOutput; -import java.io.DataOutputStream; -import java.io.File; -import java.io.FileOutputStream; import java.io.FilterOutputStream; import java.io.IOException; import java.io.InputStream; @@ -50,7 +45,6 @@ import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; -import java.io.RandomAccessFile; import java.io.UnsupportedEncodingException; import java.net.InetAddress; import java.net.InetSocketAddress; @@ -58,41 +52,24 @@ import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; import java.net.SocketTimeoutException; -import java.net.URL; import java.net.URLDecoder; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; import java.nio.charset.Charset; import java.nio.charset.CharsetEncoder; -import java.security.KeyStore; import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Calendar; import java.util.Collections; import java.util.Date; -import java.util.Enumeration; import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; -import java.util.Properties; import java.util.StringTokenizer; import java.util.TimeZone; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.zip.GZIPOutputStream; - -import javax.net.ssl.KeyManager; -import javax.net.ssl.KeyManagerFactory; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLException; -import javax.net.ssl.SSLServerSocket; -import javax.net.ssl.SSLServerSocketFactory; -import javax.net.ssl.TrustManagerFactory; import moe.yushi.authlibinjector.internal.fi.iki.elonen.NanoHTTPD.Response.IStatus; import moe.yushi.authlibinjector.internal.fi.iki.elonen.NanoHTTPD.Response.Status; @@ -186,8 +163,7 @@ public abstract class NanoHTTPD { OutputStream outputStream = null; try { outputStream = this.acceptSocket.getOutputStream(); - TempFileManager tempFileManager = NanoHTTPD.this.tempFileManagerFactory.create(); - HTTPSession session = new HTTPSession(tempFileManager, this.inputStream, outputStream, this.acceptSocket.getInetAddress()); + HTTPSession session = new HTTPSession(this.inputStream, outputStream, this.acceptSocket.getInetAddress()); while (!this.acceptSocket.isClosed()) { session.execute(); } @@ -211,126 +187,6 @@ public abstract class NanoHTTPD { } } - public static class Cookie { - - public static String getHTTPTime(int days) { - Calendar calendar = Calendar.getInstance(); - SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); - dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); - calendar.add(Calendar.DAY_OF_MONTH, days); - return dateFormat.format(calendar.getTime()); - } - - private final String n, v, e; - - public Cookie(String name, String value) { - this(name, value, 30); - } - - public Cookie(String name, String value, int numDays) { - this.n = name; - this.v = value; - this.e = getHTTPTime(numDays); - } - - public Cookie(String name, String value, String expires) { - this.n = name; - this.v = value; - this.e = expires; - } - - public String getHTTPHeader() { - String fmt = "%s=%s; expires=%s"; - return String.format(fmt, this.n, this.v, this.e); - } - } - - /** - * Provides rudimentary support for cookies. Doesn't support 'path', - * 'secure' nor 'httpOnly'. Feel free to improve it and/or add unsupported - * features. - * - * @author LordFokas - */ - public class CookieHandler implements Iterable { - - private final HashMap cookies = new HashMap<>(); - - private final ArrayList queue = new ArrayList<>(); - - public CookieHandler(Map httpHeaders) { - String raw = httpHeaders.get("cookie"); - if (raw != null) { - String[] tokens = raw.split(";"); - for (String token : tokens) { - String[] data = token.trim().split("="); - if (data.length == 2) { - this.cookies.put(data[0], data[1]); - } - } - } - } - - /** - * Set a cookie with an expiration date from a month ago, effectively - * deleting it on the client side. - * - * @param name - * The cookie name. - */ - public void delete(String name) { - set(name, "-delete-", -30); - } - - @Override - public Iterator iterator() { - return this.cookies.keySet().iterator(); - } - - /** - * Read a cookie from the HTTP Headers. - * - * @param name - * The cookie's name. - * @return The cookie's value if it exists, null otherwise. - */ - public String read(String name) { - return this.cookies.get(name); - } - - public void set(Cookie cookie) { - this.queue.add(cookie); - } - - /** - * Sets a cookie. - * - * @param name - * The cookie's name. - * @param value - * The cookie's value. - * @param expires - * How many days until the cookie expires. - */ - public void set(String name, String value, int expires) { - this.queue.add(new Cookie(name, value, Cookie.getHTTPTime(expires))); - } - - /** - * Internally used by the webserver to add all queued cookies into the - * Response's HTTP Headers. - * - * @param response - * The Response object to which headers the queued cookies - * will be added. - */ - public void unloadQueue(Response response) { - for (Cookie cookie : this.queue) { - response.addHeader("Set-Cookie", cookie.getHTTPHeader()); - } - } - } - /** * Default threading strategy for NanoHTTPD. *

@@ -377,99 +233,6 @@ public abstract class NanoHTTPD { } } - /** - * Default strategy for creating and cleaning up temporary files. - *

- *

- * By default, files are created by File.createTempFile() in - * the directory specified. - *

- */ - public static class DefaultTempFile implements TempFile { - - private final File file; - - private final OutputStream fstream; - - public DefaultTempFile(File tempdir) throws IOException { - this.file = File.createTempFile("NanoHTTPD-", "", tempdir); - this.fstream = new FileOutputStream(this.file); - } - - @Override - public void delete() throws Exception { - safeClose(this.fstream); - if (!this.file.delete()) { - throw new Exception("could not delete temporary file: " + this.file.getAbsolutePath()); - } - } - - @Override - public String getName() { - return this.file.getAbsolutePath(); - } - - @Override - public OutputStream open() throws Exception { - return this.fstream; - } - } - - /** - * Default strategy for creating and cleaning up temporary files. - *

- *

- * This class stores its files in the standard location (that is, wherever - * java.io.tmpdir points to). Files are added to an internal - * list, and deleted when no longer needed (that is, when - * clear() is invoked at the end of processing a request). - *

- */ - public static class DefaultTempFileManager implements TempFileManager { - - private final File tmpdir; - - private final List tempFiles; - - public DefaultTempFileManager() { - this.tmpdir = new File(System.getProperty("java.io.tmpdir")); - if (!tmpdir.exists()) { - tmpdir.mkdirs(); - } - this.tempFiles = new ArrayList<>(); - } - - @Override - public void clear() { - for (TempFile file : this.tempFiles) { - try { - file.delete(); - } catch (Exception ignored) { - NanoHTTPD.LOG.log(Level.WARNING, "could not delete file ", ignored); - } - } - this.tempFiles.clear(); - } - - @Override - public TempFile createTempFile(String filename_hint) throws Exception { - DefaultTempFile tempFile = new DefaultTempFile(this.tmpdir); - this.tempFiles.add(tempFile); - return tempFile; - } - } - - /** - * Default strategy for creating and cleaning up temporary files. - */ - private class DefaultTempFileManagerFactory implements TempFileManagerFactory { - - @Override - public TempFileManager create() { - return new DefaultTempFileManager(); - } - } - /** * Creates a normal ServerSocket for TCP connections */ @@ -482,49 +245,6 @@ public abstract class NanoHTTPD { } - /** - * Creates a new SSLServerSocket - */ - public static class SecureServerSocketFactory implements ServerSocketFactory { - - private SSLServerSocketFactory sslServerSocketFactory; - - private String[] sslProtocols; - - public SecureServerSocketFactory(SSLServerSocketFactory sslServerSocketFactory, String[] sslProtocols) { - this.sslServerSocketFactory = sslServerSocketFactory; - this.sslProtocols = sslProtocols; - } - - @Override - public ServerSocket create() throws IOException { - SSLServerSocket ss = null; - ss = (SSLServerSocket) this.sslServerSocketFactory.createServerSocket(); - if (this.sslProtocols != null) { - ss.setEnabledProtocols(this.sslProtocols); - } else { - ss.setEnabledProtocols(ss.getSupportedProtocols()); - } - ss.setUseClientMode(false); - ss.setWantClientAuth(false); - ss.setNeedClientAuth(false); - return ss; - } - - } - - private static final String CONTENT_DISPOSITION_REGEX = "([ |\t]*Content-Disposition[ |\t]*:)(.*)"; - - private static final Pattern CONTENT_DISPOSITION_PATTERN = Pattern.compile(CONTENT_DISPOSITION_REGEX, Pattern.CASE_INSENSITIVE); - - private static final String CONTENT_TYPE_REGEX = "([ |\t]*content-type[ |\t]*:)(.*)"; - - private static final Pattern CONTENT_TYPE_PATTERN = Pattern.compile(CONTENT_TYPE_REGEX, Pattern.CASE_INSENSITIVE); - - private static final String CONTENT_DISPOSITION_ATTRIBUTE_REGEX = "[ |\t]*([a-zA-Z]*)[ |\t]*=[ |\t]*['|\"]([^\"^']*)['|\"]"; - - private static final Pattern CONTENT_DISPOSITION_ATTRIBUTE_PATTERN = Pattern.compile(CONTENT_DISPOSITION_ATTRIBUTE_REGEX); - protected static class ContentType { private static final String ASCII_ENCODING = "US-ASCII"; @@ -602,16 +322,10 @@ public abstract class NanoHTTPD { protected class HTTPSession implements IHTTPSession { - private static final int REQUEST_BUFFER_LEN = 512; - - private static final int MEMORY_STORE_LIMIT = 1024; - public static final int BUFSIZE = 8192; public static final int MAX_HEADER_SIZE = 1024; - private final TempFileManager tempFileManager; - private final OutputStream outputStream; private final BufferedInputStream inputStream; @@ -622,14 +336,12 @@ public abstract class NanoHTTPD { private String uri; - private Method method; + private String method; private Map> parms; private Map headers; - private CookieHandler cookies; - private String queryParameterString; private String remoteIp; @@ -638,14 +350,12 @@ public abstract class NanoHTTPD { private String protocolVersion; - public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream) { - this.tempFileManager = tempFileManager; + public HTTPSession(InputStream inputStream, OutputStream outputStream) { this.inputStream = new BufferedInputStream(inputStream, HTTPSession.BUFSIZE); this.outputStream = outputStream; } - public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream, InetAddress inetAddress) { - this.tempFileManager = tempFileManager; + 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().toString(); @@ -711,119 +421,6 @@ public abstract class NanoHTTPD { } } - /** - * Decodes the Multipart Body data and put it into Key/Value pairs. - */ - private void decodeMultipartFormData(ContentType contentType, ByteBuffer fbuf, Map> parms, Map files) throws ResponseException { - int pcount = 0; - try { - int[] boundaryIdxs = getBoundaryPositions(fbuf, contentType.getBoundary().getBytes()); - if (boundaryIdxs.length < 2) { - throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but contains less than two boundary strings."); - } - - byte[] partHeaderBuff = new byte[MAX_HEADER_SIZE]; - for (int boundaryIdx = 0; boundaryIdx < boundaryIdxs.length - 1; boundaryIdx++) { - fbuf.position(boundaryIdxs[boundaryIdx]); - int len = (fbuf.remaining() < MAX_HEADER_SIZE) ? fbuf.remaining() : MAX_HEADER_SIZE; - fbuf.get(partHeaderBuff, 0, len); - BufferedReader in = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(partHeaderBuff, 0, len), Charset.forName(contentType.getEncoding())), len); - - int headerLines = 0; - // First line is boundary string - String mpline = in.readLine(); - headerLines++; - if (mpline == null || !mpline.contains(contentType.getBoundary())) { - throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but chunk does not start with boundary."); - } - - String partName = null, fileName = null, partContentType = null; - // Parse the reset of the header lines - mpline = in.readLine(); - headerLines++; - while (mpline != null && mpline.trim().length() > 0) { - Matcher matcher = CONTENT_DISPOSITION_PATTERN.matcher(mpline); - if (matcher.matches()) { - String attributeString = matcher.group(2); - matcher = CONTENT_DISPOSITION_ATTRIBUTE_PATTERN.matcher(attributeString); - while (matcher.find()) { - String key = matcher.group(1); - if ("name".equalsIgnoreCase(key)) { - partName = matcher.group(2); - } else if ("filename".equalsIgnoreCase(key)) { - fileName = matcher.group(2); - // add these two line to support multiple - // files uploaded using the same field Id - if (!fileName.isEmpty()) { - if (pcount > 0) - partName = partName + String.valueOf(pcount++); - else - pcount++; - } - } - } - } - matcher = CONTENT_TYPE_PATTERN.matcher(mpline); - if (matcher.matches()) { - partContentType = matcher.group(2).trim(); - } - mpline = in.readLine(); - headerLines++; - } - int partHeaderLength = 0; - while (headerLines-- > 0) { - partHeaderLength = scipOverNewLine(partHeaderBuff, partHeaderLength); - } - // Read the part data - if (partHeaderLength >= len - 4) { - throw new ResponseException(Response.Status.INTERNAL_ERROR, "Multipart header size exceeds MAX_HEADER_SIZE."); - } - int partDataStart = boundaryIdxs[boundaryIdx] + partHeaderLength; - int partDataEnd = boundaryIdxs[boundaryIdx + 1] - 4; - - fbuf.position(partDataStart); - - List values = parms.get(partName); - if (values == null) { - values = new ArrayList<>(); - parms.put(partName, values); - } - - if (partContentType == null) { - // Read the part into a string - byte[] data_bytes = new byte[partDataEnd - partDataStart]; - fbuf.get(data_bytes); - - values.add(new String(data_bytes, contentType.getEncoding())); - } else { - // Read it into a file - String path = saveTmpFile(fbuf, partDataStart, partDataEnd - partDataStart, fileName); - if (!files.containsKey(partName)) { - files.put(partName, path); - } else { - int count = 2; - while (files.containsKey(partName + count)) { - count++; - } - files.put(partName + count, path); - } - values.add(fileName); - } - } - } catch (ResponseException re) { - throw re; - } catch (Exception e) { - throw new ResponseException(Response.Status.INTERNAL_ERROR, e.toString()); - } - } - - private int scipOverNewLine(byte[] partHeaderBuff, int index) { - while (partHeaderBuff[index] != '\n') { - index++; - } - return ++index; - } - /** * Decodes parameters in percent-encoded URI-format ( e.g. * "name=Jack%20Daniels&pass=Single%20Malt" ) and adds them to given @@ -861,6 +458,7 @@ public abstract class NanoHTTPD { } } + @SuppressWarnings("resource") @Override public void execute() throws IOException { Response r = null; @@ -878,8 +476,6 @@ public abstract class NanoHTTPD { this.inputStream.mark(HTTPSession.BUFSIZE); try { read = this.inputStream.read(buf, 0, HTTPSession.BUFSIZE); - } catch (SSLException e) { - throw e; } catch (IOException e) { safeClose(this.inputStream); safeClose(this.outputStream); @@ -919,20 +515,10 @@ public abstract class NanoHTTPD { Map pre = new HashMap<>(); decodeHeader(hin, pre, this.parms, this.headers); - if (null != this.remoteIp) { - this.headers.put("remote-addr", this.remoteIp); - this.headers.put("http-client-ip", this.remoteIp); - } - - this.method = Method.lookup(pre.get("method")); - if (this.method == null) { - throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error. HTTP verb " + pre.get("method") + " unhandled."); - } + this.method = pre.get("method"); this.uri = pre.get("uri"); - this.cookies = new CookieHandler(this.headers); - String connection = this.headers.get("connection"); boolean keepAlive = "HTTP/1.1".equals(protocolVersion) && (connection == null || !connection.matches("(?i).*close.*")); @@ -948,10 +534,7 @@ public abstract class NanoHTTPD { if (r == null) { throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response."); } else { - String acceptEncoding = this.headers.get("accept-encoding"); - this.cookies.unloadQueue(r); r.setRequestMethod(this.method); - r.setGzipEncoding(useGzipWhenAccepted(r) && acceptEncoding != null && acceptEncoding.contains("gzip")); r.setKeepAlive(keepAlive); r.send(this.outputStream); } @@ -966,10 +549,6 @@ public abstract class NanoHTTPD { // i.e. close the stream & finalAccept object by throwing the // exception up the call stack. throw ste; - } catch (SSLException ssle) { - Response resp = newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SSL PROTOCOL FAILURE: " + ssle.getMessage()); - resp.send(this.outputStream); - safeClose(this.outputStream); } catch (IOException ioe) { Response resp = newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); resp.send(this.outputStream); @@ -980,7 +559,6 @@ public abstract class NanoHTTPD { safeClose(this.outputStream); } finally { safeClose(r); - this.tempFileManager.clear(); } } @@ -1006,57 +584,6 @@ public abstract class NanoHTTPD { return 0; } - /** - * Find the byte positions where multipart boundaries start. This reads - * a large block at a time and uses a temporary buffer to optimize - * (memory mapped) file access. - */ - private int[] getBoundaryPositions(ByteBuffer b, byte[] boundary) { - int[] res = new int[0]; - if (b.remaining() < boundary.length) { - return res; - } - - int search_window_pos = 0; - byte[] search_window = new byte[4 * 1024 + boundary.length]; - - int first_fill = (b.remaining() < search_window.length) ? b.remaining() : search_window.length; - b.get(search_window, 0, first_fill); - int new_bytes = first_fill - boundary.length; - - do { - // Search the search_window - for (int j = 0; j < new_bytes; j++) { - for (int i = 0; i < boundary.length; i++) { - if (search_window[j + i] != boundary[i]) - break; - if (i == boundary.length - 1) { - // Match found, add it to results - int[] new_res = new int[res.length + 1]; - System.arraycopy(res, 0, new_res, 0, res.length); - new_res[res.length] = search_window_pos + j; - res = new_res; - } - } - } - search_window_pos += new_bytes; - - // Copy the end of the buffer to the start - System.arraycopy(search_window, search_window.length - boundary.length, search_window, 0, boundary.length); - - // Refill search_window - new_bytes = search_window.length - boundary.length; - new_bytes = (b.remaining() < new_bytes) ? b.remaining() : new_bytes; - b.get(search_window, boundary.length, new_bytes); - } while (new_bytes > 0); - return res; - } - - @Override - public CookieHandler getCookies() { - return this.cookies; - } - @Override public final Map getHeaders() { return this.headers; @@ -1068,24 +595,10 @@ public abstract class NanoHTTPD { } @Override - public final Method getMethod() { + public final String getMethod() { return this.method; } - /** - * @deprecated use {@link #getParameters()} instead. - */ - @Override - @Deprecated - public final Map getParms() { - Map result = new HashMap<>(); - for (String key : this.parms.keySet()) { - result.put(key, this.parms.get(key).get(0)); - } - - return result; - } - @Override public final Map> getParameters() { return this.parms; @@ -1096,15 +609,6 @@ public abstract class NanoHTTPD { return this.queryParameterString; } - private RandomAccessFile getTmpBucket() { - try { - TempFile tempFile = this.tempFileManager.createTempFile(null); - return new RandomAccessFile(tempFile.getName(), "rw"); - } catch (Exception e) { - throw new Error(e); // we won't recover, so throw an error - } - } - @Override public final String getUri() { return this.uri; @@ -1123,99 +627,6 @@ public abstract class NanoHTTPD { return 0; } - @Override - public void parseBody(Map files) throws IOException, ResponseException { - RandomAccessFile randomAccessFile = null; - try { - long size = getBodySize(); - ByteArrayOutputStream baos = null; - DataOutput requestDataOutput = null; - - // Store the request in memory or a file, depending on size - if (size < MEMORY_STORE_LIMIT) { - baos = new ByteArrayOutputStream(); - requestDataOutput = new DataOutputStream(baos); - } else { - randomAccessFile = getTmpBucket(); - requestDataOutput = randomAccessFile; - } - - // Read all the body and write it to request_data_output - byte[] buf = new byte[REQUEST_BUFFER_LEN]; - while (this.rlen >= 0 && size > 0) { - this.rlen = this.inputStream.read(buf, 0, (int) Math.min(size, REQUEST_BUFFER_LEN)); - size -= this.rlen; - if (this.rlen > 0) { - requestDataOutput.write(buf, 0, this.rlen); - } - } - - ByteBuffer fbuf = null; - if (baos != null) { - fbuf = ByteBuffer.wrap(baos.toByteArray(), 0, baos.size()); - } else { - fbuf = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, randomAccessFile.length()); - randomAccessFile.seek(0); - } - - // If the method is POST, there may be parameters - // in data section, too, read it: - if (Method.POST.equals(this.method)) { - ContentType contentType = new ContentType(this.headers.get("content-type")); - if (contentType.isMultipart()) { - String boundary = contentType.getBoundary(); - if (boundary == null) { - throw new ResponseException(Response.Status.BAD_REQUEST, - "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html"); - } - decodeMultipartFormData(contentType, fbuf, this.parms, files); - } else { - byte[] postBytes = new byte[fbuf.remaining()]; - fbuf.get(postBytes); - String postLine = new String(postBytes, contentType.getEncoding()).trim(); - // Handle application/x-www-form-urlencoded - if ("application/x-www-form-urlencoded".equalsIgnoreCase(contentType.getContentType())) { - decodeParms(postLine, this.parms); - } else if (postLine.length() != 0) { - // Special case for raw POST data => create a - // special files entry "postData" with raw content - // data - files.put("postData", postLine); - } - } - } else if (Method.PUT.equals(this.method)) { - files.put("content", saveTmpFile(fbuf, 0, fbuf.limit(), null)); - } - } finally { - safeClose(randomAccessFile); - } - } - - /** - * Retrieves the content of a sent file and saves it to a temporary - * file. The full path to the saved file is returned. - */ - private String saveTmpFile(ByteBuffer b, int offset, int len, String filename_hint) { - String path = ""; - if (len > 0) { - FileOutputStream fileOutputStream = null; - try { - TempFile tempFile = this.tempFileManager.createTempFile(filename_hint); - ByteBuffer src = b.duplicate(); - fileOutputStream = new FileOutputStream(tempFile.getName()); - FileChannel dest = fileOutputStream.getChannel(); - src.position(offset).limit(offset + len); - dest.write(src.slice()); - path = tempFile.getName(); - } catch (Exception e) { // Catch exception if any - throw new Error(e); // we won't recover, so throw an error - } finally { - safeClose(fileOutputStream); - } - } - return path; - } - @Override public String getRemoteIpAddress() { return this.remoteIp; @@ -1235,23 +646,11 @@ public abstract class NanoHTTPD { void execute() throws IOException; - CookieHandler getCookies(); - Map getHeaders(); InputStream getInputStream(); - Method getMethod(); - - /** - * This method will only return the first value for a given parameter. - * You will want to use getParameters if you expect multiple values for - * a given key. - * - * @deprecated use {@link #getParameters()} instead. - */ - @Deprecated - Map getParms(); + String getMethod(); Map> getParameters(); @@ -1262,14 +661,6 @@ public abstract class NanoHTTPD { */ String getUri(); - /** - * Adds the files in the request body to the files map. - * - * @param files - * map to modify - */ - void parseBody(Map files) throws IOException, ResponseException; - /** * Get the remote ip address of the requester. * @@ -1285,41 +676,6 @@ public abstract class NanoHTTPD { String getRemoteHostName(); } - /** - * HTTP Request methods, with the ability to decode a String - * back to its enum value. - */ - public enum Method { - GET, - PUT, - POST, - DELETE, - HEAD, - OPTIONS, - TRACE, - CONNECT, - PATCH, - PROPFIND, - PROPPATCH, - MKCOL, - MOVE, - COPY, - LOCK, - UNLOCK; - - static Method lookup(String method) { - if (method == null) - return null; - - try { - return valueOf(method); - } catch (IllegalArgumentException e) { - // TODO: Log it? - return null; - } - } - } - /** * HTTP response. Return one of these from serve(). */ @@ -1488,15 +844,13 @@ public abstract class NanoHTTPD { /** * The request method that spawned this response. */ - private Method requestMethod; + private String requestMethod; /** * Use chunkedTransfer */ private boolean chunkedTransfer; - private boolean encodeAsGzip; - private boolean keepAlive; /** @@ -1564,7 +918,7 @@ public abstract class NanoHTTPD { return this.mimeType; } - public Method getRequestMethod() { + public String getRequestMethod() { return this.requestMethod; } @@ -1572,10 +926,6 @@ public abstract class NanoHTTPD { return this.status; } - public void setGzipEncoding(boolean encodeAsGzip) { - this.encodeAsGzip = encodeAsGzip; - } - public void setKeepAlive(boolean useKeepAlive) { this.keepAlive = useKeepAlive; } @@ -1605,17 +955,10 @@ public abstract class NanoHTTPD { if (getHeader("connection") == null) { printHeader(pw, "Connection", (this.keepAlive ? "keep-alive" : "close")); } - if (getHeader("content-length") != null) { - encodeAsGzip = false; - } - if (encodeAsGzip) { - printHeader(pw, "Content-Encoding", "gzip"); - setChunkedTransfer(true); - } long pending = this.data != null ? this.contentLength : 0; - if (this.requestMethod != Method.HEAD && this.chunkedTransfer) { + if (!"HEAD".equals(this.requestMethod) && this.chunkedTransfer) { printHeader(pw, "Transfer-Encoding", "chunked"); - } else if (!encodeAsGzip) { + } else { pending = sendContentLengthHeaderIfNotAlreadyPresent(pw, pending); } pw.append("\r\n"); @@ -1628,7 +971,6 @@ public abstract class NanoHTTPD { } } - @SuppressWarnings("static-method") protected void printHeader(PrintWriter pw, String key, String value) { pw.append(key).append(": ").append(value).append("\r\n"); } @@ -1648,20 +990,11 @@ public abstract class NanoHTTPD { } private void sendBodyWithCorrectTransferAndEncoding(OutputStream outputStream, long pending) throws IOException { - if (this.requestMethod != Method.HEAD && this.chunkedTransfer) { + if (!"HEAD".equals(this.requestMethod) && this.chunkedTransfer) { + @SuppressWarnings("resource") ChunkedOutputStream chunkedOutputStream = new ChunkedOutputStream(outputStream); - sendBodyWithCorrectEncoding(chunkedOutputStream, -1); + sendBody(chunkedOutputStream, -1); chunkedOutputStream.finish(); - } else { - sendBodyWithCorrectEncoding(outputStream, pending); - } - } - - private void sendBodyWithCorrectEncoding(OutputStream outputStream, long pending) throws IOException { - if (encodeAsGzip) { - GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream); - sendBody(gzipOutputStream, -1); - gzipOutputStream.finish(); } else { sendBody(outputStream, pending); } @@ -1709,7 +1042,7 @@ public abstract class NanoHTTPD { this.mimeType = mimeType; } - public void setRequestMethod(Method requestMethod) { + public void setRequestMethod(String requestMethod) { this.requestMethod = requestMethod; } @@ -1765,10 +1098,12 @@ public abstract class NanoHTTPD { } 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) { @@ -1778,46 +1113,6 @@ public abstract class NanoHTTPD { } } - /** - * A temp file. - *

- *

- * Temp files are responsible for managing the actual temporary storage and - * cleaning themselves up when no longer needed. - *

- */ - public interface TempFile { - - public void delete() throws Exception; - - public String getName(); - - public OutputStream open() throws Exception; - } - - /** - * Temp file manager. - *

- *

- * Temp file managers are created 1-to-1 with incoming requests, to create - * and cleanup temporary files created as a result of handling the request. - *

- */ - public interface TempFileManager { - - void clear(); - - public TempFile createTempFile(String filename_hint) throws Exception; - } - - /** - * Factory to create temp file managers. - */ - public interface TempFileManagerFactory { - - public TempFileManager create(); - } - /** * Factory to create ServerSocketFactories. */ @@ -1844,130 +1139,11 @@ public abstract class NanoHTTPD { */ public static final String MIME_HTML = "text/html"; - /** - * Pseudo-Parameter to use to store the actual query string in the - * parameters map for later re-processing. - */ - private static final String QUERY_STRING_PARAMETER = "NanoHttpd.QUERY_STRING"; - /** * logger to log to. */ private static final Logger LOG = Logger.getLogger(NanoHTTPD.class.getName()); - /** - * Hashtable mapping (String)FILENAME_EXTENSION -> (String)MIME_TYPE - */ - protected static Map MIME_TYPES; - - public static Map mimeTypes() { - if (MIME_TYPES == null) { - MIME_TYPES = new HashMap<>(); - loadMimeTypes(MIME_TYPES, "META-INF/nanohttpd/default-mimetypes.properties"); - loadMimeTypes(MIME_TYPES, "META-INF/nanohttpd/mimetypes.properties"); - if (MIME_TYPES.isEmpty()) { - LOG.log(Level.WARNING, "no mime types found in the classpath! please provide mimetypes.properties"); - } - } - return MIME_TYPES; - } - - @SuppressWarnings({ - "unchecked", - "rawtypes" - }) - private static void loadMimeTypes(Map result, String resourceName) { - try { - Enumeration resources = NanoHTTPD.class.getClassLoader().getResources(resourceName); - while (resources.hasMoreElements()) { - URL url = resources.nextElement(); - Properties properties = new Properties(); - InputStream stream = null; - try { - stream = url.openStream(); - properties.load(stream); - } catch (IOException e) { - LOG.log(Level.SEVERE, "could not load mimetypes from " + url, e); - } finally { - safeClose(stream); - } - result.putAll((Map) properties); - } - } catch (IOException e) { - LOG.log(Level.INFO, "no mime types available at " + resourceName); - } - }; - - /** - * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and an - * array of loaded KeyManagers. These objects must properly - * loaded/initialized by the caller. - */ - public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManager[] keyManagers) throws IOException { - SSLServerSocketFactory res = null; - try { - TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init(loadedKeyStore); - SSLContext ctx = SSLContext.getInstance("TLS"); - ctx.init(keyManagers, trustManagerFactory.getTrustManagers(), null); - res = ctx.getServerSocketFactory(); - } catch (Exception e) { - throw new IOException(e.getMessage()); - } - return res; - } - - /** - * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and a - * loaded KeyManagerFactory. These objects must properly loaded/initialized - * by the caller. - */ - public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManagerFactory loadedKeyFactory) throws IOException { - try { - return makeSSLSocketFactory(loadedKeyStore, loadedKeyFactory.getKeyManagers()); - } catch (Exception e) { - throw new IOException(e.getMessage()); - } - } - - /** - * Creates an SSLSocketFactory for HTTPS. Pass a KeyStore resource with your - * certificate and passphrase - */ - public static SSLServerSocketFactory makeSSLSocketFactory(String keyAndTrustStoreClasspathPath, char[] passphrase) throws IOException { - try { - KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); - InputStream keystoreStream = NanoHTTPD.class.getResourceAsStream(keyAndTrustStoreClasspathPath); - - if (keystoreStream == null) { - throw new IOException("Unable to load keystore from classpath: " + keyAndTrustStoreClasspathPath); - } - - keystore.load(keystoreStream, passphrase); - KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); - keyManagerFactory.init(keystore, passphrase); - return makeSSLSocketFactory(keystore, keyManagerFactory); - } catch (Exception e) { - throw new IOException(e.getMessage()); - } - } - - /** - * Get MIME type from file name extension, if possible - * - * @param uri - * the string representing a file - * @return the connected mime/type - */ - public static String getMimeTypeForFile(String uri) { - int dot = uri.lastIndexOf('.'); - String mime = null; - if (dot >= 0) { - mime = mimeTypes().get(uri.substring(dot + 1).toLowerCase()); - } - return mime == null ? "application/octet-stream" : mime; - } - private static final void safeClose(Object closeable) { try { if (closeable != null) { @@ -2001,11 +1177,6 @@ public abstract class NanoHTTPD { */ protected AsyncRunner asyncRunner; - /** - * Pluggable strategy for creating and cleaning up temporary files. - */ - private TempFileManagerFactory tempFileManagerFactory; - /** * Constructs an HTTP server on given port. */ @@ -2027,7 +1198,6 @@ public abstract class NanoHTTPD { public NanoHTTPD(String hostname, int port) { this.hostname = hostname; this.myPort = port; - setTempFileManagerFactory(new DefaultTempFileManagerFactory()); setAsyncRunner(new DefaultAsyncRunner()); } @@ -2064,54 +1234,6 @@ public abstract class NanoHTTPD { return new ServerRunnable(timeout); } - /** - * Decode parameters from a URL, handing the case where a single parameter - * name might have been supplied several times, by return lists of values. - * In general these lists will contain a single element. - * - * @param parms - * original NanoHTTPD parameters values, as passed to the - * serve() method. - * @return a map of String (parameter name) to - * List<String> (a list of the values supplied). - */ - protected static Map> decodeParameters(Map parms) { - return decodeParameters(parms.get(NanoHTTPD.QUERY_STRING_PARAMETER)); - } - - // ------------------------------------------------------------------------------- - // // - - /** - * Decode parameters from a URL, handing the case where a single parameter - * name might have been supplied several times, by return lists of values. - * In general these lists will contain a single element. - * - * @param queryString - * a query string pulled from the URL. - * @return a map of String (parameter name) to - * List<String> (a list of the values supplied). - */ - protected static Map> decodeParameters(String queryString) { - Map> parms = new HashMap<>(); - if (queryString != null) { - StringTokenizer st = new StringTokenizer(queryString, "&"); - while (st.hasMoreTokens()) { - String e = st.nextToken(); - int sep = e.indexOf('='); - String propertyName = sep >= 0 ? decodePercent(e.substring(0, sep)).trim() : decodePercent(e).trim(); - if (!parms.containsKey(propertyName)) { - parms.put(propertyName, new ArrayList()); - } - String propertyValue = sep >= 0 ? decodePercent(e.substring(sep + 1)) : null; - if (propertyValue != null) { - parms.get(propertyName).add(propertyValue); - } - } - } - return parms; - } - /** * Decode percent encoded String values. * @@ -2130,16 +1252,6 @@ public abstract class NanoHTTPD { return decoded; } - /** - * @return true if the gzip compression should be used if the client - * accespts it. Default this option is on for text content and off - * for everything. Override this for custom semantics. - */ - @SuppressWarnings("static-method") - protected boolean useGzipWhenAccepted(Response r) { - return r.getMimeType() != null && (r.getMimeType().toLowerCase().contains("text/") || r.getMimeType().toLowerCase().contains("/json")); - } - public final int getListeningPort() { return this.myServerSocket == null ? -1 : this.myServerSocket.getLocalPort(); } @@ -2160,17 +1272,6 @@ public abstract class NanoHTTPD { return hostname; } - public TempFileManagerFactory getTempFileManagerFactory() { - return tempFileManagerFactory; - } - - /** - * Call before start() to serve over HTTPS instead of HTTP - */ - public void makeSecure(SSLServerSocketFactory sslServerSocketFactory, String[] sslProtocols) { - this.serverSocketFactory = new SecureServerSocketFactory(sslServerSocketFactory, sslProtocols); - } - /** * Create a response with unknown length (using HTTP 1.1 chunking). */ @@ -2226,43 +1327,6 @@ public abstract class NanoHTTPD { * @return HTTP response, see class Response for details */ public Response serve(IHTTPSession session) { - Map files = new HashMap<>(); - Method method = session.getMethod(); - if (Method.PUT.equals(method) || Method.POST.equals(method)) { - try { - session.parseBody(files); - } catch (IOException ioe) { - return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); - } catch (ResponseException re) { - return newFixedLengthResponse(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage()); - } - } - - Map parms = session.getParms(); - parms.put(NanoHTTPD.QUERY_STRING_PARAMETER, session.getQueryParameterString()); - return serve(session.getUri(), method, session.getHeaders(), parms, files); - } - - /** - * Override this to customize the server. - *

- *

- * (By default, this returns a 404 "Not Found" plain text error response.) - * - * @param uri - * Percent-decoded URI without parameters, for example - * "/index.cgi" - * @param method - * "GET", "POST" etc. - * @param parms - * Parsed, percent decoded parameters from URI and, in case of - * POST, data. - * @param headers - * Header entries, percent decoded - * @return HTTP response, see class Response for details - */ - @Deprecated - public Response serve(String uri, Method method, Map headers, Map parms, Map files) { return newFixedLengthResponse(Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, "Not Found"); } @@ -2276,16 +1340,6 @@ public abstract class NanoHTTPD { this.asyncRunner = asyncRunner; } - /** - * Pluggable strategy for creating and cleaning up temporary files. - * - * @param tempFileManagerFactory - * new strategy for handling temp files. - */ - public void setTempFileManagerFactory(TempFileManagerFactory tempFileManagerFactory) { - this.tempFileManagerFactory = tempFileManagerFactory; - } - /** * Start the server. * diff --git a/src/main/resources/META-INF/nanohttpd/default-mimetypes.properties b/src/main/resources/META-INF/nanohttpd/default-mimetypes.properties deleted file mode 100644 index 3fb242f..0000000 --- a/src/main/resources/META-INF/nanohttpd/default-mimetypes.properties +++ /dev/null @@ -1,30 +0,0 @@ -#default mime types for nanohttpd, use META-INF/mimetypes.properties for user defined mimetypes -css=text/css -htm=text/html -html=text/html -xml=text/xml -java=text/x-java-source, text/java -md=text/plain -txt=text/plain -asc=text/plain -gif=image/gif -jpg=image/jpeg -jpeg=image/jpeg -png=image/png -svg=image/svg+xml -mp3=audio/mpeg -m3u=audio/mpeg-url -mp4=video/mp4 -ogv=video/ogg -flv=video/x-flv -mov=video/quicktime -swf=application/x-shockwave-flash -js=application/javascript -pdf=application/pdf -doc=application/msword -ogg=application/x-ogg -zip=application/octet-stream -exe=application/octet-stream -class=application/octet-stream -m3u8=application/vnd.apple.mpegurl -ts=video/mp2t \ No newline at end of file From f31e9f53dd134f33026da58223dee5f399f46b85 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Sun, 30 Dec 2018 18:15:48 +0800 Subject: [PATCH 08/16] [nanohttpd]split into multi files --- .../httpd/LegacySkinAPIFilter.java | 11 +- .../authlibinjector/httpd/URLFilter.java | 4 +- .../authlibinjector/httpd/URLProcessor.java | 10 +- .../fi/iki/elonen/ChunkedOutputStream.java | 43 ++ .../internal/fi/iki/elonen/ContentType.java | 79 +++ .../internal/fi/iki/elonen/IHTTPSession.java | 44 ++ .../internal/fi/iki/elonen/IStatus.java | 8 + .../internal/fi/iki/elonen/NanoHTTPD.java | 630 +----------------- .../internal/fi/iki/elonen/Response.java | 316 +++++++++ .../fi/iki/elonen/ResponseException.java | 20 + .../internal/fi/iki/elonen/Status.java | 78 +++ 11 files changed, 611 insertions(+), 632 deletions(-) create mode 100644 src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/ChunkedOutputStream.java create mode 100644 src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/ContentType.java create mode 100644 src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/IHTTPSession.java create mode 100644 src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/IStatus.java create mode 100644 src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/Response.java create mode 100644 src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/ResponseException.java create mode 100644 src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/Status.java diff --git a/src/main/java/moe/yushi/authlibinjector/httpd/LegacySkinAPIFilter.java b/src/main/java/moe/yushi/authlibinjector/httpd/LegacySkinAPIFilter.java index 1d8f244..bf0f1db 100644 --- a/src/main/java/moe/yushi/authlibinjector/httpd/LegacySkinAPIFilter.java +++ b/src/main/java/moe/yushi/authlibinjector/httpd/LegacySkinAPIFilter.java @@ -3,7 +3,6 @@ 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.internal.fi.iki.elonen.NanoHTTPD.newFixedLengthResponse; import static moe.yushi.authlibinjector.util.IOUtils.asString; import static moe.yushi.authlibinjector.util.IOUtils.getURL; import static moe.yushi.authlibinjector.util.IOUtils.newUncheckedIOException; @@ -19,9 +18,9 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import moe.yushi.authlibinjector.YggdrasilConfiguration; -import moe.yushi.authlibinjector.internal.fi.iki.elonen.NanoHTTPD.IHTTPSession; -import moe.yushi.authlibinjector.internal.fi.iki.elonen.NanoHTTPD.Response; -import moe.yushi.authlibinjector.internal.fi.iki.elonen.NanoHTTPD.Response.Status; +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; @@ -73,11 +72,11 @@ public class LegacySkinAPIFilter implements URLFilter { throw newUncheckedIOException("Failed to retrieve skin from " + url, e); } Logging.HTTPD.info("Retrieved skin for " + username + " from " + url + ", " + data.length + " bytes"); - return of(newFixedLengthResponse(Status.OK, "image/png", new ByteArrayInputStream(data), data.length)); + 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(newFixedLengthResponse(Status.NOT_FOUND, null, null)); + return of(Response.newFixedLength(Status.NOT_FOUND, null, null)); } } diff --git a/src/main/java/moe/yushi/authlibinjector/httpd/URLFilter.java b/src/main/java/moe/yushi/authlibinjector/httpd/URLFilter.java index 6f1d32a..083e5b9 100644 --- a/src/main/java/moe/yushi/authlibinjector/httpd/URLFilter.java +++ b/src/main/java/moe/yushi/authlibinjector/httpd/URLFilter.java @@ -2,8 +2,8 @@ package moe.yushi.authlibinjector.httpd; import java.util.Optional; -import moe.yushi.authlibinjector.internal.fi.iki.elonen.NanoHTTPD.IHTTPSession; -import moe.yushi.authlibinjector.internal.fi.iki.elonen.NanoHTTPD.Response; +import moe.yushi.authlibinjector.internal.fi.iki.elonen.IHTTPSession; +import moe.yushi.authlibinjector.internal.fi.iki.elonen.Response; public interface URLFilter { diff --git a/src/main/java/moe/yushi/authlibinjector/httpd/URLProcessor.java b/src/main/java/moe/yushi/authlibinjector/httpd/URLProcessor.java index 0a4821b..74817b7 100644 --- a/src/main/java/moe/yushi/authlibinjector/httpd/URLProcessor.java +++ b/src/main/java/moe/yushi/authlibinjector/httpd/URLProcessor.java @@ -1,8 +1,5 @@ package moe.yushi.authlibinjector.httpd; -import static moe.yushi.authlibinjector.internal.fi.iki.elonen.NanoHTTPD.Response.Status.INTERNAL_ERROR; -import static moe.yushi.authlibinjector.internal.fi.iki.elonen.NanoHTTPD.Response.Status.NOT_FOUND; - import java.io.IOException; import java.util.List; import java.util.Optional; @@ -10,7 +7,10 @@ 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.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 { @@ -90,7 +90,7 @@ public class URLProcessor { result = filter.handle(domain, path, session); } catch (Throwable e) { Logging.HTTPD.log(Level.WARNING, "An error occurred while processing request [" + session.getUri() + "]", e); - return newFixedLengthResponse(INTERNAL_ERROR, null, null); + return Response.newFixedLength(Status.INTERNAL_ERROR, null, null); } if (result.isPresent()) { @@ -102,7 +102,7 @@ public class URLProcessor { } Logging.HTTPD.fine("No handler is found for [" + session.getUri() + "]"); - return newFixedLengthResponse(NOT_FOUND, MIME_PLAINTEXT, "Not Found"); + return Response.newFixedLength(Status.NOT_FOUND, MIME_PLAINTEXT, "Not Found"); } }; } diff --git a/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/ChunkedOutputStream.java b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/ChunkedOutputStream.java new file mode 100644 index 0000000..c93ce93 --- /dev/null +++ b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/ChunkedOutputStream.java @@ -0,0 +1,43 @@ +package moe.yushi.authlibinjector.internal.fi.iki.elonen; + +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()); + out.write(b, off, len); + out.write("\r\n".getBytes()); + } + + public void finish() throws IOException { + out.write("0\r\n\r\n".getBytes()); + } +} diff --git a/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/ContentType.java b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/ContentType.java new file mode 100644 index 0000000..b3160f8 --- /dev/null +++ b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/ContentType.java @@ -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; + } +} diff --git a/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/IHTTPSession.java b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/IHTTPSession.java new file mode 100644 index 0000000..c6e54cc --- /dev/null +++ b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/IHTTPSession.java @@ -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 getHeaders(); + + InputStream getInputStream(); + + String getMethod(); + + Map> 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(); +} diff --git a/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/IStatus.java b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/IStatus.java new file mode 100644 index 0000000..53de8d8 --- /dev/null +++ b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/IStatus.java @@ -0,0 +1,8 @@ +package moe.yushi.authlibinjector.internal.fi.iki.elonen; + +public interface IStatus { + + String getDescription(); + + int getRequestStatus(); +} diff --git a/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/NanoHTTPD.java b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/NanoHTTPD.java index da84d12..f303f9f 100644 --- a/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/NanoHTTPD.java +++ b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/NanoHTTPD.java @@ -35,16 +35,12 @@ package moe.yushi.authlibinjector.internal.fi.iki.elonen; import java.io.BufferedInputStream; import java.io.BufferedReader; -import java.io.BufferedWriter; import java.io.ByteArrayInputStream; import java.io.Closeable; -import java.io.FilterOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.net.InetAddress; import java.net.InetSocketAddress; @@ -53,26 +49,15 @@ import java.net.Socket; import java.net.SocketException; import java.net.SocketTimeoutException; import java.net.URLDecoder; -import java.nio.charset.Charset; -import java.nio.charset.CharsetEncoder; -import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; -import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Map.Entry; import java.util.StringTokenizer; -import java.util.TimeZone; import java.util.logging.Level; import java.util.logging.Logger; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import moe.yushi.authlibinjector.internal.fi.iki.elonen.NanoHTTPD.Response.IStatus; -import moe.yushi.authlibinjector.internal.fi.iki.elonen.NanoHTTPD.Response.Status; /** * A simple, tiny, nicely embeddable HTTP server in Java @@ -83,47 +68,7 @@ import moe.yushi.authlibinjector.internal.fi.iki.elonen.NanoHTTPD.Response.Statu * Copyright (c) 2012-2013 by Paul S. Hawke, 2001,2005-2013 by Jarno Elonen, * 2010 by Konstantinos Togias *

- *

- *

- * Features + limitations: - *

    - *

    - *

  • Only one Java file
  • - *
  • Java 5 compatible
  • - *
  • Released as open source, Modified BSD licence
  • - *
  • No fixed config files, logging, authorization etc. (Implement yourself if - * you need them.)
  • - *
  • Supports parameter parsing of GET and POST methods (+ rudimentary PUT - * support in 1.25)
  • - *
  • Supports both dynamic content and file serving
  • - *
  • Supports file upload (since version 1.2, 2010)
  • - *
  • Supports partial content (streaming)
  • - *
  • Supports ETags
  • - *
  • Never caches anything
  • - *
  • Doesn't limit bandwidth, request time or simultaneous connections
  • - *
  • Default code serves files and shows all HTTP parameters and headers
  • - *
  • File server supports directory listing, index.html and index.htm
  • - *
  • File server supports partial content (streaming)
  • - *
  • File server supports ETags
  • - *
  • File server does the 301 redirection trick for directories without '/'
  • - *
  • File server supports simple skipping for files (continue download)
  • - *
  • File server serves also very long files without memory overhead
  • - *
  • Contains a built-in list of most common MIME types
  • - *
  • All header names are converted to lower case so they don't vary between - * browsers/clients
  • - *

    - *

- *

- *

- * How to use: - *

    - *

    - *

  • Subclass and implement serve() and embed to your own program
  • - *

    - *

- *

- * See the separate "LICENSE.md" file for the distribution license (Modified BSD - * licence) + * See the separate "META-INF/licenses/nanohttpd.txt" file for the distribution license (Modified BSD licence) */ public abstract class NanoHTTPD { @@ -245,81 +190,6 @@ public abstract class NanoHTTPD { } - protected static 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; - } - } - protected class HTTPSession implements IHTTPSession { public static final int BUFSIZE = 8192; @@ -376,13 +246,13 @@ public abstract class NanoHTTPD { StringTokenizer st = new StringTokenizer(inLine); if (!st.hasMoreTokens()) { - throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html"); + 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(Response.Status.BAD_REQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html"); + throw new ResponseException(Status.BAD_REQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html"); } String uri = st.nextToken(); @@ -417,7 +287,7 @@ public abstract class NanoHTTPD { pre.put("uri", uri); } catch (IOException ioe) { - throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe); + throw new ResponseException(Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe); } } @@ -532,7 +402,7 @@ public abstract class NanoHTTPD { // (this.inputStream.totalRead() - pos_before_serve)) if (r == null) { - throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response."); + throw new ResponseException(Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response."); } else { r.setRequestMethod(this.method); r.setKeepAlive(keepAlive); @@ -550,11 +420,11 @@ public abstract class NanoHTTPD { // exception up the call stack. throw ste; } catch (IOException ioe) { - Response resp = newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); + 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 = newFixedLengthResponse(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage()); + Response resp = Response.newFixedLength(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage()); resp.send(this.outputStream); safeClose(this.outputStream); } finally { @@ -638,440 +508,6 @@ public abstract class NanoHTTPD { } } - /** - * Handles one session, i.e. parses the HTTP request and returns the - * response. - */ - public interface IHTTPSession { - - void execute() throws IOException; - - Map getHeaders(); - - InputStream getInputStream(); - - String getMethod(); - - Map> 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(); - } - - /** - * HTTP response. Return one of these from serve(). - */ - public static class Response implements Closeable { - - public interface IStatus { - - String getDescription(); - - int getRequestStatus(); - } - - /** - * 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"), - 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; - } - - } - - /** - * 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 - */ - private static 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()); - out.write(b, off, len); - out.write("\r\n".getBytes()); - } - - public void finish() throws IOException { - out.write("0\r\n\r\n".getBytes()); - } - - } - - /** - * 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 header = new HashMap() { - - @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 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 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(); - 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) { - 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; - } - } - - public static final class ResponseException extends Exception { - - private static final long serialVersionUID = 6569838532917408380L; - - private final Response.Status status; - - public ResponseException(Response.Status status, String message) { - super(message); - this.status = status; - } - - public ResponseException(Response.Status status, String message, Exception e) { - super(message, e); - this.status = status; - } - - public Response.Status getStatus() { - return this.status; - } - } - /** * The runnable that will be used for the main listening thread. */ @@ -1142,9 +578,9 @@ public abstract class NanoHTTPD { /** * logger to log to. */ - private static final Logger LOG = Logger.getLogger(NanoHTTPD.class.getName()); + static final Logger LOG = Logger.getLogger(NanoHTTPD.class.getName()); - private static final void safeClose(Object closeable) { + static final void safeClose(Object closeable) { try { if (closeable != null) { if (closeable instanceof Closeable) { @@ -1242,7 +678,7 @@ public abstract class NanoHTTPD { * @return expanded form of the input, for example "foo%20bar" becomes * "foo bar" */ - protected static String decodePercent(String str) { + private static String decodePercent(String str) { String decoded = null; try { decoded = URLDecoder.decode(str, "UTF8"); @@ -1272,50 +708,6 @@ public abstract class NanoHTTPD { return hostname; } - /** - * Create a response with unknown length (using HTTP 1.1 chunking). - */ - public static Response newChunkedResponse(IStatus status, String mimeType, InputStream data) { - return new Response(status, mimeType, data, -1); - } - - /** - * Create a response with known length. - */ - public static Response newFixedLengthResponse(IStatus status, String mimeType, InputStream data, long totalBytes) { - return new Response(status, mimeType, data, totalBytes); - } - - /** - * Create a text response with known length. - */ - public static Response newFixedLengthResponse(IStatus status, String mimeType, String txt) { - ContentType contentType = new ContentType(mimeType); - if (txt == null) { - return newFixedLengthResponse(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 newFixedLengthResponse(status, contentType.getContentTypeHeader(), new ByteArrayInputStream(bytes), bytes.length); - } - } - - /** - * Create a text response with known length. - */ - public static Response newFixedLengthResponse(String msg) { - return newFixedLengthResponse(Status.OK, NanoHTTPD.MIME_HTML, msg); - } - /** * Override this to customize the server. *

@@ -1327,7 +719,7 @@ public abstract class NanoHTTPD { * @return HTTP response, see class Response for details */ public Response serve(IHTTPSession session) { - return newFixedLengthResponse(Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, "Not Found"); + return Response.newFixedLength(Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, "Not Found"); } /** diff --git a/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/Response.java b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/Response.java new file mode 100644 index 0000000..d0e78d0 --- /dev/null +++ b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/Response.java @@ -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 header = new HashMap() { + + @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 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 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); + } +} diff --git a/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/ResponseException.java b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/ResponseException.java new file mode 100644 index 0000000..06f4c55 --- /dev/null +++ b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/ResponseException.java @@ -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; + } +} diff --git a/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/Status.java b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/Status.java new file mode 100644 index 0000000..55640ad --- /dev/null +++ b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/Status.java @@ -0,0 +1,78 @@ +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"), + 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; + } +} From bdd5f4bfcb2cc78e4ab364949c1fe4e0610fd3f2 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Sun, 30 Dec 2018 21:47:02 +0800 Subject: [PATCH 09/16] [nanohttpd]support chunked encoding&100-continue --- .../fi/iki/elonen/ChunkedInputStream.java | 111 ++++++++++++ .../fi/iki/elonen/FixedLengthInputStream.java | 38 +++++ .../internal/fi/iki/elonen/IHTTPSession.java | 2 +- .../internal/fi/iki/elonen/NanoHTTPD.java | 74 +++++++- .../fi/iki/elonen/ChunkedInputStreamTest.java | 158 ++++++++++++++++++ .../elonen/FixedLengthInputStreamTest.java | 51 ++++++ 6 files changed, 424 insertions(+), 10 deletions(-) create mode 100644 src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/ChunkedInputStream.java create mode 100644 src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/FixedLengthInputStream.java create mode 100644 src/test/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/ChunkedInputStreamTest.java create mode 100644 src/test/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/FixedLengthInputStreamTest.java diff --git a/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/ChunkedInputStream.java b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/ChunkedInputStream.java new file mode 100644 index 0000000..c38b3df --- /dev/null +++ b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/ChunkedInputStream.java @@ -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; + } + } +} diff --git a/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/FixedLengthInputStream.java b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/FixedLengthInputStream.java new file mode 100644 index 0000000..0b6605b --- /dev/null +++ b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/FixedLengthInputStream.java @@ -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); + } +} diff --git a/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/IHTTPSession.java b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/IHTTPSession.java index c6e54cc..aee7dba 100644 --- a/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/IHTTPSession.java +++ b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/IHTTPSession.java @@ -15,7 +15,7 @@ public interface IHTTPSession { Map getHeaders(); - InputStream getInputStream(); + InputStream getInputStream() throws IOException; String getMethod(); diff --git a/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/NanoHTTPD.java b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/NanoHTTPD.java index f303f9f..58e9fba 100644 --- a/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/NanoHTTPD.java +++ b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/NanoHTTPD.java @@ -1,5 +1,7 @@ package moe.yushi.authlibinjector.internal.fi.iki.elonen; +import static java.nio.charset.StandardCharsets.US_ASCII; + /* * #%L * NanoHttpd-Core @@ -200,6 +202,8 @@ public abstract class NanoHTTPD { private final BufferedInputStream inputStream; + private InputStream parsedInputStream; + private int splitbyte; private int rlen; @@ -220,6 +224,11 @@ public abstract class NanoHTTPD { 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; @@ -392,14 +401,52 @@ public abstract class NanoHTTPD { String connection = this.headers.get("connection"); boolean keepAlive = "HTTP/1.1".equals(protocolVersion) && (connection == null || !connection.matches("(?i).*close.*")); - // Ok, now do the serve() + 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"); + } - // TODO: long body_size = getBodySize(); - // TODO: long pos_before_serve = this.inputStream.totalRead() - // (requires implementation for totalRead()) - r = serve(this); - // TODO: this.inputStream.skip(body_size - - // (this.inputStream.totalRead() - pos_before_serve)) + } 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."); @@ -460,8 +507,17 @@ public abstract class NanoHTTPD { } @Override - public final InputStream getInputStream() { - return this.inputStream; + 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 diff --git a/src/test/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/ChunkedInputStreamTest.java b/src/test/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/ChunkedInputStreamTest.java new file mode 100644 index 0000000..8c960a2 --- /dev/null +++ b/src/test/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/ChunkedInputStreamTest.java @@ -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); + } +} diff --git a/src/test/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/FixedLengthInputStreamTest.java b/src/test/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/FixedLengthInputStreamTest.java new file mode 100644 index 0000000..c7f7d69 --- /dev/null +++ b/src/test/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/FixedLengthInputStreamTest.java @@ -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); + } +} From 9cfb6325a38d26c623507df0adf4616f69691e8b Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Mon, 31 Dec 2018 00:52:48 +0800 Subject: [PATCH 10/16] Support reverse proxy --- .../authlibinjector/httpd/URLProcessor.java | 115 ++++++++++++++++-- .../internal/fi/iki/elonen/Status.java | 1 + .../yushi/authlibinjector/util/IOUtils.java | 10 +- 3 files changed, 114 insertions(+), 12 deletions(-) diff --git a/src/main/java/moe/yushi/authlibinjector/httpd/URLProcessor.java b/src/main/java/moe/yushi/authlibinjector/httpd/URLProcessor.java index 74817b7..7e8005d 100644 --- a/src/main/java/moe/yushi/authlibinjector/httpd/URLProcessor.java +++ b/src/main/java/moe/yushi/authlibinjector/httpd/URLProcessor.java @@ -1,13 +1,26 @@ 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; @@ -15,8 +28,8 @@ import moe.yushi.authlibinjector.util.Logging; public class URLProcessor { - private static final Pattern URL_REGEX = Pattern.compile("^https?:\\/\\/(?[^\\/]+)(?\\/.*)$"); - private static final Pattern LOCAL_URL_REGEX = Pattern.compile("^/(?[^\\/]+)(?\\/.*)$"); + private static final Pattern URL_REGEX = Pattern.compile("^(?https?):\\/\\/(?[^\\/]+)(?\\/.*)$"); + private static final Pattern LOCAL_URL_REGEX = Pattern.compile("^/(?https?)/(?[^\\/]+)(?\\/.*)$"); private List filters; private URLRedirector redirector; @@ -31,17 +44,18 @@ public class URLProcessor { if (!matcher.find()) { return Optional.empty(); } + String protocol = matcher.group("protocol"); String domain = matcher.group("domain"); String path = matcher.group("path"); - Optional result = transform(domain, path); + Optional result = transform(protocol, domain, path); if (result.isPresent()) { Logging.TRANSFORM.fine("Transformed url [" + inputUrl + "] to [" + result.get() + "]"); } return result; } - private Optional transform(String domain, String path) { + private Optional transform(String protocol, String domain, String path) { boolean handleLocally = false; for (URLFilter filter : filters) { if (filter.canHandle(domain, path)) { @@ -51,7 +65,7 @@ public class URLProcessor { } if (handleLocally) { - return Optional.of("http://127.0.0.1:" + getLocalApiPort() + "/" + domain + path); + return Optional.of("http://127.0.0.1:" + getLocalApiPort() + "/" + protocol + "/" + domain + path); } return redirector.redirect(domain, path); @@ -81,6 +95,7 @@ public class URLProcessor { 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) { @@ -90,7 +105,7 @@ public class URLProcessor { 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, null, null); + return Response.newFixedLength(Status.INTERNAL_ERROR, MIME_PLAINTEXT, "Internal Server Error"); } if (result.isPresent()) { @@ -99,11 +114,93 @@ public class URLProcessor { } } } - } - Logging.HTTPD.fine("No handler is found for [" + session.getUri() + "]"); - return Response.newFixedLength(Status.NOT_FOUND, MIME_PLAINTEXT, "Not Found"); + String target = redirector.redirect(domain, path) + .orElseGet(() -> protocol + "://" + domain + path); + try { + return reverseProxy(session, target); + } catch (IOException e) { + Logging.HTTPD.log(Level.WARNING, "Reserve 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 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 requestHeaders = session.getHeaders(); + ignoredHeaders.forEach(requestHeaders::remove); + + InputStream clientIn = session.getInputStream(); + + Logging.HTTPD.fine(() -> "Reserve proxy: > " + method + " " + url + ", headers: " + requestHeaders); + + HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); + conn.setRequestMethod(method); + conn.setDoOutput(clientIn != null); + requestHeaders.forEach(conn::setRequestProperty); + conn.connect(); + + if (clientIn != null) { + try (OutputStream upstreamOut = conn.getOutputStream()) { + transfer(clientIn, upstreamOut); + } + } + + int responseCode = conn.getResponseCode(); + String reponseMessage = conn.getResponseMessage(); + Map> 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(() -> "Reserve 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> 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; + } } diff --git a/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/Status.java b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/Status.java index 55640ad..4d32a14 100644 --- a/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/Status.java +++ b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/Status.java @@ -45,6 +45,7 @@ public enum Status implements IStatus { 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"); diff --git a/src/main/java/moe/yushi/authlibinjector/util/IOUtils.java b/src/main/java/moe/yushi/authlibinjector/util/IOUtils.java index b8ed853..9f4fc6c 100644 --- a/src/main/java/moe/yushi/authlibinjector/util/IOUtils.java +++ b/src/main/java/moe/yushi/authlibinjector/util/IOUtils.java @@ -40,12 +40,16 @@ public final class IOUtils { 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) { From eeda91c329de364cc40426c97fabea456e322ad7 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Mon, 31 Dec 2018 02:45:21 +0800 Subject: [PATCH 11/16] Support @mojang suffix --- .../authlibinjector/AuthlibInjector.java | 13 ++- .../httpd/LegacySkinAPIFilter.java | 6 +- .../httpd/QueryProfileFilter.java | 76 +++++++++++++++ .../httpd/QueryUUIDsFilter.java | 95 +++++++++++++++++++ .../authlibinjector/httpd/URLFilter.java | 3 +- .../yggdrasil/YggdrasilResponseBuilder.java | 45 +++++++++ 6 files changed, 232 insertions(+), 6 deletions(-) create mode 100644 src/main/java/moe/yushi/authlibinjector/httpd/QueryProfileFilter.java create mode 100644 src/main/java/moe/yushi/authlibinjector/httpd/QueryUUIDsFilter.java create mode 100644 src/main/java/moe/yushi/authlibinjector/yggdrasil/YggdrasilResponseBuilder.java diff --git a/src/main/java/moe/yushi/authlibinjector/AuthlibInjector.java b/src/main/java/moe/yushi/authlibinjector/AuthlibInjector.java index af2445d..bd36d34 100644 --- a/src/main/java/moe/yushi/authlibinjector/AuthlibInjector.java +++ b/src/main/java/moe/yushi/authlibinjector/AuthlibInjector.java @@ -23,6 +23,8 @@ 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; @@ -33,6 +35,9 @@ import moe.yushi.authlibinjector.transform.SkinWhitelistTransformUnit; 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 { @@ -271,12 +276,18 @@ public final class AuthlibInjector { private static URLProcessor createURLProcessor(YggdrasilConfiguration config) { List 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(config)); + filters.add(new LegacySkinAPIFilter(customClient)); } + filters.add(new QueryUUIDsFilter(mojangClient, customClient)); + filters.add(new QueryProfileFilter(mojangClient, customClient)); + return new URLProcessor(filters, new DefaultURLRedirector(config)); } diff --git a/src/main/java/moe/yushi/authlibinjector/httpd/LegacySkinAPIFilter.java b/src/main/java/moe/yushi/authlibinjector/httpd/LegacySkinAPIFilter.java index bf0f1db..cff5015 100644 --- a/src/main/java/moe/yushi/authlibinjector/httpd/LegacySkinAPIFilter.java +++ b/src/main/java/moe/yushi/authlibinjector/httpd/LegacySkinAPIFilter.java @@ -17,14 +17,12 @@ import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; -import moe.yushi.authlibinjector.YggdrasilConfiguration; 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.CustomYggdrasilAPIProvider; import moe.yushi.authlibinjector.yggdrasil.YggdrasilClient; public class LegacySkinAPIFilter implements URLFilter { @@ -33,8 +31,8 @@ public class LegacySkinAPIFilter implements URLFilter { private YggdrasilClient upstream; - public LegacySkinAPIFilter(YggdrasilConfiguration configuration) { - this.upstream = new YggdrasilClient(new CustomYggdrasilAPIProvider(configuration)); + public LegacySkinAPIFilter(YggdrasilClient upstream) { + this.upstream = upstream; } @Override diff --git a/src/main/java/moe/yushi/authlibinjector/httpd/QueryProfileFilter.java b/src/main/java/moe/yushi/authlibinjector/httpd/QueryProfileFilter.java new file mode 100644 index 0000000..34a65e8 --- /dev/null +++ b/src/main/java/moe/yushi/authlibinjector/httpd/QueryProfileFilter.java @@ -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/(?[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 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 unsignedValues = session.getParameters().get("unsigned"); + if (unsignedValues != null && unsignedValues.get(0).equals("false")) { + withSignature = true; + } + + Optional 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)); + } + } + +} diff --git a/src/main/java/moe/yushi/authlibinjector/httpd/QueryUUIDsFilter.java b/src/main/java/moe/yushi/authlibinjector/httpd/QueryUUIDsFilter.java new file mode 100644 index 0000000..cf68b64 --- /dev/null +++ b/src/main/java/moe/yushi/authlibinjector/httpd/QueryUUIDsFilter.java @@ -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 handle(String domain, String path, IHTTPSession session) throws IOException { + if (domain.equals("api.mojang.com") && path.equals("/profiles/minecraft") && session.getMethod().equals("POST")) { + Set 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 performQuery(Set names) { + Set customNames = new LinkedHashSet<>(); + Set 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 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()); + } + +} diff --git a/src/main/java/moe/yushi/authlibinjector/httpd/URLFilter.java b/src/main/java/moe/yushi/authlibinjector/httpd/URLFilter.java index 083e5b9..46e8cdc 100644 --- a/src/main/java/moe/yushi/authlibinjector/httpd/URLFilter.java +++ b/src/main/java/moe/yushi/authlibinjector/httpd/URLFilter.java @@ -1,5 +1,6 @@ package moe.yushi.authlibinjector.httpd; +import java.io.IOException; import java.util.Optional; import moe.yushi.authlibinjector.internal.fi.iki.elonen.IHTTPSession; @@ -9,5 +10,5 @@ public interface URLFilter { boolean canHandle(String domain, String path); - Optional handle(String domain, String path, IHTTPSession session); + Optional handle(String domain, String path, IHTTPSession session) throws IOException; } diff --git a/src/main/java/moe/yushi/authlibinjector/yggdrasil/YggdrasilResponseBuilder.java b/src/main/java/moe/yushi/authlibinjector/yggdrasil/YggdrasilResponseBuilder.java new file mode 100644 index 0000000..cfe8b97 --- /dev/null +++ b/src/main/java/moe/yushi/authlibinjector/yggdrasil/YggdrasilResponseBuilder.java @@ -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 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(); + } +} From cbc1e8c6641f1709d0b4f87e9b620ac548739c49 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Mon, 31 Dec 2018 11:22:50 +0800 Subject: [PATCH 12/16] Javadoc for URLFilter&URLProcessor&URLRedirector --- .../yushi/authlibinjector/httpd/URLFilter.java | 16 ++++++++++++++++ .../authlibinjector/httpd/URLProcessor.java | 10 ++++++++++ .../authlibinjector/httpd/URLRedirector.java | 4 ++++ 3 files changed, 30 insertions(+) diff --git a/src/main/java/moe/yushi/authlibinjector/httpd/URLFilter.java b/src/main/java/moe/yushi/authlibinjector/httpd/URLFilter.java index 46e8cdc..fdecb6e 100644 --- a/src/main/java/moe/yushi/authlibinjector/httpd/URLFilter.java +++ b/src/main/java/moe/yushi/authlibinjector/httpd/URLFilter.java @@ -6,8 +6,24 @@ 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/ (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 handle(String domain, String path, IHTTPSession session) throws IOException; diff --git a/src/main/java/moe/yushi/authlibinjector/httpd/URLProcessor.java b/src/main/java/moe/yushi/authlibinjector/httpd/URLProcessor.java index 7e8005d..893c7f3 100644 --- a/src/main/java/moe/yushi/authlibinjector/httpd/URLProcessor.java +++ b/src/main/java/moe/yushi/authlibinjector/httpd/URLProcessor.java @@ -39,6 +39,16 @@ public class URLProcessor { 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 transformURL(String inputUrl) { Matcher matcher = URL_REGEX.matcher(inputUrl); if (!matcher.find()) { diff --git a/src/main/java/moe/yushi/authlibinjector/httpd/URLRedirector.java b/src/main/java/moe/yushi/authlibinjector/httpd/URLRedirector.java index 6688f64..41e8923 100644 --- a/src/main/java/moe/yushi/authlibinjector/httpd/URLRedirector.java +++ b/src/main/java/moe/yushi/authlibinjector/httpd/URLRedirector.java @@ -2,6 +2,10 @@ 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 redirect(String domain, String path); } From 1148e0c6bce5a2bd846ec5fa91615d6e144d4c07 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Mon, 31 Dec 2018 12:33:04 +0800 Subject: [PATCH 13/16] polish --- .../yushi/authlibinjector/httpd/URLProcessor.java | 6 +++--- .../internal/fi/iki/elonen/ChunkedOutputStream.java | 8 +++++--- .../internal/fi/iki/elonen/NanoHTTPD.java | 6 +++--- .../internal/org/json/simple/parser/Yylex.java | 13 ------------- 4 files changed, 11 insertions(+), 22 deletions(-) diff --git a/src/main/java/moe/yushi/authlibinjector/httpd/URLProcessor.java b/src/main/java/moe/yushi/authlibinjector/httpd/URLProcessor.java index 893c7f3..4f2d390 100644 --- a/src/main/java/moe/yushi/authlibinjector/httpd/URLProcessor.java +++ b/src/main/java/moe/yushi/authlibinjector/httpd/URLProcessor.java @@ -130,7 +130,7 @@ public class URLProcessor { try { return reverseProxy(session, target); } catch (IOException e) { - Logging.HTTPD.log(Level.WARNING, "Reserve proxy error", e); + Logging.HTTPD.log(Level.WARNING, "Reverse proxy error", e); return Response.newFixedLength(Status.BAD_GATEWAY, MIME_PLAINTEXT, "Bad Gateway"); } } else { @@ -154,7 +154,7 @@ public class URLProcessor { InputStream clientIn = session.getInputStream(); - Logging.HTTPD.fine(() -> "Reserve proxy: > " + method + " " + url + ", headers: " + requestHeaders); + Logging.HTTPD.fine(() -> "Reverse proxy: > " + method + " " + url + ", headers: " + requestHeaders); HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); conn.setRequestMethod(method); @@ -182,7 +182,7 @@ public class URLProcessor { } catch (IOException e) { upstreamIn = conn.getErrorStream(); } - Logging.HTTPD.fine(() -> "Reserve proxy: < " + responseCode + " " + reponseMessage + " , headers: " + responseHeaders); + Logging.HTTPD.fine(() -> "Reverse proxy: < " + responseCode + " " + reponseMessage + " , headers: " + responseHeaders); IStatus status = new IStatus() { @Override diff --git a/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/ChunkedOutputStream.java b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/ChunkedOutputStream.java index c93ce93..261a4cf 100644 --- a/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/ChunkedOutputStream.java +++ b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/ChunkedOutputStream.java @@ -1,5 +1,7 @@ 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; @@ -32,12 +34,12 @@ class ChunkedOutputStream extends FilterOutputStream { public void write(byte[] b, int off, int len) throws IOException { if (len == 0) return; - out.write(String.format("%x\r\n", len).getBytes()); + out.write(String.format("%x\r\n", len).getBytes(US_ASCII)); out.write(b, off, len); - out.write("\r\n".getBytes()); + out.write("\r\n".getBytes(US_ASCII)); } public void finish() throws IOException { - out.write("0\r\n\r\n".getBytes()); + out.write("0\r\n\r\n".getBytes(US_ASCII)); } } diff --git a/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/NanoHTTPD.java b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/NanoHTTPD.java index 58e9fba..9b920e5 100644 --- a/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/NanoHTTPD.java +++ b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/NanoHTTPD.java @@ -237,8 +237,8 @@ public abstract class NanoHTTPD { 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().toString(); - this.remoteHostname = inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() ? "localhost" : inetAddress.getHostName().toString(); + 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<>(); } @@ -388,7 +388,7 @@ public abstract class NanoHTTPD { } // Create a BufferedReader for parsing the header. - BufferedReader hin = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, this.rlen))); + BufferedReader hin = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, this.rlen), US_ASCII)); // Decode the header into parms and header java properties Map pre = new HashMap<>(); diff --git a/src/main/java/moe/yushi/authlibinjector/internal/org/json/simple/parser/Yylex.java b/src/main/java/moe/yushi/authlibinjector/internal/org/json/simple/parser/Yylex.java index 1c82000..c03fafd 100644 --- a/src/main/java/moe/yushi/authlibinjector/internal/org/json/simple/parser/Yylex.java +++ b/src/main/java/moe/yushi/authlibinjector/internal/org/json/simple/parser/Yylex.java @@ -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. * From c297c4debc650f95973e819d8718a7ee777a63d3 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Mon, 31 Dec 2018 12:42:57 +0800 Subject: [PATCH 14/16] Remove unnecessary connection.connect()/disconnect() --- .../yushi/authlibinjector/httpd/URLProcessor.java | 1 - .../moe/yushi/authlibinjector/util/IOUtils.java | 15 +++++---------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/main/java/moe/yushi/authlibinjector/httpd/URLProcessor.java b/src/main/java/moe/yushi/authlibinjector/httpd/URLProcessor.java index 4f2d390..d9e7ed6 100644 --- a/src/main/java/moe/yushi/authlibinjector/httpd/URLProcessor.java +++ b/src/main/java/moe/yushi/authlibinjector/httpd/URLProcessor.java @@ -160,7 +160,6 @@ public class URLProcessor { conn.setRequestMethod(method); conn.setDoOutput(clientIn != null); requestHeaders.forEach(conn::setRequestProperty); - conn.connect(); if (clientIn != null) { try (OutputStream upstreamOut = conn.getOutputStream()) { diff --git a/src/main/java/moe/yushi/authlibinjector/util/IOUtils.java b/src/main/java/moe/yushi/authlibinjector/util/IOUtils.java index 9f4fc6c..c2c482e 100644 --- a/src/main/java/moe/yushi/authlibinjector/util/IOUtils.java +++ b/src/main/java/moe/yushi/authlibinjector/util/IOUtils.java @@ -25,16 +25,11 @@ 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); } } From 029782551d1d170cd32c405b3ee3d3130ce454ce Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Mon, 31 Dec 2018 13:25:27 +0800 Subject: [PATCH 15/16] Add prop: httpd.disable --- .../moe/yushi/authlibinjector/AuthlibInjector.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/main/java/moe/yushi/authlibinjector/AuthlibInjector.java b/src/main/java/moe/yushi/authlibinjector/AuthlibInjector.java index bd36d34..4a61698 100644 --- a/src/main/java/moe/yushi/authlibinjector/AuthlibInjector.java +++ b/src/main/java/moe/yushi/authlibinjector/AuthlibInjector.java @@ -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; @@ -86,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"; // ==== @@ -273,7 +276,11 @@ public final class AuthlibInjector { } } - private static URLProcessor createURLProcessor(YggdrasilConfiguration config) { + private static List createFilters(YggdrasilConfiguration config) { + if (Boolean.getBoolean(PROP_DISABLE_HTTPD)) { + return emptyList(); + } + List filters = new ArrayList<>(); YggdrasilClient customClient = new YggdrasilClient(new CustomYggdrasilAPIProvider(config)); @@ -288,11 +295,11 @@ public final class AuthlibInjector { filters.add(new QueryUUIDsFilter(mojangClient, customClient)); filters.add(new QueryProfileFilter(mojangClient, customClient)); - return new URLProcessor(filters, new DefaultURLRedirector(config)); + return filters; } private static ClassTransformer createTransformer(YggdrasilConfiguration config) { - URLProcessor urlProcessor = createURLProcessor(config); + URLProcessor urlProcessor = new URLProcessor(createFilters(config), new DefaultURLRedirector(config)); ClassTransformer transformer = new ClassTransformer(); for (String ignore : nonTransformablePackages) { From 27e775114c23895d1fe178e823673d685c09e469 Mon Sep 17 00:00:00 2001 From: yushijinhun Date: Mon, 31 Dec 2018 13:26:58 +0800 Subject: [PATCH 16/16] Remove duplicate ldc transform log --- .../moe/yushi/authlibinjector/transform/LdcTransformUnit.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/moe/yushi/authlibinjector/transform/LdcTransformUnit.java b/src/main/java/moe/yushi/authlibinjector/transform/LdcTransformUnit.java index 3e6cf1f..bdd9785 100644 --- a/src/main/java/moe/yushi/authlibinjector/transform/LdcTransformUnit.java +++ b/src/main/java/moe/yushi/authlibinjector/transform/LdcTransformUnit.java @@ -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 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);