From 12408d92fde59fa8ebcc2fa2cec157cc522554ce Mon Sep 17 00:00:00 2001 From: Ethan Zuo Date: Mon, 26 Jun 2023 17:48:57 +0800 Subject: [PATCH 1/9] Deprecated NanoHTTPD --- README.en.md | 4 +- README.md | 4 +- build.gradle | 3 - .../fi/iki/elonen/ChunkedInputStream.java | 127 ------ .../fi/iki/elonen/ChunkedOutputStream.java | 91 ---- .../internal/fi/iki/elonen/ContentType.java | 125 ------ .../fi/iki/elonen/FixedLengthInputStream.java | 54 --- .../internal/fi/iki/elonen/HTTPSession.java | 406 ------------------ .../internal/fi/iki/elonen/IHTTPSession.java | 87 ---- .../internal/fi/iki/elonen/IStatus.java | 54 --- .../internal/fi/iki/elonen/NanoHTTPD.java | 329 -------------- .../internal/fi/iki/elonen/Response.java | 319 -------------- .../fi/iki/elonen/ResponseException.java | 66 --- .../internal/fi/iki/elonen/Status.java | 125 ------ .../internal/fi/iki/elonen/package-info.java | 6 - .../resources/META-INF/licenses/nanohttpd.txt | 26 -- .../fi/iki/elonen/ChunkedInputStreamTest.java | 199 --------- .../elonen/FixedLengthInputStreamTest.java | 70 --- 18 files changed, 4 insertions(+), 2091 deletions(-) delete mode 100644 src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/ChunkedInputStream.java delete mode 100644 src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/ChunkedOutputStream.java delete mode 100644 src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/ContentType.java delete mode 100644 src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/FixedLengthInputStream.java delete mode 100644 src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/HTTPSession.java delete mode 100644 src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/IHTTPSession.java delete mode 100644 src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/IStatus.java delete mode 100644 src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/NanoHTTPD.java delete mode 100644 src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/Response.java delete mode 100644 src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/ResponseException.java delete mode 100644 src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/Status.java delete mode 100644 src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/package-info.java delete mode 100644 src/main/resources/META-INF/licenses/nanohttpd.txt delete mode 100644 src/test/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/ChunkedInputStreamTest.java delete mode 100644 src/test/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/FixedLengthInputStreamTest.java diff --git a/README.en.md b/README.en.md index 61aeb0f..93ef671 100644 --- a/README.en.md +++ b/README.en.md @@ -74,6 +74,7 @@ Configure Minecraft server with the following JVM parameter: -Dauthlibinjector.disableHttpd Disable local HTTP server. Features (see below) depending on local HTTP server will be unavailable: + - Mojang Yggdrasil server - Mojang namespace - Legacy skin API polyfill @@ -157,5 +158,4 @@ This work is licensed under the [GNU Affero General Public License v3.0](https:/ * [authlib-injector](https://github.com/yushijinhun/authlib-injector) by [Haowei Wen](https://github.com/yushijinhun) This is the base of this project, which makes our ideas possible. * [Gson](https://github.com/google/gson) by Google Inc. -* [ASM](https://asm.ow2.io) by INRIA, France Telecom -* [NanoHttpd](https://github.com/NanoHttpd/nanohttpd) \ No newline at end of file +* [ASM](https://asm.ow2.io) by INRIA, France Telecom \ No newline at end of file diff --git a/README.md b/README.md index da06291..e0919f3 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,7 @@ gradle -Dauthlibinjector.disableHttpd 禁用内建的 HTTP 服务器. 以下依赖内建 HTTP 服务器的功能将不可用: + - Mojang 验证服务器 - Mojang 命名空间 - 旧式皮肤 API polyfill @@ -155,5 +156,4 @@ gradle * [authlib-injector](https://github.com/yushijinhun/authlib-injector) by [Haowei Wen](https://github.com/yushijinhun) 这是本项目的基础, 它使得我们的想法成为可能. * [Gson](https://github.com/google/gson) by Google Inc. - * [ASM](https://asm.ow2.io) by INRIA, France Telecom - * [NanoHttpd](https://github.com/NanoHttpd/nanohttpd) \ No newline at end of file + * [ASM](https://asm.ow2.io) by INRIA, France Telecom \ No newline at end of file diff --git a/build.gradle b/build.gradle index 6a37b11..920707b 100644 --- a/build.gradle +++ b/build.gradle @@ -55,9 +55,6 @@ shadowJar { exclude 'META-INF/maven/**' exclude 'module-info.class' exclude '**/module-info.class' - - relocate 'com.google.gson', 'xyz.zuoyx.multiyggdrasil.internal.com.google.gson' - relocate 'org.objectweb.asm', 'xyz.zuoyx.multiyggdrasil.internal.org.objectweb.asm' } defaultTasks 'clean', 'shadowJar' diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/ChunkedInputStream.java b/src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/ChunkedInputStream.java deleted file mode 100644 index 6c34af2..0000000 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/ChunkedInputStream.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright (C) 2019 Haowei Wen and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package xyz.zuoyx.multiyggdrasil.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/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/ChunkedOutputStream.java b/src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/ChunkedOutputStream.java deleted file mode 100644 index 0d8d9bf..0000000 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/ChunkedOutputStream.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright (C) 2019 Haowei Wen and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -/* - * 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. - */ -package xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen; - -import static java.nio.charset.StandardCharsets.US_ASCII; - -import java.io.FilterOutputStream; -import java.io.IOException; -import java.io.OutputStream; - -/** - * Output stream that will automatically send every write to the wrapped - * OutputStream according to chunked transfer: - * ... - */ -class ChunkedOutputStream extends FilterOutputStream { - - public ChunkedOutputStream(OutputStream out) { - super(out); - } - - @Override - public void write(int b) throws IOException { - byte[] data = { - (byte) b - }; - write(data, 0, 1); - } - - @Override - public void write(byte[] b) throws IOException { - write(b, 0, b.length); - } - - @Override - public void write(byte[] b, int off, int len) throws IOException { - if (len == 0) - return; - out.write(String.format("%x\r\n", len).getBytes(US_ASCII)); - out.write(b, off, len); - out.write("\r\n".getBytes(US_ASCII)); - } - - public void finish() throws IOException { - out.write("0\r\n\r\n".getBytes(US_ASCII)); - } -} diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/ContentType.java b/src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/ContentType.java deleted file mode 100644 index 11f194a..0000000 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/ContentType.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright (C) 2019 Haowei Wen and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -/* - * 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. - */ -package xyz.zuoyx.multiyggdrasil.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/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/FixedLengthInputStream.java b/src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/FixedLengthInputStream.java deleted file mode 100644 index 026fe97..0000000 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/FixedLengthInputStream.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (C) 2019 Haowei Wen and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package xyz.zuoyx.multiyggdrasil.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/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/HTTPSession.java b/src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/HTTPSession.java deleted file mode 100644 index 2facea5..0000000 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/HTTPSession.java +++ /dev/null @@ -1,406 +0,0 @@ -/* - * Copyright (C) 2020 Haowei Wen and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -/* - * 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. - */ -package xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen; - -import static java.nio.charset.StandardCharsets.ISO_8859_1; -import static xyz.zuoyx.multiyggdrasil.util.IOUtils.CONTENT_TYPE_TEXT; -import static xyz.zuoyx.multiyggdrasil.util.Logging.log; -import static xyz.zuoyx.multiyggdrasil.util.Logging.Level.DEBUG; -import java.io.BufferedInputStream; -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.UnsupportedEncodingException; -import java.net.InetSocketAddress; -import java.net.SocketException; -import java.net.SocketTimeoutException; -import java.net.URLDecoder; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.StringTokenizer; -import java.util.function.Function; - -class HTTPSession implements IHTTPSession { - - public static class ConnectionCloseException extends SocketException {} - - public static final int BUFSIZE = 8192; - - private final OutputStream outputStream; - private final BufferedInputStream inputStream; - private final InetSocketAddress remoteAddr; - - private String uri; - private String method; - private String queryParameterString; - private Map> parms; - private Map headers; - private String protocolVersion; - - private InputStream parsedInputStream; - - private boolean expect100Continue; - private boolean continueSent; - private boolean isServing; - private final Object servingLock = new Object(); - - public HTTPSession(InputStream inputStream, OutputStream outputStream, InetSocketAddress remoteAddr) { - this.inputStream = new BufferedInputStream(inputStream, BUFSIZE); - this.outputStream = outputStream; - this.remoteAddr = remoteAddr; - } - - private ByteArrayInputStream readHeader() throws IOException { - // 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[BUFSIZE]; - int splitbyte = 0; - int rlen = 0; - - int read = -1; - this.inputStream.mark(BUFSIZE); - try { - read = this.inputStream.read(buf, 0, BUFSIZE); - } catch (IOException e) { - NanoHTTPD.safeClose(this.inputStream); - NanoHTTPD.safeClose(this.outputStream); - throw new ConnectionCloseException(); - } - if (read == -1) { - // socket was been closed - NanoHTTPD.safeClose(this.inputStream); - NanoHTTPD.safeClose(this.outputStream); - throw new ConnectionCloseException(); - } - while (read > 0) { - rlen += read; - splitbyte = findHeaderEnd(buf, rlen); - if (splitbyte > 0) { - break; - } - read = this.inputStream.read(buf, rlen, BUFSIZE - rlen); - } - - if (splitbyte < rlen) { - this.inputStream.reset(); - this.inputStream.skip(splitbyte); - } - - return new ByteArrayInputStream(buf, 0, rlen); - } - - private void parseHeader(BufferedReader in) throws ResponseException { - try { - String requestLine = in.readLine(); - if (requestLine == null) { - throw new ResponseException(Status.BAD_REQUEST, "BAD REQUEST: Syntax error."); - } - - StringTokenizer st = new StringTokenizer(requestLine); - if (!st.hasMoreTokens()) { - throw new ResponseException(Status.BAD_REQUEST, "BAD REQUEST: Syntax error."); - } - - this.method = st.nextToken(); - - if (!st.hasMoreTokens()) { - throw new ResponseException(Status.BAD_REQUEST, "BAD REQUEST: Missing URI."); - } - - String rawUri = st.nextToken(); - - // Decode parameters from the URI - int qmi = rawUri.indexOf('?'); - if (qmi >= 0) { - this.queryParameterString = rawUri.substring(qmi + 1); - this.parms = Collections.unmodifiableMap(decodeParms(this.queryParameterString)); - this.uri = decodePercent(rawUri.substring(0, qmi)); - } else { - this.queryParameterString = null; - this.parms = Collections.emptyMap(); - this.uri = decodePercent(rawUri); - } - - // 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()) { - this.protocolVersion = st.nextToken(); - } else { - this.protocolVersion = "HTTP/1.1"; - log(DEBUG, "no protocol version specified, strange. Assuming HTTP/1.1."); - } - - Map headers = new LinkedHashMap<>(); - 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.ROOT), line.substring(p + 1).trim()); - } - line = in.readLine(); - } - this.headers = Collections.unmodifiableMap(headers); - - } catch (IOException ioe) { - throw new ResponseException(Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe); - } - } - - public void execute(Function handler) throws IOException { - Response r = null; - try { - parseHeader(new BufferedReader(new InputStreamReader(readHeader(), ISO_8859_1))); - - String transferEncoding = this.headers.get("transfer-encoding"); - String contentLengthStr = this.headers.get("content-length"); - if (transferEncoding != null && contentLengthStr == null) { - if ("chunked".equals(transferEncoding)) { - this.parsedInputStream = new ChunkedInputStream(this.inputStream); - } else { - throw new ResponseException(Status.NOT_IMPLEMENTED, "Unsupported Transfer-Encoding"); - } - - } else if (transferEncoding == null && contentLengthStr != null) { - int contentLength = -1; - try { - contentLength = Integer.parseInt(contentLengthStr); - } catch (NumberFormatException e) { - } - if (contentLength < 0) { - throw new ResponseException(Status.BAD_REQUEST, "The request has an invalid Content-Length header."); - } - this.parsedInputStream = new FixedLengthInputStream(this.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 - this.parsedInputStream = null; - } - - this.expect100Continue = "HTTP/1.1".equals(this.protocolVersion) - && "100-continue".equals(this.headers.get("expect")) - && this.parsedInputStream != null; - this.continueSent = false; - - // Ok, now do the serve() - this.isServing = true; - try { - r = handler.apply(this); - } finally { - synchronized (this.servingLock) { - this.isServing = false; - } - } - - if (!(this.parsedInputStream == null || (this.expect100Continue && !this.continueSent))) { - // consume the input - while (this.parsedInputStream.read() != -1) - ; - } - - boolean keepAlive = "HTTP/1.1".equals(this.protocolVersion) && !"close".equals(this.headers.get("connection")); - - if (r == null) { - throw new ResponseException(Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response."); - } else { - r.setRequestMethod(this.method); - r.setKeepAlive(keepAlive); - r.send(this.outputStream); - } - if (!keepAlive || "close".equals(r.getHeader("connection"))) { - throw new ConnectionCloseException(); - } - } catch (SocketException e) { - // throw it out to close socket object (finalAccept) - throw e; - } catch (SocketTimeoutException ste) { - // treat socket timeouts the same way we treat socket exceptions - // i.e. close the stream & finalAccept object by throwing the - // exception up the call stack. - throw ste; - } catch (IOException ioe) { - Response resp = Response.newFixedLength(Status.INTERNAL_ERROR, CONTENT_TYPE_TEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); - resp.send(this.outputStream); - NanoHTTPD.safeClose(this.outputStream); - } catch (ResponseException re) { - Response resp = Response.newFixedLength(re.getStatus(), CONTENT_TYPE_TEXT, re.getMessage()); - resp.send(this.outputStream); - NanoHTTPD.safeClose(this.outputStream); - } finally { - NanoHTTPD.safeClose(r); - } - } - - @Override - public final Map getHeaders() { - return this.headers; - } - - @Override - public final InputStream getInputStream() throws IOException { - synchronized (this.servingLock) { - if (!this.isServing) { - throw new IllegalStateException(); - } - if (this.expect100Continue && !this.continueSent) { - this.continueSent = true; - this.outputStream.write("HTTP/1.1 100 Continue\r\n\r\n".getBytes(ISO_8859_1)); - } - } - return this.parsedInputStream; - } - - @Override - public final String getMethod() { - return this.method; - } - - @Override - public final Map> getParameters() { - return this.parms; - } - - @Override - public String getQueryParameterString() { - return this.queryParameterString; - } - - @Override - public final String getUri() { - return this.uri; - } - - @Override - public InetSocketAddress getRemoteAddress() { - return this.remoteAddr; - } - - /** - * Find byte index separating header from body. It must be the last byte - * of the first two sequential new lines. - */ - private static 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; - } - - /** - * Decode percent encoded String values. - * - * @param str - * the percent encoded String - * @return expanded form of the input, for example "foo%20bar" becomes - * "foo bar" - */ - private static String decodePercent(String str) { - try { - return URLDecoder.decode(str, "UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); // never happens - } - } - - /** - * Decodes parameters in percent-encoded URI-format ( e.g. - * "name=Jack%20Daniels&pass=Single%20Malt" ) and adds them to given - * Map. - */ - private static Map> decodeParms(String parms) { - Map> result = new LinkedHashMap<>(); - 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 = result.get(key); - if (values == null) { - values = new ArrayList<>(); - result.put(key, values); - } - - values.add(value); - } - return result; - } -} diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/IHTTPSession.java b/src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/IHTTPSession.java deleted file mode 100644 index da331fa..0000000 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/IHTTPSession.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (C) 2020 Haowei Wen and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -/* - * 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. - */ -package xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen; - -import java.io.IOException; -import java.io.InputStream; -import java.net.InetSocketAddress; -import java.util.List; -import java.util.Map; - -/** - * Handles one session, i.e. parses the HTTP request and returns the - * response. - */ -public interface IHTTPSession { - - InetSocketAddress getRemoteAddress(); - - String getMethod(); - - /** - * @return the path part of the URL. - */ - String getUri(); - - /** - * @return raw query string, null if no query exists - */ - String getQueryParameterString(); - - /** - * @return decoded query parameters - */ - Map> getParameters(); - - Map getHeaders(); - - /** - * @return request body, null if the request does not have a payload - */ - InputStream getInputStream() throws IOException; - -} diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/IStatus.java b/src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/IStatus.java deleted file mode 100644 index 483944d..0000000 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/IStatus.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (C) 2019 Haowei Wen and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -/* - * 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. - */ -package xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen; - -public interface IStatus { - - String getDescription(); - - int getRequestStatus(); -} diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/NanoHTTPD.java b/src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/NanoHTTPD.java deleted file mode 100644 index f42ea9d..0000000 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/NanoHTTPD.java +++ /dev/null @@ -1,329 +0,0 @@ -/* - * Copyright (C) 2020 Haowei Wen and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -/* - * 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. - */ -package xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen; - -import static xyz.zuoyx.multiyggdrasil.util.IOUtils.CONTENT_TYPE_TEXT; -import static xyz.zuoyx.multiyggdrasil.util.Logging.log; -import static xyz.zuoyx.multiyggdrasil.util.Logging.Level.DEBUG; -import static xyz.zuoyx.multiyggdrasil.util.Logging.Level.ERROR; -import java.io.Closeable; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.InetSocketAddress; -import java.net.ServerSocket; -import java.net.Socket; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.atomic.AtomicLong; -import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.HTTPSession.ConnectionCloseException; - -/** - * 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 - *

- * See the separate "META-INF/licenses/nanohttpd.txt" file for the distribution license (Modified BSD licence) - */ -public abstract class NanoHTTPD { - - /** - * The runnable that will be used for every new client connection. - */ - private class ClientHandler implements Runnable { - - private final InputStream inputStream; - - private final Socket acceptSocket; - - public ClientHandler(InputStream inputStream, Socket acceptSocket) { - this.inputStream = inputStream; - this.acceptSocket = acceptSocket; - } - - public void close() { - safeClose(this.inputStream); - safeClose(this.acceptSocket); - } - - @Override - public void run() { - OutputStream outputStream = null; - try { - outputStream = this.acceptSocket.getOutputStream(); - HTTPSession session = new HTTPSession(this.inputStream, outputStream, (InetSocketAddress) this.acceptSocket.getRemoteSocketAddress()); - while (!this.acceptSocket.isClosed()) { - session.execute(NanoHTTPD.this::serve); - } - } catch (ConnectionCloseException e) { - // When the socket is closed by the client, - // we throw our own ConnectionCloseException - // to break the "keep alive" loop above. If - // the exception was anything other - // than the expected SocketException OR a - // SocketTimeoutException, print the - // stacktrace - } catch (Exception e) { - log(ERROR, "Communication with the client broken, or an bug in the handler code", e); - } finally { - safeClose(outputStream); - safeClose(this.inputStream); - safeClose(this.acceptSocket); - NanoHTTPD.this.asyncRunner.closed(this); - } - } - } - - /** - * Default threading strategy for NanoHTTPD. - *

- *

- * 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. - *

- */ - private static class AsyncRunner { - - private final AtomicLong requestCount = new AtomicLong(); - private final List running = new CopyOnWriteArrayList<>(); - - public void closeAll() { - for (ClientHandler clientHandler : this.running) { - clientHandler.close(); - } - } - - public void closed(ClientHandler clientHandler) { - this.running.remove(clientHandler); - } - - public void exec(ClientHandler clientHandler) { - Thread t = new Thread(clientHandler); - t.setDaemon(true); - t.setName("NanoHttpd Request Processor (#" + this.requestCount.incrementAndGet() + ")"); - this.running.add(clientHandler); - t.start(); - } - } - - /** - * The runnable that will be used for the main listening thread. - */ - private class ServerRunnable implements Runnable { - - /** - * 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). - */ - private static final int SOCKET_READ_TIMEOUT = 5000; - - private IOException bindException; - - private boolean hasBinded = false; - - @Override - public void run() { - try { - serverSocket.bind(hostname != null ? new InetSocketAddress(hostname, port) : new InetSocketAddress(port)); - hasBinded = true; - } catch (IOException e) { - this.bindException = e; - return; - } - do { - try { - @SuppressWarnings("resource") - final Socket finalAccept = NanoHTTPD.this.serverSocket.accept(); - finalAccept.setSoTimeout(SOCKET_READ_TIMEOUT); - @SuppressWarnings("resource") - final InputStream inputStream = finalAccept.getInputStream(); - NanoHTTPD.this.asyncRunner.exec(new ClientHandler(inputStream, finalAccept)); - } catch (IOException e) { - log(DEBUG, "Communication with the client broken", e); - } - } while (!NanoHTTPD.this.serverSocket.isClosed()); - } - } - - 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) { - log(ERROR, "Could not close", e); - } - } - - private final String hostname; - private final int port; - - private volatile ServerSocket serverSocket; - private Thread listenerThread; - - private final AsyncRunner asyncRunner = new AsyncRunner(); - - /** - * Constructs an HTTP server on given port. - */ - public NanoHTTPD(int port) { - this(null, port); - } - - // ------------------------------------------------------------------------------- - // // - // - // Threading Strategy. - // - // ------------------------------------------------------------------------------- - // // - - /** - * Constructs an HTTP server on given hostname and port. - */ - public NanoHTTPD(String hostname, int port) { - this.hostname = hostname; - this.port = port; - } - - public final int getListeningPort() { - return this.serverSocket == null ? -1 : this.serverSocket.getLocalPort(); - } - - public final boolean isAlive() { - return wasStarted() && !this.serverSocket.isClosed() && this.listenerThread.isAlive(); - } - - public String getHostname() { - return hostname; - } - - /** - * 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) { - return Response.newFixedLength(Status.NOT_FOUND, CONTENT_TYPE_TEXT, "Not Found"); - } - - /** - * Starts the server in daemon mode. - */ - public void start() throws IOException { - start(true); - } - - /** - * Start the server. - * - * @param daemon - * start the thread daemon or not. - * @throws IOException - * if the socket is in use. - */ - public void start(boolean daemon) throws IOException { - this.serverSocket = new ServerSocket(); - this.serverSocket.setReuseAddress(true); - - ServerRunnable serverRunnable = new ServerRunnable(); - this.listenerThread = new Thread(serverRunnable); - this.listenerThread.setDaemon(daemon); - this.listenerThread.setName("NanoHttpd Main Listener"); - this.listenerThread.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.serverSocket); - this.asyncRunner.closeAll(); - if (this.listenerThread != null) { - this.listenerThread.join(); - } - } catch (Exception e) { - log(ERROR, "Could not stop all connections", e); - } - } - - public final boolean wasStarted() { - return this.serverSocket != null && this.listenerThread != null; - } -} diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/Response.java b/src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/Response.java deleted file mode 100644 index 5abd1c1..0000000 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/Response.java +++ /dev/null @@ -1,319 +0,0 @@ -/* - * Copyright (C) 2021 Haowei Wen and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -/* - * 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. - */ -package xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen; - -import static java.util.Objects.requireNonNull; -import static xyz.zuoyx.multiyggdrasil.util.Logging.log; -import static xyz.zuoyx.multiyggdrasil.util.Logging.Level.ERROR; -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.LinkedHashMap; -import java.util.Locale; -import java.util.Map; -import java.util.TimeZone; - -/** - * 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. - */ - private final Map headers = new LinkedHashMap<>(); - - /** - * 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.headers.put(name.toLowerCase(Locale.ROOT), requireNonNull(value)); - } - - public String getHeader(String name) { - return this.headers.get(name.toLowerCase(Locale.ROOT)); - } - - public InputStream getData() { - return this.data; - } - - 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())); - } - this.headers.forEach((name, value) -> printHeader(pw, name, value)); - 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) { - log(ERROR, "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"); - if (contentLengthString == null) { - pw.print("Content-Length: " + defaultSize + "\r\n"); - return defaultSize; - } else { - long size = defaultSize; - try { - size = Long.parseLong(contentLengthString); - } catch (NumberFormatException ex) { - log(ERROR, "content-length was not number " + contentLengthString); - } - 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(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) { - throw new RuntimeException(e); // never happens, utf-8 is always available - } - 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/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/ResponseException.java b/src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/ResponseException.java deleted file mode 100644 index 192b579..0000000 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/ResponseException.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2019 Haowei Wen and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -/* - * 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. - */ -package xyz.zuoyx.multiyggdrasil.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/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/Status.java b/src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/Status.java deleted file mode 100644 index 5b52b70..0000000 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/Status.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright (C) 2019 Haowei Wen and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -/* - * 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. - */ -package xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen; - -/** - * Some HTTP response status codes - */ -public enum Status implements IStatus { - SWITCH_PROTOCOL(101, "Switching Protocols"), - - OK(200, "OK"), - CREATED(201, "Created"), - ACCEPTED(202, "Accepted"), - NO_CONTENT(204, "No Content"), - PARTIAL_CONTENT(206, "Partial Content"), - MULTI_STATUS(207, "Multi-Status"), - - REDIRECT(301, "Moved Permanently"), - /** - * Many user agents mishandle 302 in ways that violate the RFC1945 - * spec (i.e., redirect a POST to a GET). 303 and 307 were added in - * RFC2616 to address this. You should prefer 303 and 307 unless the - * calling user agent does not support 303 and 307 functionality - */ - @Deprecated - FOUND(302, "Found"), - REDIRECT_SEE_OTHER(303, "See Other"), - NOT_MODIFIED(304, "Not Modified"), - TEMPORARY_REDIRECT(307, "Temporary Redirect"), - - BAD_REQUEST(400, "Bad Request"), - UNAUTHORIZED(401, "Unauthorized"), - FORBIDDEN(403, "Forbidden"), - NOT_FOUND(404, "Not Found"), - METHOD_NOT_ALLOWED(405, "Method Not Allowed"), - NOT_ACCEPTABLE(406, "Not Acceptable"), - REQUEST_TIMEOUT(408, "Request Timeout"), - CONFLICT(409, "Conflict"), - GONE(410, "Gone"), - LENGTH_REQUIRED(411, "Length Required"), - PRECONDITION_FAILED(412, "Precondition Failed"), - PAYLOAD_TOO_LARGE(413, "Payload Too Large"), - UNSUPPORTED_MEDIA_TYPE(415, "Unsupported Media Type"), - RANGE_NOT_SATISFIABLE(416, "Requested Range Not Satisfiable"), - EXPECTATION_FAILED(417, "Expectation Failed"), - TOO_MANY_REQUESTS(429, "Too Many Requests"), - - INTERNAL_ERROR(500, "Internal Server Error"), - NOT_IMPLEMENTED(501, "Not Implemented"), - BAD_GATEWAY(502, "Bad Gateway"), - SERVICE_UNAVAILABLE(503, "Service Unavailable"), - UNSUPPORTED_HTTP_VERSION(505, "HTTP Version Not Supported"); - - private final int requestStatus; - - private final String description; - - Status(int requestStatus, String description) { - this.requestStatus = requestStatus; - this.description = description; - } - - public static Status lookup(int requestStatus) { - for (Status status : Status.values()) { - if (status.getRequestStatus() == requestStatus) { - return status; - } - } - return null; - } - - @Override - public String getDescription() { - return "" + this.requestStatus + " " + this.description; - } - - @Override - public int getRequestStatus() { - return this.requestStatus; - } -} diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/package-info.java b/src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/package-info.java deleted file mode 100644 index fd12229..0000000 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Modified nanohttpd. - *

- * See license in META-INF/licenses/nanohttpd.txt - */ -package xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen; diff --git a/src/main/resources/META-INF/licenses/nanohttpd.txt b/src/main/resources/META-INF/licenses/nanohttpd.txt deleted file mode 100644 index a715254..0000000 --- a/src/main/resources/META-INF/licenses/nanohttpd.txt +++ /dev/null @@ -1,26 +0,0 @@ -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: - -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. diff --git a/src/test/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/ChunkedInputStreamTest.java b/src/test/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/ChunkedInputStreamTest.java deleted file mode 100644 index 4be77f3..0000000 --- a/src/test/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/ChunkedInputStreamTest.java +++ /dev/null @@ -1,199 +0,0 @@ -/* - * Copyright (C) 2019 Haowei Wen and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen; - -import static java.nio.charset.StandardCharsets.US_ASCII; -import static xyz.zuoyx.multiyggdrasil.util.IOUtils.asBytes; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.io.ByteArrayInputStream; -import java.io.EOFException; -import java.io.IOException; -import java.io.InputStream; - -import org.junit.jupiter.api.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 - public void testReadEOF1() { - assertThrows(EOFException.class, () -> { - byte[] data = ("a").getBytes(US_ASCII); - ByteArrayInputStream underlying = new ByteArrayInputStream(data); - InputStream in = new ChunkedInputStream(underlying); - asBytes(in); - }); - } - - @Test - public void testReadEOF2() { - assertThrows(EOFException.class, () -> { - byte[] data = ("a\r").getBytes(US_ASCII); - ByteArrayInputStream underlying = new ByteArrayInputStream(data); - InputStream in = new ChunkedInputStream(underlying); - asBytes(in); - }); - } - - @Test - public void testReadEOF3() { - assertThrows(EOFException.class, () -> { - byte[] data = ("a\r\n").getBytes(US_ASCII); - ByteArrayInputStream underlying = new ByteArrayInputStream(data); - InputStream in = new ChunkedInputStream(underlying); - asBytes(in); - }); - } - - @Test - public void testReadEOF4() { - assertThrows(EOFException.class, () -> { - byte[] data = ("a\r\nabc").getBytes(US_ASCII); - ByteArrayInputStream underlying = new ByteArrayInputStream(data); - InputStream in = new ChunkedInputStream(underlying); - asBytes(in); - }); - } - - @Test - public void testReadEOF5() { - assertThrows(EOFException.class, () -> { - byte[] data = ("a\r\n123456789a\r").getBytes(US_ASCII); - ByteArrayInputStream underlying = new ByteArrayInputStream(data); - InputStream in = new ChunkedInputStream(underlying); - asBytes(in); - }); - } - - @Test - public void testReadEOF6() { - assertThrows(EOFException.class, () -> { - byte[] data = ("a\r\n123456789a\r\n").getBytes(US_ASCII); - ByteArrayInputStream underlying = new ByteArrayInputStream(data); - InputStream in = new ChunkedInputStream(underlying); - asBytes(in); - }); - } - - @Test - public void testReadEOF7() { - assertThrows(EOFException.class, () -> { - 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 - public void testBadIn1() { - assertThrows(IOException.class, () -> { - byte[] data = ("-1").getBytes(US_ASCII); - ByteArrayInputStream underlying = new ByteArrayInputStream(data); - InputStream in = new ChunkedInputStream(underlying); - asBytes(in); - }); - } - - @Test - public void testBadIn2() { - assertThrows(IOException.class, () -> { - byte[] data = ("a\ra").getBytes(US_ASCII); - ByteArrayInputStream underlying = new ByteArrayInputStream(data); - InputStream in = new ChunkedInputStream(underlying); - asBytes(in); - }); - } - - @Test - public void testBadIn3() { - assertThrows(IOException.class, () -> { - byte[] data = ("a\r\n123456789aa").getBytes(US_ASCII); - ByteArrayInputStream underlying = new ByteArrayInputStream(data); - InputStream in = new ChunkedInputStream(underlying); - asBytes(in); - }); - } - - @Test - public void testBadIn4() { - assertThrows(IOException.class, () -> { - byte[] data = ("a\r\n123456789a\ra").getBytes(US_ASCII); - ByteArrayInputStream underlying = new ByteArrayInputStream(data); - InputStream in = new ChunkedInputStream(underlying); - asBytes(in); - }); - } - - @Test - public void testBadIn5() { - assertThrows(IOException.class, () -> { - 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/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/FixedLengthInputStreamTest.java b/src/test/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/FixedLengthInputStreamTest.java deleted file mode 100644 index b3cdfec..0000000 --- a/src/test/java/xyz/zuoyx/multiyggdrasil/internal/fi/iki/elonen/FixedLengthInputStreamTest.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (C) 2019 Haowei Wen and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen; - -import static xyz.zuoyx.multiyggdrasil.util.IOUtils.asBytes; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.io.ByteArrayInputStream; -import java.io.EOFException; -import java.io.IOException; -import java.io.InputStream; -import java.util.Arrays; - -import org.junit.jupiter.api.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 - public void testReadEOF() throws IOException { - assertThrows(EOFException.class, () -> { - byte[] data = new byte[]{0x11, 0x22, 0x33, 0x44, 0x55}; - InputStream in = new FixedLengthInputStream(new ByteArrayInputStream(data), 6); - asBytes(in); - }); - } -} From ae38fbb4219007284470533c1a10fd0d56f7d4e5 Mon Sep 17 00:00:00 2001 From: Ethan Zuo Date: Mon, 26 Jun 2023 18:39:02 +0800 Subject: [PATCH 2/9] Switching from NanoHTTPD to HttpServer --- .../zuoyx/multiyggdrasil/MultiYggdrasil.java | 10 +- .../httpd/AntiFeaturesFilter.java | 32 +++-- .../httpd/DebugApiEndpoint.java | 14 +- .../httpd/LegacySkinAPIFilter.java | 23 ++-- .../httpd/MultiHasJoinedServerFilter.java | 24 ++-- .../httpd/MultiQueryProfileFilter.java | 27 ++-- .../httpd/MultiQueryUUIDsFilter.java | 18 ++- .../httpd/ProfileKeyFilter.java | 16 +-- .../httpd/PublickeysFilter.java | 15 +- .../httpd/QueryProfileFilter.java | 27 ++-- .../httpd/QueryUUIDsFilter.java | 18 ++- .../zuoyx/multiyggdrasil/httpd/URLFilter.java | 6 +- .../multiyggdrasil/httpd/URLProcessor.java | 129 +++++++++--------- .../zuoyx/multiyggdrasil/util/IOUtils.java | 59 ++++++-- .../yggdrasil/YggdrasilClient.java | 7 + 15 files changed, 232 insertions(+), 193 deletions(-) diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/MultiYggdrasil.java b/src/main/java/xyz/zuoyx/multiyggdrasil/MultiYggdrasil.java index 880a7e8..6875851 100644 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/MultiYggdrasil.java +++ b/src/main/java/xyz/zuoyx/multiyggdrasil/MultiYggdrasil.java @@ -36,6 +36,8 @@ import java.io.UncheckedIOException; import java.lang.instrument.Instrumentation; import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Paths; import java.util.ArrayList; @@ -143,7 +145,7 @@ private static APIMetadata fetchAPIMetadata(String apiUrl) { } else { try { - HttpURLConnection connection = (HttpURLConnection) new URL(apiUrl).openConnection(); + HttpURLConnection connection = (HttpURLConnection) new URI(apiUrl).toURL().openConnection(); String ali = connection.getHeaderField("x-authlib-injector-api-location"); if (ali != null) { @@ -169,7 +171,7 @@ private static APIMetadata fetchAPIMetadata(String apiUrl) { try (InputStream in = connection.getInputStream()) { metadataResponse = asString(asBytes(in)); } - } catch (IOException e) { + } catch (URISyntaxException | IOException e) { log(ERROR, "Failed to fetch metadata: " + e); throw new InitializationException(e); } @@ -211,8 +213,8 @@ private static void warnIfHttp(String url) { } private static String addHttpsIfMissing(String url) { - String lowercased = url.toLowerCase(); - if (!lowercased.startsWith("http://") && !lowercased.startsWith("https://")) { + String lowerCase = url.toLowerCase(); + if (!lowerCase.startsWith("http://") && !lowerCase.startsWith("https://")) { url = "https://" + url; } return url; diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/AntiFeaturesFilter.java b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/AntiFeaturesFilter.java index fc3a96d..bb13e26 100644 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/AntiFeaturesFilter.java +++ b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/AntiFeaturesFilter.java @@ -17,11 +17,9 @@ package xyz.zuoyx.multiyggdrasil.httpd; import static xyz.zuoyx.multiyggdrasil.util.IOUtils.CONTENT_TYPE_JSON; -import static xyz.zuoyx.multiyggdrasil.util.IOUtils.CONTENT_TYPE_TEXT; -import java.util.Optional; -import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.IHTTPSession; -import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.Response; -import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.Status; +import static xyz.zuoyx.multiyggdrasil.util.IOUtils.sendResponse; +import java.io.IOException; +import com.sun.net.httpserver.HttpExchange; /** * Disables Mojang's anti-features. @@ -38,17 +36,21 @@ public boolean canHandle(String domain) { } @Override - public Optional handle(String domain, String path, IHTTPSession session) { - if (domain.equals("api.minecraftservices.com") && path.equals("/privileges") && session.getMethod().equals("GET")) { - return Optional.of(Response.newFixedLength(Status.OK, CONTENT_TYPE_JSON, RESPONSE_PRIVILEGES)); - } else if (domain.equals("api.minecraftservices.com") && path.equals("/player/attributes") && session.getMethod().equals("GET")) { - return Optional.of(Response.newFixedLength(Status.OK, CONTENT_TYPE_JSON, RESPONSE_PLAYER_ATTRIBUTES)); - } else if (domain.equals("api.minecraftservices.com") && path.equals("/privacy/blocklist") && session.getMethod().equals("GET")) { - return Optional.of(Response.newFixedLength(Status.OK, CONTENT_TYPE_JSON, RESPONSE_PRIVACY_BLOCKLIST)); - } else if (domain.equals("sessionserver.mojang.com") && path.equals("/blockedservers") && session.getMethod().equals("GET")) { - return Optional.of(Response.newFixedLength(Status.NOT_FOUND, CONTENT_TYPE_TEXT, "")); + public boolean handle(String domain, String path, HttpExchange exchange) throws IOException { + if (domain.equals("api.minecraftservices.com") && path.equals("/privileges") && exchange.getRequestMethod().equals("GET")) { + sendResponse(exchange, 200, CONTENT_TYPE_JSON, RESPONSE_PRIVILEGES.getBytes()); + return true; + } else if (domain.equals("api.minecraftservices.com") && path.equals("/player/attributes") && exchange.getRequestMethod().equals("GET")) { + sendResponse(exchange, 200, CONTENT_TYPE_JSON, RESPONSE_PLAYER_ATTRIBUTES.getBytes()); + return true; + } else if (domain.equals("api.minecraftservices.com") && path.equals("/privacy/blocklist") && exchange.getRequestMethod().equals("GET")) { + sendResponse(exchange, 200, CONTENT_TYPE_JSON, RESPONSE_PRIVACY_BLOCKLIST.getBytes()); + return true; + } else if (domain.equals("sessionserver.mojang.com") && path.equals("/blockedservers") && exchange.getRequestMethod().equals("GET")) { + sendResponse(exchange, 404, null, null); + return true; } else { - return Optional.empty(); + return false; } } } diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/DebugApiEndpoint.java b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/DebugApiEndpoint.java index 66890ce..2b9c1d3 100644 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/DebugApiEndpoint.java +++ b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/DebugApiEndpoint.java @@ -17,12 +17,12 @@ package xyz.zuoyx.multiyggdrasil.httpd; import static xyz.zuoyx.multiyggdrasil.util.IOUtils.CONTENT_TYPE_JSON; +import static xyz.zuoyx.multiyggdrasil.util.IOUtils.sendResponse; import static xyz.zuoyx.multiyggdrasil.util.JsonUtils.toJsonString; +import java.io.IOException; +import com.sun.net.httpserver.HttpExchange; import com.google.gson.JsonObject; import xyz.zuoyx.multiyggdrasil.MultiYggdrasil; -import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.IHTTPSession; -import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.Response; -import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.Status; import xyz.zuoyx.multiyggdrasil.transform.PerformanceMetrics; /** @@ -30,8 +30,8 @@ */ public class DebugApiEndpoint { - public Response serve(IHTTPSession session) { - if (session.getUri().equals("/debug/metrics") && session.getMethod().equals("GET")) { + public void serve(HttpExchange exchange) throws IOException { + if (exchange.getRequestURI().getPath().equals("/debug/metrics") && exchange.getRequestMethod().equals("GET")) { PerformanceMetrics metrics = MultiYggdrasil.getClassTransformer().performanceMetrics; JsonObject response = new JsonObject(); response.addProperty("totalTime", metrics.getTotalTime()); @@ -40,9 +40,9 @@ public Response serve(IHTTPSession session) { response.addProperty("analysisTime", metrics.getAnalysisTime()); response.addProperty("classesScanned", metrics.getClassesScanned()); response.addProperty("classesSkipped", metrics.getClassesSkipped()); - return Response.newFixedLength(Status.OK, CONTENT_TYPE_JSON, toJsonString(response)); + sendResponse(exchange, 200, CONTENT_TYPE_JSON, toJsonString(response).getBytes()); } else { - return Response.newFixedLength(Status.NOT_FOUND, null, null); + sendResponse(exchange, 404, null, null); } } } diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/LegacySkinAPIFilter.java b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/LegacySkinAPIFilter.java index 0e97f95..a34d106 100644 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/LegacySkinAPIFilter.java +++ b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/LegacySkinAPIFilter.java @@ -17,28 +17,26 @@ package xyz.zuoyx.multiyggdrasil.httpd; import static java.nio.charset.StandardCharsets.ISO_8859_1; -import static java.util.Optional.empty; -import static java.util.Optional.of; import static java.util.Optional.ofNullable; +import static xyz.zuoyx.multiyggdrasil.util.IOUtils.CONTENT_TYPE_IMAGE; import static xyz.zuoyx.multiyggdrasil.util.IOUtils.asString; import static xyz.zuoyx.multiyggdrasil.util.IOUtils.http; import static xyz.zuoyx.multiyggdrasil.util.IOUtils.newUncheckedIOException; +import static xyz.zuoyx.multiyggdrasil.util.IOUtils.sendResponse; import static xyz.zuoyx.multiyggdrasil.util.JsonUtils.parseJson; import static xyz.zuoyx.multiyggdrasil.util.Logging.log; import static xyz.zuoyx.multiyggdrasil.util.Logging.Level.DEBUG; import static xyz.zuoyx.multiyggdrasil.util.Logging.Level.INFO; -import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.UncheckedIOException; +import java.net.URISyntaxException; import java.util.Base64; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.IHTTPSession; -import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.Response; -import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.Status; +import com.sun.net.httpserver.HttpExchange; import xyz.zuoyx.multiyggdrasil.util.JsonUtils; import xyz.zuoyx.multiyggdrasil.yggdrasil.YggdrasilClient; @@ -58,12 +56,12 @@ public boolean canHandle(String domain) { } @Override - public Optional handle(String domain, String path, IHTTPSession session) { + public boolean handle(String domain, String path, HttpExchange exchange) throws IOException { if (!domain.equals("skins.minecraft.net")) - return empty(); + return false; Matcher matcher = PATH_SKINS.matcher(path); if (!matcher.find()) - return empty(); + return false; String username = matcher.group("username"); // Minecraft does not encode non-ASCII characters in URLs @@ -87,16 +85,17 @@ public Optional handle(String domain, String path, IHTTPSession sessio byte[] data; try { data = http("GET", url); - } catch (IOException e) { + } catch (URISyntaxException | IOException e) { throw newUncheckedIOException("Failed to retrieve skin from " + url, e); } log(INFO, "Retrieved skin for " + username + " from " + url + ", " + data.length + " bytes"); - return of(Response.newFixedLength(Status.OK, "image/png", new ByteArrayInputStream(data), data.length)); + sendResponse(exchange, 200, CONTENT_TYPE_IMAGE, data); } else { log(INFO, "No skin is found for " + username); - return of(Response.newFixedLength(Status.NOT_FOUND, null, null)); + sendResponse(exchange, 404, null, null); } + return true; } private Optional obtainTextureUrl(String texturesPayload, String textureType) throws UncheckedIOException { diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/MultiHasJoinedServerFilter.java b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/MultiHasJoinedServerFilter.java index 6ba5681..71ce703 100644 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/MultiHasJoinedServerFilter.java +++ b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/MultiHasJoinedServerFilter.java @@ -17,17 +17,17 @@ package xyz.zuoyx.multiyggdrasil.httpd; import static xyz.zuoyx.multiyggdrasil.util.IOUtils.CONTENT_TYPE_JSON; +import static xyz.zuoyx.multiyggdrasil.util.IOUtils.parseQueryParams; +import static xyz.zuoyx.multiyggdrasil.util.IOUtils.sendResponse; import static xyz.zuoyx.multiyggdrasil.util.Logging.Level.ERROR; import static xyz.zuoyx.multiyggdrasil.util.Logging.log; +import java.io.IOException; import java.io.UncheckedIOException; -import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; -import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.IHTTPSession; -import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.Response; -import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.Status; +import com.sun.net.httpserver.HttpExchange; import xyz.zuoyx.multiyggdrasil.yggdrasil.GameProfile; import xyz.zuoyx.multiyggdrasil.yggdrasil.NamespacedID; import xyz.zuoyx.multiyggdrasil.yggdrasil.YggdrasilAPIProvider; @@ -55,12 +55,9 @@ public boolean canHandle(String domain) { } @Override - public Optional handle(String domain, String path, IHTTPSession session) { - if (domain.equals("sessionserver.mojang.com") && path.equals("/session/minecraft/hasJoined") && session.getMethod().equals("GET")) { - Map params = new LinkedHashMap<>(); - session.getParameters().forEach( - (key ,value) -> params.put(key, value.get(0)) - ); + public boolean handle(String domain, String path, HttpExchange exchange) throws IOException { + if (domain.equals("sessionserver.mojang.com") && path.equals("/session/minecraft/hasJoined") && exchange.getRequestMethod().equals("GET")) { + Map params = parseQueryParams(exchange.getRequestURI().getQuery()); Optional response = Optional.empty(); for (YggdrasilClient client : clients) { @@ -81,12 +78,13 @@ public Optional handle(String domain, String path, IHTTPSession sessio } if (response.isPresent()) { - return Optional.of(Response.newFixedLength(Status.OK, CONTENT_TYPE_JSON, YggdrasilResponseBuilder.hasJoinedServer(response.get()))); + sendResponse(exchange, 200, CONTENT_TYPE_JSON, YggdrasilResponseBuilder.hasJoinedServer(response.get()).getBytes()); } else { - return Optional.of(Response.newFixedLength(Status.NO_CONTENT, null, null)); + sendResponse(exchange, 204, null, null); } + return true; } else { - return Optional.empty(); + return false; } } } diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/MultiQueryProfileFilter.java b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/MultiQueryProfileFilter.java index 72e49f2..ae0ae3a 100644 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/MultiQueryProfileFilter.java +++ b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/MultiQueryProfileFilter.java @@ -16,19 +16,18 @@ */ package xyz.zuoyx.multiyggdrasil.httpd; -import static java.util.Optional.empty; import static xyz.zuoyx.multiyggdrasil.util.IOUtils.CONTENT_TYPE_JSON; +import static xyz.zuoyx.multiyggdrasil.util.IOUtils.parseQueryParams; +import static xyz.zuoyx.multiyggdrasil.util.IOUtils.sendResponse; import static xyz.zuoyx.multiyggdrasil.util.UUIDUtils.fromUnsignedUUID; -import java.util.List; +import java.io.IOException; import java.util.Optional; import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; -import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.IHTTPSession; -import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.Response; -import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.Status; +import com.sun.net.httpserver.HttpExchange; import xyz.zuoyx.multiyggdrasil.yggdrasil.GameProfile; import xyz.zuoyx.multiyggdrasil.yggdrasil.NamespacedID; import xyz.zuoyx.multiyggdrasil.yggdrasil.YggdrasilClient; @@ -54,23 +53,24 @@ public boolean canHandle(String domain) { } @Override - public Optional handle(String domain, String path, IHTTPSession session) { + public boolean handle(String domain, String path, HttpExchange exchange) throws IOException { if (!domain.equals("sessionserver.mojang.com")) - return empty(); + return false; Matcher matcher = PATH_REGEX.matcher(path); if (!matcher.find()) - return empty(); + return false; UUID uuid; try { uuid = fromUnsignedUUID(matcher.group("uuid")); } catch (IllegalArgumentException e) { - return Optional.of(Response.newFixedLength(Status.NO_CONTENT, null, null)); + sendResponse(exchange, 204, null, null); + return true; } boolean withSignature = false; - List unsignedValues = session.getParameters().get("unsigned"); - if (unsignedValues != null && unsignedValues.get(0).equals("false")) { + String unsignedValues = parseQueryParams(exchange.getRequestURI().getQuery()).get("unsigned"); + if (unsignedValues != null && unsignedValues.equals("false")) { withSignature = true; } @@ -83,10 +83,11 @@ public Optional handle(String domain, String path, IHTTPSession sessio } if (response.isPresent()) { - return Optional.of(Response.newFixedLength(Status.OK, CONTENT_TYPE_JSON, YggdrasilResponseBuilder.queryProfile(response.get(), withSignature))); + sendResponse(exchange, 200, CONTENT_TYPE_JSON, YggdrasilResponseBuilder.queryProfile(response.get(), withSignature).getBytes()); } else { - return Optional.of(Response.newFixedLength(Status.NO_CONTENT, null, null)); + sendResponse(exchange, 204, null, null); } + return true; } } diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/MultiQueryUUIDsFilter.java b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/MultiQueryUUIDsFilter.java index 2ab1c53..bf26d57 100644 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/MultiQueryUUIDsFilter.java +++ b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/MultiQueryUUIDsFilter.java @@ -19,18 +19,16 @@ import static xyz.zuoyx.multiyggdrasil.util.IOUtils.CONTENT_TYPE_JSON; import static xyz.zuoyx.multiyggdrasil.util.IOUtils.asBytes; import static xyz.zuoyx.multiyggdrasil.util.IOUtils.asString; +import static xyz.zuoyx.multiyggdrasil.util.IOUtils.sendResponse; import static xyz.zuoyx.multiyggdrasil.util.JsonUtils.asJsonString; import static xyz.zuoyx.multiyggdrasil.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 xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.IHTTPSession; -import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.Response; -import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.Status; +import com.sun.net.httpserver.HttpExchange; import xyz.zuoyx.multiyggdrasil.yggdrasil.NamespacedID; import xyz.zuoyx.multiyggdrasil.yggdrasil.YggdrasilClient; import xyz.zuoyx.multiyggdrasil.yggdrasil.YggdrasilResponseBuilder; @@ -53,15 +51,15 @@ public boolean canHandle(String domain) { } @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")) { + public boolean handle(String domain, String path, HttpExchange exchange) throws IOException { + if (domain.equals("api.mojang.com") && path.equals("/profiles/minecraft") && exchange.getRequestMethod().equals("POST")) { Set request = new LinkedHashSet<>(); - parseJson(asString(asBytes(session.getInputStream()))).getAsJsonArray() + parseJson(asString(asBytes(exchange.getRequestBody()))).getAsJsonArray() .forEach(element -> request.add(asJsonString(element))); - return Optional.of(Response.newFixedLength(Status.OK, CONTENT_TYPE_JSON, - YggdrasilResponseBuilder.queryUUIDs(performQuery(request)))); + sendResponse(exchange, 200, CONTENT_TYPE_JSON, YggdrasilResponseBuilder.queryUUIDs(performQuery(request)).getBytes()); + return true; } else { - return Optional.empty(); + return false; } } diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/ProfileKeyFilter.java b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/ProfileKeyFilter.java index c84fd86..e7c692e 100644 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/ProfileKeyFilter.java +++ b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/ProfileKeyFilter.java @@ -18,7 +18,9 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static xyz.zuoyx.multiyggdrasil.util.IOUtils.CONTENT_TYPE_JSON; +import static xyz.zuoyx.multiyggdrasil.util.IOUtils.sendResponse; import static xyz.zuoyx.multiyggdrasil.util.JsonUtils.toJsonString; +import java.io.IOException; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; @@ -26,11 +28,8 @@ import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.Base64; -import java.util.Optional; +import com.sun.net.httpserver.HttpExchange; import com.google.gson.JsonObject; -import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.IHTTPSession; -import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.Response; -import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.Status; /** * Intercepts Minecraft's request to ..., @@ -44,11 +43,12 @@ public boolean canHandle(String domain) { } @Override - public Optional handle(String domain, String path, IHTTPSession session) { - if (domain.equals("api.minecraftservices.com") && path.equals("/player/certificates") && session.getMethod().equals("POST")) { - return Optional.of(Response.newFixedLength(Status.OK, CONTENT_TYPE_JSON, toJsonString(makeDummyResponse()))); + public boolean handle(String domain, String path, HttpExchange exchange) throws IOException { + if (domain.equals("api.minecraftservices.com") && path.equals("/player/certificates") && exchange.getRequestMethod().equals("POST")) { + sendResponse(exchange, 200, CONTENT_TYPE_JSON, toJsonString(makeDummyResponse()).getBytes()); + return true; } - return Optional.empty(); + return false; } private JsonObject makeDummyResponse() { diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/PublickeysFilter.java b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/PublickeysFilter.java index bd5d514..ae9ca76 100644 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/PublickeysFilter.java +++ b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/PublickeysFilter.java @@ -17,15 +17,13 @@ package xyz.zuoyx.multiyggdrasil.httpd; import static xyz.zuoyx.multiyggdrasil.util.IOUtils.CONTENT_TYPE_JSON; +import static xyz.zuoyx.multiyggdrasil.util.IOUtils.sendResponse; import static xyz.zuoyx.multiyggdrasil.util.JsonUtils.toJsonString; import java.io.IOException; import java.security.PublicKey; import java.util.Base64; -import java.util.Optional; -import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.IHTTPSession; -import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.Response; -import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.Status; +import com.sun.net.httpserver.HttpExchange; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import xyz.zuoyx.multiyggdrasil.transform.support.YggdrasilKeyTransformUnit; @@ -38,11 +36,12 @@ public boolean canHandle(String domain) { } @Override - public Optional handle(String domain, String path, IHTTPSession session) throws IOException { - if (domain.equals("api.minecraftservices.com") && path.equals("/publickeys") && session.getMethod().equals("GET")) { - return Optional.of(Response.newFixedLength(Status.OK, CONTENT_TYPE_JSON, toJsonString(makePublickeysResponse()))); + public boolean handle(String domain, String path, HttpExchange exchange) throws IOException { + if (domain.equals("api.minecraftservices.com") && path.equals("/publickeys") && exchange.getRequestMethod().equals("GET")) { + sendResponse(exchange, 200, CONTENT_TYPE_JSON, toJsonString(makePublickeysResponse()).getBytes()); + return true; } - return Optional.empty(); + return false; } private JsonObject makePublickeysResponse() { diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/QueryProfileFilter.java b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/QueryProfileFilter.java index 680cc0a..828c6e7 100644 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/QueryProfileFilter.java +++ b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/QueryProfileFilter.java @@ -16,19 +16,18 @@ */ package xyz.zuoyx.multiyggdrasil.httpd; -import static java.util.Optional.empty; import static xyz.zuoyx.multiyggdrasil.util.IOUtils.CONTENT_TYPE_JSON; +import static xyz.zuoyx.multiyggdrasil.util.IOUtils.parseQueryParams; +import static xyz.zuoyx.multiyggdrasil.util.IOUtils.sendResponse; import static xyz.zuoyx.multiyggdrasil.util.UUIDUtils.fromUnsignedUUID; -import java.util.List; +import java.io.IOException; import java.util.Optional; import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; -import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.IHTTPSession; -import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.Response; -import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.Status; +import com.sun.net.httpserver.HttpExchange; import xyz.zuoyx.multiyggdrasil.yggdrasil.GameProfile; import xyz.zuoyx.multiyggdrasil.yggdrasil.YggdrasilClient; import xyz.zuoyx.multiyggdrasil.yggdrasil.YggdrasilResponseBuilder; @@ -51,23 +50,24 @@ public boolean canHandle(String domain) { } @Override - public Optional handle(String domain, String path, IHTTPSession session) { + public boolean handle(String domain, String path, HttpExchange exchange) throws IOException { if (!domain.equals("sessionserver.mojang.com")) - return empty(); + return false; Matcher matcher = PATH_REGEX.matcher(path); if (!matcher.find()) - return empty(); + return false; UUID uuid; try { uuid = fromUnsignedUUID(matcher.group("uuid")); } catch (IllegalArgumentException e) { - return Optional.of(Response.newFixedLength(Status.NO_CONTENT, null, null)); + sendResponse(exchange, 204, null, null); + return true; } boolean withSignature = false; - List unsignedValues = session.getParameters().get("unsigned"); - if (unsignedValues != null && unsignedValues.get(0).equals("false")) { + String unsignedValues = parseQueryParams(exchange.getRequestURI().getQuery()).get("unsigned"); + if (unsignedValues != null && unsignedValues.equals("false")) { withSignature = true; } @@ -83,10 +83,11 @@ public Optional handle(String domain, String path, IHTTPSession sessio } if (response.isPresent()) { - return Optional.of(Response.newFixedLength(Status.OK, CONTENT_TYPE_JSON, YggdrasilResponseBuilder.queryProfile(response.get(), withSignature))); + sendResponse(exchange, 200, CONTENT_TYPE_JSON, YggdrasilResponseBuilder.queryProfile(response.get(), withSignature).getBytes()); } else { - return Optional.of(Response.newFixedLength(Status.NO_CONTENT, null, null)); + sendResponse(exchange, 204, null, null); } + return true; } } diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/QueryUUIDsFilter.java b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/QueryUUIDsFilter.java index 9166880..7c166d9 100644 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/QueryUUIDsFilter.java +++ b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/QueryUUIDsFilter.java @@ -19,6 +19,7 @@ import static xyz.zuoyx.multiyggdrasil.util.IOUtils.CONTENT_TYPE_JSON; import static xyz.zuoyx.multiyggdrasil.util.IOUtils.asBytes; import static xyz.zuoyx.multiyggdrasil.util.IOUtils.asString; +import static xyz.zuoyx.multiyggdrasil.util.IOUtils.sendResponse; import static xyz.zuoyx.multiyggdrasil.util.JsonUtils.asJsonString; import static xyz.zuoyx.multiyggdrasil.util.JsonUtils.parseJson; import static xyz.zuoyx.multiyggdrasil.util.Logging.log; @@ -27,12 +28,9 @@ 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 xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.IHTTPSession; -import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.Response; -import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.Status; +import com.sun.net.httpserver.HttpExchange; import xyz.zuoyx.multiyggdrasil.yggdrasil.YggdrasilClient; import xyz.zuoyx.multiyggdrasil.yggdrasil.YggdrasilResponseBuilder; @@ -52,15 +50,15 @@ public boolean canHandle(String domain) { } @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")) { + public boolean handle(String domain, String path, HttpExchange exchange) throws IOException { + if (domain.equals("api.mojang.com") && path.equals("/profiles/minecraft") && exchange.getRequestMethod().equals("POST")) { Set request = new LinkedHashSet<>(); - parseJson(asString(asBytes(session.getInputStream()))).getAsJsonArray() + parseJson(asString(asBytes(exchange.getRequestBody()))).getAsJsonArray() .forEach(element -> request.add(asJsonString(element))); - return Optional.of(Response.newFixedLength(Status.OK, CONTENT_TYPE_JSON, - YggdrasilResponseBuilder.queryUUIDs(performQuery(request)))); + sendResponse(exchange, 200, CONTENT_TYPE_JSON, YggdrasilResponseBuilder.queryUUIDs(performQuery(request)).getBytes()); + return true; } else { - return Optional.empty(); + return false; } } diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/URLFilter.java b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/URLFilter.java index 6209743..ddb249e 100644 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/URLFilter.java +++ b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/URLFilter.java @@ -17,10 +17,8 @@ package xyz.zuoyx.multiyggdrasil.httpd; import java.io.IOException; -import java.util.Optional; -import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.IHTTPSession; -import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.Response; +import com.sun.net.httpserver.HttpExchange; /** * A URLFilter filters the URLs in the bytecode, and intercepts those it is interested in. @@ -37,5 +35,5 @@ public interface URLFilter { */ boolean canHandle(String domain); - Optional handle(String domain, String path, IHTTPSession session) throws IOException; + boolean handle(String domain, String path, HttpExchange exchange) throws IOException; } diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/URLProcessor.java b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/URLProcessor.java index b13e465..3927f43 100644 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/URLProcessor.java +++ b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/URLProcessor.java @@ -17,6 +17,8 @@ package xyz.zuoyx.multiyggdrasil.httpd; import static xyz.zuoyx.multiyggdrasil.util.IOUtils.CONTENT_TYPE_TEXT; +import static xyz.zuoyx.multiyggdrasil.util.IOUtils.asBytes; +import static xyz.zuoyx.multiyggdrasil.util.IOUtils.sendResponse; import static xyz.zuoyx.multiyggdrasil.util.IOUtils.transfer; import static xyz.zuoyx.multiyggdrasil.util.Logging.log; import static xyz.zuoyx.multiyggdrasil.util.Logging.Level.DEBUG; @@ -26,7 +28,9 @@ import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; -import java.net.URL; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URISyntaxException; import java.util.Arrays; import java.util.HashSet; import java.util.LinkedHashMap; @@ -35,14 +39,13 @@ import java.util.Map.Entry; import java.util.Optional; import java.util.Set; +import java.util.concurrent.Executors; import java.util.regex.Matcher; import java.util.regex.Pattern; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; import xyz.zuoyx.multiyggdrasil.Config; -import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.IHTTPSession; -import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.IStatus; -import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.NanoHTTPD; -import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.Response; -import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.Status; public class URLProcessor { @@ -104,50 +107,54 @@ private Optional transform(String protocol, String domain, String path) } private DebugApiEndpoint debugApi = new DebugApiEndpoint(); - private volatile NanoHTTPD httpd; - private final Object httpdLock = new Object(); + private volatile HttpServer httpServer; + private final Object httpServerLock = new Object(); private int getLocalApiPort() { - synchronized (httpdLock) { - if (httpd == null) { - httpd = createHttpd(); + synchronized (httpServerLock) { + if (httpServer == null) { try { - httpd.start(); + httpServer = createHttpServer(); } catch (IOException e) { - throw new IllegalStateException("Httpd failed to start"); + throw new IllegalStateException("HTTP server failed to create"); } - log(INFO, "Httpd is running on port " + httpd.getListeningPort()); + httpServer.setExecutor(Executors.newCachedThreadPool()); + httpServer.start(); + log(INFO, "HTTP server is running on port " + httpServer.getAddress().getPort()); } - return httpd.getListeningPort(); + return httpServer.getAddress().getPort(); } } - private NanoHTTPD createHttpd() { - return new NanoHTTPD("127.0.0.1", Config.httpdPort) { + private HttpServer createHttpServer() throws IOException { + HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", Config.httpdPort), 0); + server.createContext("/", new HttpHandler() { @Override - public Response serve(IHTTPSession session) { - if (session.getUri().startsWith("/debug/")) { - return debugApi.serve(session); + public void handle(HttpExchange exchange) throws IOException { + if (exchange.getRequestURI().getPath().startsWith("/debug/")) { + debugApi.serve(exchange); + return; } - Matcher matcher = LOCAL_URL_REGEX.matcher(session.getUri()); + Matcher matcher = LOCAL_URL_REGEX.matcher(exchange.getRequestURI().getPath()); if (matcher.find()) { String protocol = matcher.group("protocol"); String domain = matcher.group("domain"); String path = matcher.group("path"); for (URLFilter filter : filters) { if (filter.canHandle(domain)) { - Optional result; + boolean result; try { - result = filter.handle(domain, path, session); + result = filter.handle(domain, path, exchange); } catch (Throwable e) { - log(WARNING, "An error occurred while processing request [" + session.getUri() + "]", e); - return Response.newFixedLength(Status.INTERNAL_ERROR, CONTENT_TYPE_TEXT, "Internal Server Error"); + log(WARNING, "An error occurred while processing request [" + exchange.getRequestURI().getPath() + "]", e); + sendResponse(exchange, 500, CONTENT_TYPE_TEXT, "Internal Server Error".getBytes()); + return; } - if (result.isPresent()) { - log(DEBUG, "Request to [" + session.getUri() + "] is handled by [" + filter + "]"); - return result.get(); + if (result) { + log(DEBUG, "Request to [" + exchange.getRequestURI().getPath() + "] is handled by [" + filter + "]"); + return; } } } @@ -155,40 +162,44 @@ public Response serve(IHTTPSession session) { String target = redirector.redirect(domain, path) .orElseGet(() -> protocol + "://" + domain + path); try { - return reverseProxy(session, target); - } catch (IOException e) { + reverseProxy(exchange, target); + } catch (URISyntaxException | IOException e) { log(WARNING, "Reverse proxy error", e); - return Response.newFixedLength(Status.BAD_GATEWAY, CONTENT_TYPE_TEXT, "Bad Gateway"); + sendResponse(exchange, 502, CONTENT_TYPE_TEXT, "Bad Gateway".getBytes()); } } else { - log(DEBUG, "No handler is found for [" + session.getUri() + "]"); - return Response.newFixedLength(Status.NOT_FOUND, CONTENT_TYPE_TEXT, "Not Found"); + log(DEBUG, "No handler is found for [" + exchange.getRequestURI().getPath() + "]"); + sendResponse(exchange, 404, CONTENT_TYPE_TEXT, "Not Found".getBytes()); } } - }; + }); + return server; } 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(); + private void reverseProxy(HttpExchange exchange, String upstream) throws URISyntaxException, IOException { + String method = exchange.getRequestMethod(); - String url = session.getQueryParameterString() == null ? upstream : upstream + "?" + session.getQueryParameterString(); + String rawQuery = exchange.getRequestURI().getRawQuery(); + String url = rawQuery == null ? upstream : upstream + "?" + rawQuery; - Map requestHeaders = new LinkedHashMap<>(session.getHeaders()); + Map> requestHeaders = new LinkedHashMap<>(exchange.getRequestHeaders()); ignoredHeaders.forEach(requestHeaders::remove); - InputStream clientIn = session.getInputStream(); + InputStream clientIn = exchange.getRequestBody(); log(DEBUG, "Reverse proxy: > " + method + " " + url + ", headers: " + requestHeaders); - HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); + HttpURLConnection conn = (HttpURLConnection) new URI(url).toURL().openConnection(); conn.setRequestMethod(method); - conn.setDoOutput(clientIn != null); - requestHeaders.forEach(conn::setRequestProperty); + conn.setDoOutput(clientIn.available() != 0); + requestHeaders.forEach((key, values) -> { + String value = String.join(",", values); + conn.setRequestProperty(key, value); + }); - if (clientIn != null) { + if (clientIn.available() != 0) { try (OutputStream upstreamOut = conn.getOutputStream()) { transfer(clientIn, upstreamOut); } @@ -210,18 +221,7 @@ private Response reverseProxy(IHTTPSession session, String upstream) throws IOEx } log(DEBUG, "Reverse proxy: < " + responseCode + " " + reponseMessage + " , headers: " + responseHeaders); - IStatus status = new IStatus() { - @Override - public int getRequestStatus() { - return responseCode; - } - - @Override - public String getDescription() { - return responseCode + " " + reponseMessage; - } - }; - + // no content long contentLength = -1; for (Entry> header : responseHeaders.entrySet()) { if ("content-length".equalsIgnoreCase(header.getKey())) { @@ -230,19 +230,12 @@ public String getDescription() { } } - Response response; - if (contentLength == -1) { - if (conn.getHeaderField("transfer-encoding") == null) { - // no content - response = Response.newFixedLength(status, null, upstreamIn, 0); - } else { - response = Response.newChunked(status, null, upstreamIn); - } - } else { - response = Response.newFixedLength(status, null, upstreamIn, contentLength); + if (contentLength == -1 && conn.getHeaderField("transfer-encoding") != null) { + // chunked encoding + contentLength = 0; } - responseHeaders.forEach((name, values) -> values.forEach(value -> response.addHeader(name, value))); + responseHeaders.forEach((name, values) -> values.forEach(value -> exchange.getResponseHeaders().add(name, value))); - return response; + sendResponse(exchange, responseCode, null, asBytes(upstreamIn), contentLength); } } diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/util/IOUtils.java b/src/main/java/xyz/zuoyx/multiyggdrasil/util/IOUtils.java index 432a4de..e9f1c95 100644 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/util/IOUtils.java +++ b/src/main/java/xyz/zuoyx/multiyggdrasil/util/IOUtils.java @@ -24,26 +24,31 @@ import java.io.UncheckedIOException; import java.net.HttpURLConnection; import java.net.Proxy; -import java.net.URL; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.LinkedHashMap; +import java.util.Map; +import com.sun.net.httpserver.HttpExchange; public final class IOUtils { public static final String CONTENT_TYPE_JSON = "application/json; charset=utf-8"; public static final String CONTENT_TYPE_TEXT = "text/plain; charset=utf-8"; + public static final String CONTENT_TYPE_IMAGE = "image/png"; - private static HttpURLConnection createConnection(String url, Proxy proxy) throws IOException { + private static HttpURLConnection createConnection(String url, Proxy proxy) throws URISyntaxException, IOException { if (proxy == null) { - return (HttpURLConnection) new URL(url).openConnection(); + return (HttpURLConnection) new URI(url).toURL().openConnection(); } else { - return (HttpURLConnection) new URL(url).openConnection(proxy); + return (HttpURLConnection) new URI(url).toURL().openConnection(proxy); } } - public static byte[] http(String method, String url) throws IOException { + public static byte[] http(String method, String url) throws URISyntaxException, IOException { return http(method, url, null); } - public static byte[] http(String method, String url, Proxy proxy) throws IOException { + public static byte[] http(String method, String url, Proxy proxy) throws URISyntaxException, IOException { HttpURLConnection conn = createConnection(url, proxy); conn.setRequestMethod(method); try (InputStream in = conn.getInputStream()) { @@ -51,11 +56,11 @@ public static byte[] http(String method, String url, Proxy proxy) throws IOExcep } } - public static byte[] http(String method, String url, byte[] payload, String contentType) throws IOException { + public static byte[] http(String method, String url, byte[] payload, String contentType) throws URISyntaxException, IOException { return http(method, url, payload, contentType, null); } - public static byte[] http(String method, String url, byte[] payload, String contentType, Proxy proxy) throws IOException { + public static byte[] http(String method, String url, byte[] payload, String contentType, Proxy proxy) throws URISyntaxException, IOException { HttpURLConnection conn = createConnection(url, proxy); conn.setRequestMethod(method); conn.setDoOutput(true); @@ -68,6 +73,44 @@ public static byte[] http(String method, String url, byte[] payload, String cont } } + public static void sendResponse(HttpExchange exchange, int status, String mimeType, byte[] data) throws IOException { + if (data != null) { + sendResponse(exchange, status, mimeType, data, data.length); + } else { + exchange.sendResponseHeaders(status, -1); + } + } + + public static void sendResponse(HttpExchange exchange, int status, String mimeType, byte[] data, long length) throws IOException { + if (mimeType != null) { + exchange.getResponseHeaders().set("Content-Type", mimeType); + } + exchange.sendResponseHeaders(status, length); + if (length != -1) { + OutputStream os = exchange.getResponseBody(); + os.write(data); + os.close(); + } + } + + public static Map parseQueryParams(String query) { + Map queryParams = new LinkedHashMap<>(); + + if (query != null) { + String[] params = query.split("&"); + for (String param : params) { + String[] keyValue = param.split("="); + if (keyValue.length == 2) { + String key = keyValue[0]; + String value = keyValue[1]; + queryParams.put(key, value); + } + } + } + + return queryParams; + } + public static byte[] asBytes(InputStream in) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); transfer(in, out); diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/yggdrasil/YggdrasilClient.java b/src/main/java/xyz/zuoyx/multiyggdrasil/yggdrasil/YggdrasilClient.java index 4565ef4..b2a57bb 100644 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/yggdrasil/YggdrasilClient.java +++ b/src/main/java/xyz/zuoyx/multiyggdrasil/yggdrasil/YggdrasilClient.java @@ -30,6 +30,7 @@ import java.io.IOException; import java.io.UncheckedIOException; import java.net.Proxy; +import java.net.URISyntaxException; import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; @@ -60,6 +61,8 @@ public Map queryUUIDs(Set names) throws UncheckedIOExcepti responseText = asString(http("POST", apiProvider.queryUUIDsByNames(), JsonUtils.toJsonString(names).getBytes(UTF_8), CONTENT_TYPE_JSON, proxy)); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Invalid URL '" + apiProvider.queryUUIDsByNames() + "'"); } catch (IOException e) { throw new UncheckedIOException(e); } @@ -87,6 +90,8 @@ public Optional queryProfile(UUID uuid, boolean withSignature) thro String responseText; try { responseText = asString(http("GET", url, proxy)); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Invalid URL '" + url + "'"); } catch (IOException e) { throw new UncheckedIOException(e); } @@ -103,6 +108,8 @@ public Optional hasJoinedServer(String username, String serverId, S String responseText; try { responseText = asString(http("GET", apiProvider.hasJoinedServer(username, serverId, ip) , proxy)); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Invalid URL '" + apiProvider.hasJoinedServer(username, serverId, ip) + "'"); } catch (IOException e) { throw new UncheckedIOException(e); } From b48dc7f13314d265eb1064d8ecb3feff63f58149 Mon Sep 17 00:00:00 2001 From: Ethan Zuo Date: Mon, 26 Jun 2023 18:55:23 +0800 Subject: [PATCH 3/9] update dependencies and code cleanup --- README.en.md | 2 +- README.md | 2 +- build.gradle | 14 ++++++++------ .../support/YggdrasilKeyTransformUnit.java | 5 ++++- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/README.en.md b/README.en.md index 93ef671..50ca1e9 100644 --- a/README.en.md +++ b/README.en.md @@ -3,7 +3,7 @@ # MultiYggdrasil [![latest release](https://img.shields.io/github/v/tag/YuxuanZuo/MultiYggdrasil?color=yellow&include_prereleases&label=version&sort=semver&style=flat-square)](https://github.com/YuxuanZuo/MultiYggdrasil/releases) -[![ci status](https://img.shields.io/github/workflow/status/YuxuanZuo/MultiYggdrasil/CI?style=flat-square)](https://github.com/YuxuanZuo/MultiYggdrasil/actions?query=workflow%3ACI) +[![ci status](https://img.shields.io/github/actions/workflow/status/YuxuanZuo/MultiYggdrasil/ci.yml?branch=develop)](https://github.com/YuxuanZuo/MultiYggdrasil/actions?query=workflow%3ACI) [![license agpl-3.0](https://img.shields.io/badge/license-AGPL--3.0-blue.svg?style=flat-square)](https://github.com/YuxuanZuo/MultiYggdrasil/blob/develop/LICENSE) A fork of authlib-injector with support for coexist with the Mojang authentication server. diff --git a/README.md b/README.md index e0919f3..4ec8159 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ # MultiYggdrasil [![latest release](https://img.shields.io/github/v/tag/YuxuanZuo/MultiYggdrasil?color=yellow&include_prereleases&label=version&sort=semver&style=flat-square)](https://github.com/YuxuanZuo/MultiYggdrasil/releases) -[![ci status](https://img.shields.io/github/workflow/status/YuxuanZuo/MultiYggdrasil/CI?style=flat-square)](https://github.com/YuxuanZuo/MultiYggdrasil/actions?query=workflow%3ACI) +[![ci status](https://img.shields.io/github/actions/workflow/status/YuxuanZuo/MultiYggdrasil/ci.yml?branch=develop)](https://github.com/YuxuanZuo/MultiYggdrasil/actions?query=workflow%3ACI) [![license agpl-3.0](https://img.shields.io/badge/license-AGPL--3.0-blue.svg?style=flat-square)](https://github.com/YuxuanZuo/MultiYggdrasil/blob/develop/LICENSE) 一个 [authlib-injector](https://github.com/yushijinhun/authlib-injector) 的分支, 添加了与 Mojang 验证服务器共存的支持. diff --git a/build.gradle b/build.gradle index 920707b..5707e84 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { - id 'com.github.johnrengelman.shadow' version '7.1.2' - id 'com.palantir.git-version' version '0.15.0' + id 'com.github.johnrengelman.shadow' version '8.1.1' + id 'com.palantir.git-version' version '3.0.0' id 'java' } @@ -9,16 +9,18 @@ repositories { } dependencies { - implementation 'com.google.code.gson:gson:2.9.0' - implementation 'org.ow2.asm:asm:9.3' - testImplementation 'org.junit.jupiter:junit-jupiter:5.8.2' + implementation 'com.google.code.gson:gson:2.10.1' + implementation 'org.ow2.asm:asm:9.5' + testImplementation 'org.junit.jupiter:junit-jupiter:5.9.3' } test { useJUnitPlatform() } -sourceCompatibility = 17 +compileJava { + options.release = 17 +} def buildNumber = System.getenv('AI_BUILD_NUMBER') def gitInfo = versionDetails() diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/transform/support/YggdrasilKeyTransformUnit.java b/src/main/java/xyz/zuoyx/multiyggdrasil/transform/support/YggdrasilKeyTransformUnit.java index c473836..c31e36f 100644 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/transform/support/YggdrasilKeyTransformUnit.java +++ b/src/main/java/xyz/zuoyx/multiyggdrasil/transform/support/YggdrasilKeyTransformUnit.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 Haowei Wen and contributors + * Copyright (C) 2023 Haowei Wen and contributors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -104,7 +104,9 @@ protected byte[] engineSign() { throw new UnsupportedOperationException(); } + @Override + @Deprecated protected void engineSetParameter(String param, Object value) { } @@ -119,6 +121,7 @@ protected void engineInitSign(PrivateKey privateKey) { } @Override + @Deprecated protected Object engineGetParameter(String param) { return null; } From 3bbf2f7f1aca90388bad2de82aed781bb65c5b92 Mon Sep 17 00:00:00 2001 From: Ethan Zuo Date: Sat, 1 Jul 2023 19:55:25 +0800 Subject: [PATCH 4/9] Update README --- README.en.md | 9 ++++++--- README.md | 8 +++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/README.en.md b/README.en.md index 50ca1e9..8055037 100644 --- a/README.en.md +++ b/README.en.md @@ -27,6 +27,9 @@ Configure Minecraft server with the following JVM parameter: ``` -javaagent:{/path/to/MultiYggdrasil.jar}={Authentication Server URL} ``` +Note: Unless the custom authentication server supports Mojang authentication server coexistence, this feature will not +be enabled by default. You need to enable it by adding specific JVM parameters. Please refer to the [Options](README.en.md#options) +section for more details. ## Options ``` @@ -122,9 +125,9 @@ Configure Minecraft server with the following JVM parameter: In order to distinguish the username of players from the custom authentication server from that of the Mojang server, the player who from the custom authentication server will add a namespace suffix to their username. For example: - Notch.custom + Notch.cust If the option "-Dmultiyggdrasil.namespace" is not set and the field "namespace" is not sent by authentication - server, the server will issue a default namespace called "custom". If any fields were sent, the server will use the + server, the server will issue a default namespace called "cust". If any fields were sent, the server will use the namespace that you defined earlier. Some features that conflict with Mojang Yggdrasil server will no longer available anymore: @@ -135,7 +138,7 @@ Configure Minecraft server with the following JVM parameter: (The default is to give priority to verification of the genuine player). -Dmultiyggdrasil.namespace={namespace string} - Set the namespace used by the feature "Mojang authentication server". Allowed characters are a-z0-9._- . + Set the namespace used by the feature "Mojang authentication server". Allowed characters are a-z0-9_- . -Dmultiyggdrasil.noNamespaceSuffix Do not add namespace suffix to the username. diff --git a/README.md b/README.md index 4ec8159..e307631 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ gradle ``` -javaagent:{MultiYggdrasil.jar 的路径}={验证服务器 URL (API 地址)} ``` +注意: 除非验证服务器支持, 否则默认情况下不会启用 Mojang 验证服务器共存功能, 您需要通过添加特定 JVM 参数来启用这项功能. +详情请参阅[参数](README.md#参数)小节. ## 参数 ``` @@ -125,8 +127,8 @@ gradle 为了将自定义验证服务器角色与正版角色的用户名区别开, 前者的用户名将被添加命名空间后缀. 例如: - Notch.custom - 若未设置 -Dmultiyggdrasil.namespace 参数且验证服务器未设置 namespace 字段, 将使用默认命名空间 custom, 否则使用定义的命名空间. + Notch.cust + 若未设置 -Dmultiyggdrasil.namespace 参数且验证服务器未设置 namespace 字段, 将使用默认命名空间 cust, 否则使用定义的命名空间. 以下与 Mojang 验证服务器冲突的功能将不可用: - Mojang 命名空间 @@ -135,7 +137,7 @@ gradle 在登录游戏服务器时优先验证来自自定义验证服务器的角色(默认为优先验证正版角色). -Dmultiyggdrasil.namespace={命名空间字符串} - 设置 Mojang 验证服务器 功能使用的命名空间, 允许的字符为 a-z0-9._- . + 设置 Mojang 验证服务器 功能使用的命名空间, 允许的字符为 a-z0-9_- . -Dmultiyggdrasil.noNamespaceSuffix 不要在用户名中添加命名空间后缀. From f12e83eee3afedcb621b7677793197ea3881eb29 Mon Sep 17 00:00:00 2001 From: Ethan Zuo Date: Sat, 23 Sep 2023 01:12:19 +0800 Subject: [PATCH 5/9] Cleaning up & optimizing --- .../zuoyx/multiyggdrasil/MultiYggdrasil.java | 2 +- .../httpd/AntiFeaturesFilter.java | 9 +-- .../httpd/LegacySkinAPIFilter.java | 12 +-- .../httpd/MultiHasJoinedServerFilter.java | 6 +- .../httpd/MultiQueryProfileFilter.java | 10 +-- .../httpd/MultiQueryUUIDsFilter.java | 6 +- .../httpd/ProfileKeyFilter.java | 7 +- .../httpd/PublickeysFilter.java | 7 +- .../httpd/QueryProfileFilter.java | 10 +-- .../httpd/QueryUUIDsFilter.java | 6 +- .../zuoyx/multiyggdrasil/httpd/URLFilter.java | 7 +- .../multiyggdrasil/httpd/URLProcessor.java | 74 +++++++++---------- ...ungeeCordAllowedCharactersTransformer.java | 2 +- .../support/CitizensTransformer.java | 2 +- .../support/HasJoinedServerTransformer.java | 6 +- .../support/SkinWhitelistTransformUnit.java | 2 +- .../util/UnsupportedURLException.java | 40 ++++++++++ .../yggdrasil/YggdrasilClient.java | 12 +-- 18 files changed, 126 insertions(+), 94 deletions(-) create mode 100644 src/main/java/xyz/zuoyx/multiyggdrasil/util/UnsupportedURLException.java diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/MultiYggdrasil.java b/src/main/java/xyz/zuoyx/multiyggdrasil/MultiYggdrasil.java index 6875851..1ad1d1f 100644 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/MultiYggdrasil.java +++ b/src/main/java/xyz/zuoyx/multiyggdrasil/MultiYggdrasil.java @@ -158,7 +158,7 @@ private static APIMetadata fetchAPIMetadata(String apiUrl) { try (InputStream in = connection.getInputStream()) { while (in.read() != -1) ; - } catch (IOException e) { + } catch (IOException ignored) { } log(INFO, "Redirect to: " + absoluteAli); diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/AntiFeaturesFilter.java b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/AntiFeaturesFilter.java index bb13e26..545bd7b 100644 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/AntiFeaturesFilter.java +++ b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/AntiFeaturesFilter.java @@ -20,6 +20,7 @@ import static xyz.zuoyx.multiyggdrasil.util.IOUtils.sendResponse; import java.io.IOException; import com.sun.net.httpserver.HttpExchange; +import xyz.zuoyx.multiyggdrasil.util.UnsupportedURLException; /** * Disables Mojang's anti-features. @@ -36,21 +37,17 @@ public boolean canHandle(String domain) { } @Override - public boolean handle(String domain, String path, HttpExchange exchange) throws IOException { + public void handle(String domain, String path, HttpExchange exchange) throws UnsupportedURLException, IOException { if (domain.equals("api.minecraftservices.com") && path.equals("/privileges") && exchange.getRequestMethod().equals("GET")) { sendResponse(exchange, 200, CONTENT_TYPE_JSON, RESPONSE_PRIVILEGES.getBytes()); - return true; } else if (domain.equals("api.minecraftservices.com") && path.equals("/player/attributes") && exchange.getRequestMethod().equals("GET")) { sendResponse(exchange, 200, CONTENT_TYPE_JSON, RESPONSE_PLAYER_ATTRIBUTES.getBytes()); - return true; } else if (domain.equals("api.minecraftservices.com") && path.equals("/privacy/blocklist") && exchange.getRequestMethod().equals("GET")) { sendResponse(exchange, 200, CONTENT_TYPE_JSON, RESPONSE_PRIVACY_BLOCKLIST.getBytes()); - return true; } else if (domain.equals("sessionserver.mojang.com") && path.equals("/blockedservers") && exchange.getRequestMethod().equals("GET")) { sendResponse(exchange, 404, null, null); - return true; } else { - return false; + throw new UnsupportedURLException(); } } } diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/LegacySkinAPIFilter.java b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/LegacySkinAPIFilter.java index a34d106..f483e4e 100644 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/LegacySkinAPIFilter.java +++ b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/LegacySkinAPIFilter.java @@ -38,6 +38,7 @@ import com.google.gson.JsonObject; import com.sun.net.httpserver.HttpExchange; import xyz.zuoyx.multiyggdrasil.util.JsonUtils; +import xyz.zuoyx.multiyggdrasil.util.UnsupportedURLException; import xyz.zuoyx.multiyggdrasil.yggdrasil.YggdrasilClient; public class LegacySkinAPIFilter implements URLFilter { @@ -56,12 +57,12 @@ public boolean canHandle(String domain) { } @Override - public boolean handle(String domain, String path, HttpExchange exchange) throws IOException { + public void handle(String domain, String path, HttpExchange exchange) throws UnsupportedURLException, IOException { if (!domain.equals("skins.minecraft.net")) - return false; + throw new UnsupportedURLException(); Matcher matcher = PATH_SKINS.matcher(path); if (!matcher.find()) - return false; + throw new UnsupportedURLException(); String username = matcher.group("username"); // Minecraft does not encode non-ASCII characters in URLs @@ -85,7 +86,9 @@ public boolean handle(String domain, String path, HttpExchange exchange) throws byte[] data; try { data = http("GET", url); - } catch (URISyntaxException | IOException e) { + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Invalid URL [" + url + "]"); + } catch (IOException e) { throw newUncheckedIOException("Failed to retrieve skin from " + url, e); } log(INFO, "Retrieved skin for " + username + " from " + url + ", " + data.length + " bytes"); @@ -95,7 +98,6 @@ public boolean handle(String domain, String path, HttpExchange exchange) throws log(INFO, "No skin is found for " + username); sendResponse(exchange, 404, null, null); } - return true; } private Optional obtainTextureUrl(String texturesPayload, String textureType) throws UncheckedIOException { diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/MultiHasJoinedServerFilter.java b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/MultiHasJoinedServerFilter.java index 71ce703..d8424ad 100644 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/MultiHasJoinedServerFilter.java +++ b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/MultiHasJoinedServerFilter.java @@ -28,6 +28,7 @@ import java.util.Optional; import com.sun.net.httpserver.HttpExchange; +import xyz.zuoyx.multiyggdrasil.util.UnsupportedURLException; import xyz.zuoyx.multiyggdrasil.yggdrasil.GameProfile; import xyz.zuoyx.multiyggdrasil.yggdrasil.NamespacedID; import xyz.zuoyx.multiyggdrasil.yggdrasil.YggdrasilAPIProvider; @@ -55,7 +56,7 @@ public boolean canHandle(String domain) { } @Override - public boolean handle(String domain, String path, HttpExchange exchange) throws IOException { + public void handle(String domain, String path, HttpExchange exchange) throws UnsupportedURLException, IOException { if (domain.equals("sessionserver.mojang.com") && path.equals("/session/minecraft/hasJoined") && exchange.getRequestMethod().equals("GET")) { Map params = parseQueryParams(exchange.getRequestURI().getQuery()); @@ -82,9 +83,8 @@ public boolean handle(String domain, String path, HttpExchange exchange) throws } else { sendResponse(exchange, 204, null, null); } - return true; } else { - return false; + throw new UnsupportedURLException(); } } } diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/MultiQueryProfileFilter.java b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/MultiQueryProfileFilter.java index ae0ae3a..8206c18 100644 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/MultiQueryProfileFilter.java +++ b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/MultiQueryProfileFilter.java @@ -28,6 +28,7 @@ import java.util.regex.Pattern; import com.sun.net.httpserver.HttpExchange; +import xyz.zuoyx.multiyggdrasil.util.UnsupportedURLException; import xyz.zuoyx.multiyggdrasil.yggdrasil.GameProfile; import xyz.zuoyx.multiyggdrasil.yggdrasil.NamespacedID; import xyz.zuoyx.multiyggdrasil.yggdrasil.YggdrasilClient; @@ -53,19 +54,19 @@ public boolean canHandle(String domain) { } @Override - public boolean handle(String domain, String path, HttpExchange exchange) throws IOException { + public void handle(String domain, String path, HttpExchange exchange) throws UnsupportedURLException, IOException { if (!domain.equals("sessionserver.mojang.com")) - return false; + throw new UnsupportedURLException(); Matcher matcher = PATH_REGEX.matcher(path); if (!matcher.find()) - return false; + throw new UnsupportedURLException(); UUID uuid; try { uuid = fromUnsignedUUID(matcher.group("uuid")); } catch (IllegalArgumentException e) { sendResponse(exchange, 204, null, null); - return true; + return; } boolean withSignature = false; @@ -87,7 +88,6 @@ public boolean handle(String domain, String path, HttpExchange exchange) throws } else { sendResponse(exchange, 204, null, null); } - return true; } } diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/MultiQueryUUIDsFilter.java b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/MultiQueryUUIDsFilter.java index bf26d57..313fbee 100644 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/MultiQueryUUIDsFilter.java +++ b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/MultiQueryUUIDsFilter.java @@ -29,6 +29,7 @@ import java.util.Set; import java.util.UUID; import com.sun.net.httpserver.HttpExchange; +import xyz.zuoyx.multiyggdrasil.util.UnsupportedURLException; import xyz.zuoyx.multiyggdrasil.yggdrasil.NamespacedID; import xyz.zuoyx.multiyggdrasil.yggdrasil.YggdrasilClient; import xyz.zuoyx.multiyggdrasil.yggdrasil.YggdrasilResponseBuilder; @@ -51,15 +52,14 @@ public boolean canHandle(String domain) { } @Override - public boolean handle(String domain, String path, HttpExchange exchange) throws IOException { + public void handle(String domain, String path, HttpExchange exchange) throws UnsupportedURLException, IOException { if (domain.equals("api.mojang.com") && path.equals("/profiles/minecraft") && exchange.getRequestMethod().equals("POST")) { Set request = new LinkedHashSet<>(); parseJson(asString(asBytes(exchange.getRequestBody()))).getAsJsonArray() .forEach(element -> request.add(asJsonString(element))); sendResponse(exchange, 200, CONTENT_TYPE_JSON, YggdrasilResponseBuilder.queryUUIDs(performQuery(request)).getBytes()); - return true; } else { - return false; + throw new UnsupportedURLException(); } } diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/ProfileKeyFilter.java b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/ProfileKeyFilter.java index e7c692e..38f2f29 100644 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/ProfileKeyFilter.java +++ b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/ProfileKeyFilter.java @@ -30,6 +30,7 @@ import java.util.Base64; import com.sun.net.httpserver.HttpExchange; import com.google.gson.JsonObject; +import xyz.zuoyx.multiyggdrasil.util.UnsupportedURLException; /** * Intercepts Minecraft's request to ..., @@ -43,12 +44,12 @@ public boolean canHandle(String domain) { } @Override - public boolean handle(String domain, String path, HttpExchange exchange) throws IOException { + public void handle(String domain, String path, HttpExchange exchange) throws UnsupportedURLException, IOException { if (domain.equals("api.minecraftservices.com") && path.equals("/player/certificates") && exchange.getRequestMethod().equals("POST")) { sendResponse(exchange, 200, CONTENT_TYPE_JSON, toJsonString(makeDummyResponse()).getBytes()); - return true; + } else { + throw new UnsupportedURLException(); } - return false; } private JsonObject makeDummyResponse() { diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/PublickeysFilter.java b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/PublickeysFilter.java index ae9ca76..7d8adec 100644 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/PublickeysFilter.java +++ b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/PublickeysFilter.java @@ -27,6 +27,7 @@ import com.google.gson.JsonArray; import com.google.gson.JsonObject; import xyz.zuoyx.multiyggdrasil.transform.support.YggdrasilKeyTransformUnit; +import xyz.zuoyx.multiyggdrasil.util.UnsupportedURLException; public class PublickeysFilter implements URLFilter { @@ -36,12 +37,12 @@ public boolean canHandle(String domain) { } @Override - public boolean handle(String domain, String path, HttpExchange exchange) throws IOException { + public void handle(String domain, String path, HttpExchange exchange) throws UnsupportedURLException, IOException { if (domain.equals("api.minecraftservices.com") && path.equals("/publickeys") && exchange.getRequestMethod().equals("GET")) { sendResponse(exchange, 200, CONTENT_TYPE_JSON, toJsonString(makePublickeysResponse()).getBytes()); - return true; + } else { + throw new UnsupportedURLException(); } - return false; } private JsonObject makePublickeysResponse() { diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/QueryProfileFilter.java b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/QueryProfileFilter.java index 828c6e7..c1b5b01 100644 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/QueryProfileFilter.java +++ b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/QueryProfileFilter.java @@ -28,6 +28,7 @@ import java.util.regex.Pattern; import com.sun.net.httpserver.HttpExchange; +import xyz.zuoyx.multiyggdrasil.util.UnsupportedURLException; import xyz.zuoyx.multiyggdrasil.yggdrasil.GameProfile; import xyz.zuoyx.multiyggdrasil.yggdrasil.YggdrasilClient; import xyz.zuoyx.multiyggdrasil.yggdrasil.YggdrasilResponseBuilder; @@ -50,19 +51,19 @@ public boolean canHandle(String domain) { } @Override - public boolean handle(String domain, String path, HttpExchange exchange) throws IOException { + public void handle(String domain, String path, HttpExchange exchange) throws UnsupportedURLException, IOException { if (!domain.equals("sessionserver.mojang.com")) - return false; + throw new UnsupportedURLException(); Matcher matcher = PATH_REGEX.matcher(path); if (!matcher.find()) - return false; + throw new UnsupportedURLException(); UUID uuid; try { uuid = fromUnsignedUUID(matcher.group("uuid")); } catch (IllegalArgumentException e) { sendResponse(exchange, 204, null, null); - return true; + return; } boolean withSignature = false; @@ -87,7 +88,6 @@ public boolean handle(String domain, String path, HttpExchange exchange) throws } else { sendResponse(exchange, 204, null, null); } - return true; } } diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/QueryUUIDsFilter.java b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/QueryUUIDsFilter.java index 7c166d9..04686bc 100644 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/QueryUUIDsFilter.java +++ b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/QueryUUIDsFilter.java @@ -31,6 +31,7 @@ import java.util.Set; import java.util.UUID; import com.sun.net.httpserver.HttpExchange; +import xyz.zuoyx.multiyggdrasil.util.UnsupportedURLException; import xyz.zuoyx.multiyggdrasil.yggdrasil.YggdrasilClient; import xyz.zuoyx.multiyggdrasil.yggdrasil.YggdrasilResponseBuilder; @@ -50,15 +51,14 @@ public boolean canHandle(String domain) { } @Override - public boolean handle(String domain, String path, HttpExchange exchange) throws IOException { + public void handle(String domain, String path, HttpExchange exchange) throws UnsupportedURLException, IOException { if (domain.equals("api.mojang.com") && path.equals("/profiles/minecraft") && exchange.getRequestMethod().equals("POST")) { Set request = new LinkedHashSet<>(); parseJson(asString(asBytes(exchange.getRequestBody()))).getAsJsonArray() .forEach(element -> request.add(asJsonString(element))); sendResponse(exchange, 200, CONTENT_TYPE_JSON, YggdrasilResponseBuilder.queryUUIDs(performQuery(request)).getBytes()); - return true; } else { - return false; + throw new UnsupportedURLException(); } } diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/URLFilter.java b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/URLFilter.java index ddb249e..417b1b6 100644 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/URLFilter.java +++ b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/URLFilter.java @@ -19,6 +19,7 @@ import java.io.IOException; import com.sun.net.httpserver.HttpExchange; +import xyz.zuoyx.multiyggdrasil.util.UnsupportedURLException; /** * A URLFilter filters the URLs in the bytecode, and intercepts those it is interested in. @@ -27,13 +28,13 @@ public interface URLFilter { /** * Returns true if the filter MAY be interested in the given domain. - * + *

* If this method returns true, the domain will be intercepted. * And when a request is sent to this domain, handle() will be invoked. - * If it turns out that the filter doesn't really want to intercept the URL (handle() returns empty), + * If it turns out that the filter doesn't really want to intercept the URL (handle() throws UnsupportedURLException), * the request will be reverse-proxied to the original URL, as if nothing has happened. */ boolean canHandle(String domain); - boolean handle(String domain, String path, HttpExchange exchange) throws IOException; + void handle(String domain, String path, HttpExchange exchange) throws UnsupportedURLException, IOException; } diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/URLProcessor.java b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/URLProcessor.java index 3927f43..144d70f 100644 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/URLProcessor.java +++ b/src/main/java/xyz/zuoyx/multiyggdrasil/httpd/URLProcessor.java @@ -43,9 +43,9 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import com.sun.net.httpserver.HttpExchange; -import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; import xyz.zuoyx.multiyggdrasil.Config; +import xyz.zuoyx.multiyggdrasil.util.UnsupportedURLException; public class URLProcessor { @@ -62,7 +62,7 @@ public URLProcessor(List filters, URLRedirector 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. @@ -128,49 +128,41 @@ private int getLocalApiPort() { private HttpServer createHttpServer() throws IOException { HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", Config.httpdPort), 0); - server.createContext("/", new HttpHandler() { - @Override - public void handle(HttpExchange exchange) throws IOException { - if (exchange.getRequestURI().getPath().startsWith("/debug/")) { - debugApi.serve(exchange); - return; - } - - Matcher matcher = LOCAL_URL_REGEX.matcher(exchange.getRequestURI().getPath()); - if (matcher.find()) { - String protocol = matcher.group("protocol"); - String domain = matcher.group("domain"); - String path = matcher.group("path"); - for (URLFilter filter : filters) { - if (filter.canHandle(domain)) { - boolean result; - try { - result = filter.handle(domain, path, exchange); - } catch (Throwable e) { - log(WARNING, "An error occurred while processing request [" + exchange.getRequestURI().getPath() + "]", e); - sendResponse(exchange, 500, CONTENT_TYPE_TEXT, "Internal Server Error".getBytes()); - return; - } - - if (result) { - log(DEBUG, "Request to [" + exchange.getRequestURI().getPath() + "] is handled by [" + filter + "]"); - return; - } + server.createContext("/debug/", exchange -> debugApi.serve(exchange)); + server.createContext("/", exchange -> { + Matcher matcher = LOCAL_URL_REGEX.matcher(exchange.getRequestURI().getPath()); + if (matcher.find()) { + String protocol = matcher.group("protocol"); + String domain = matcher.group("domain"); + String path = matcher.group("path"); + for (URLFilter filter : filters) { + if (filter.canHandle(domain)) { + try { + filter.handle(domain, path, exchange); + } catch (UnsupportedURLException e) { + continue; + } catch (Throwable e) { + log(WARNING, "An error occurred while processing request [" + exchange.getRequestURI().getPath() + "]", e); + sendResponse(exchange, 500, CONTENT_TYPE_TEXT, "Internal Server Error".getBytes()); + return; } - } - String target = redirector.redirect(domain, path) - .orElseGet(() -> protocol + "://" + domain + path); - try { - reverseProxy(exchange, target); - } catch (URISyntaxException | IOException e) { - log(WARNING, "Reverse proxy error", e); - sendResponse(exchange, 502, CONTENT_TYPE_TEXT, "Bad Gateway".getBytes()); + log(DEBUG, "Request to [" + exchange.getRequestURI().getPath() + "] is handled by [" + filter + "]"); + return; } - } else { - log(DEBUG, "No handler is found for [" + exchange.getRequestURI().getPath() + "]"); - sendResponse(exchange, 404, CONTENT_TYPE_TEXT, "Not Found".getBytes()); } + + String target = redirector.redirect(domain, path) + .orElseGet(() -> protocol + "://" + domain + path); + try { + reverseProxy(exchange, target); + } catch (URISyntaxException | IOException e) { + log(WARNING, "Reverse proxy error", e); + sendResponse(exchange, 502, CONTENT_TYPE_TEXT, "Bad Gateway".getBytes()); + } + } else { + log(DEBUG, "No handler is found for [" + exchange.getRequestURI().getPath() + "]"); + sendResponse(exchange, 404, CONTENT_TYPE_TEXT, "Not Found".getBytes()); } }); return server; diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/transform/support/BungeeCordAllowedCharactersTransformer.java b/src/main/java/xyz/zuoyx/multiyggdrasil/transform/support/BungeeCordAllowedCharactersTransformer.java index f564d9f..2d38fd5 100644 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/transform/support/BungeeCordAllowedCharactersTransformer.java +++ b/src/main/java/xyz/zuoyx/multiyggdrasil/transform/support/BungeeCordAllowedCharactersTransformer.java @@ -26,7 +26,7 @@ /** * Hacks BungeeCord to allow special characters to occur in the username. - * + *

* Since ..., * BungeeCord allows only certain characters to occur in the username when online-mode is on. */ diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/transform/support/CitizensTransformer.java b/src/main/java/xyz/zuoyx/multiyggdrasil/transform/support/CitizensTransformer.java index 135a4c5..e23ca73 100644 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/transform/support/CitizensTransformer.java +++ b/src/main/java/xyz/zuoyx/multiyggdrasil/transform/support/CitizensTransformer.java @@ -35,7 +35,7 @@ /** * 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 MultiYggdrasil's url. diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/transform/support/HasJoinedServerTransformer.java b/src/main/java/xyz/zuoyx/multiyggdrasil/transform/support/HasJoinedServerTransformer.java index 8e0ae9d..35b5b3b 100644 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/transform/support/HasJoinedServerTransformer.java +++ b/src/main/java/xyz/zuoyx/multiyggdrasil/transform/support/HasJoinedServerTransformer.java @@ -28,10 +28,8 @@ import xyz.zuoyx.multiyggdrasil.transform.TransformUnit; /** - * Hack authlib to create game profile with username in hasJoined response. - * - * Generally, server will create game profile with username sent by client. - * This transformer changed this behavior. + * By default, Minecraft uses the username sent by the client to create a game profile. + * This transformer will make Minecraft use the username from the hasJoined response to create the game profile. */ public class HasJoinedServerTransformer implements TransformUnit { diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/transform/support/SkinWhitelistTransformUnit.java b/src/main/java/xyz/zuoyx/multiyggdrasil/transform/support/SkinWhitelistTransformUnit.java index 9a455b6..f105f10 100644 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/transform/support/SkinWhitelistTransformUnit.java +++ b/src/main/java/xyz/zuoyx/multiyggdrasil/transform/support/SkinWhitelistTransformUnit.java @@ -68,7 +68,7 @@ public static boolean isWhitelistedDomain(String url) { try { domain = new URI(url).getHost(); } catch (URISyntaxException e) { - throw new IllegalArgumentException("Invalid URL '" + url + "'"); + throw new IllegalArgumentException("Invalid URL [" + url + "]"); } for (String pattern : DEFAULT_BLACKLISTED_DOMAINS) { diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/util/UnsupportedURLException.java b/src/main/java/xyz/zuoyx/multiyggdrasil/util/UnsupportedURLException.java new file mode 100644 index 0000000..bc2f756 --- /dev/null +++ b/src/main/java/xyz/zuoyx/multiyggdrasil/util/UnsupportedURLException.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2023 Ethan Zuo + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package xyz.zuoyx.multiyggdrasil.util; + +import java.io.Serial; + +public class UnsupportedURLException extends Exception { + @Serial + private static final long serialVersionUID = 7895188952767140345L; + + public UnsupportedURLException() { + this(null, null); + } + + public UnsupportedURLException(String message) { + this(message, null); + } + + public UnsupportedURLException(String message, Throwable cause) { + super(message, cause, false, false); + } + + public UnsupportedURLException(Throwable cause) { + this(null, cause); + } +} diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/yggdrasil/YggdrasilClient.java b/src/main/java/xyz/zuoyx/multiyggdrasil/yggdrasil/YggdrasilClient.java index b2a57bb..255011a 100644 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/yggdrasil/YggdrasilClient.java +++ b/src/main/java/xyz/zuoyx/multiyggdrasil/yggdrasil/YggdrasilClient.java @@ -62,9 +62,9 @@ public Map queryUUIDs(Set names) throws UncheckedIOExcepti JsonUtils.toJsonString(names).getBytes(UTF_8), CONTENT_TYPE_JSON, proxy)); } catch (URISyntaxException e) { - throw new IllegalArgumentException("Invalid URL '" + apiProvider.queryUUIDsByNames() + "'"); + throw new IllegalArgumentException("Invalid URL [" + apiProvider.queryUUIDsByNames() + "]"); } catch (IOException e) { - throw new UncheckedIOException(e); + throw newUncheckedIOException("Failed to request URL [" + apiProvider.queryUUIDsByNames() + "]", e); } log(DEBUG, "Query UUIDs of " + names + " at [" + apiProvider + "], response: " + responseText); @@ -91,9 +91,9 @@ public Optional queryProfile(UUID uuid, boolean withSignature) thro try { responseText = asString(http("GET", url, proxy)); } catch (URISyntaxException e) { - throw new IllegalArgumentException("Invalid URL '" + url + "'"); + throw new IllegalArgumentException("Invalid URL [" + url + "]"); } catch (IOException e) { - throw new UncheckedIOException(e); + throw newUncheckedIOException("Failed to request URL [" + url + "]", e); } if (responseText.isEmpty()) { log(DEBUG, "Query profile of [" + uuid + "] at [" + apiProvider + "], not found"); @@ -109,9 +109,9 @@ public Optional hasJoinedServer(String username, String serverId, S try { responseText = asString(http("GET", apiProvider.hasJoinedServer(username, serverId, ip) , proxy)); } catch (URISyntaxException e) { - throw new IllegalArgumentException("Invalid URL '" + apiProvider.hasJoinedServer(username, serverId, ip) + "'"); + throw new IllegalArgumentException("Invalid URL [" + apiProvider.hasJoinedServer(username, serverId, ip) + "]"); } catch (IOException e) { - throw new UncheckedIOException(e); + throw newUncheckedIOException("Failed to request URL [" + apiProvider.hasJoinedServer(username, serverId, ip) + "]", e); } if (responseText.isEmpty()) { log(DEBUG, "Username [" + username + "] is not authenticated at [" + apiProvider + "] Yggdrasil server"); From b89c7d838501fc20906ea0d14e3f7e03e9268546 Mon Sep 17 00:00:00 2001 From: Evan Goode Date: Sun, 24 Sep 2023 05:51:33 +0800 Subject: [PATCH 6/9] Include release_time in release JSON manifest For https://github.com/yushijinhun/authlib-injector/issues/219 --- .github/workflows/deploy_release.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/deploy_release.yml b/.github/workflows/deploy_release.yml index d5ed345..89c1e2c 100644 --- a/.github/workflows/deploy_release.yml +++ b/.github/workflows/deploy_release.yml @@ -38,6 +38,7 @@ jobs: build_number=$(grep -Pom1 '@@release\.build_number=\K.*(?=@@)' <<< $release_body) version_number=$(grep -Pom1 '@@release\.version_number=\K.*(?=@@)' <<< $release_body) asset_name='${{ github.event.release.assets[0].name }}' + release_published_at='${{ github.event.release.published_at }}' cd ~/deploy git config --local user.name "github-actions[bot]" @@ -49,12 +50,14 @@ jobs: jq -n \ --arg build_number "$build_number" \ --arg version "$version_number" \ + --arg release_time "$release_published_at" \ --arg download_url "https://multiyggdrasil.zuoyx.xyz/artifact/$build_number/$asset_name" \ --arg sha256 "$sha256" \ ' { "build_number": $build_number|tonumber, "version": $version, + "release_time": $release_time, "download_url": $download_url, "checksums": { "sha256": $sha256 From 2bee3223ec276d4ed6bff664a926bf323265b0d3 Mon Sep 17 00:00:00 2001 From: Haowei Wen Date: Fri, 17 Nov 2023 23:43:36 +0800 Subject: [PATCH 7/9] Fix skin not displayed in Minecraft 1.20.2 --- .../support/YggdrasilKeyTransformUnit.java | 39 ++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/transform/support/YggdrasilKeyTransformUnit.java b/src/main/java/xyz/zuoyx/multiyggdrasil/transform/support/YggdrasilKeyTransformUnit.java index c31e36f..1894f7f 100644 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/transform/support/YggdrasilKeyTransformUnit.java +++ b/src/main/java/xyz/zuoyx/multiyggdrasil/transform/support/YggdrasilKeyTransformUnit.java @@ -16,16 +16,17 @@ */ package xyz.zuoyx.multiyggdrasil.transform.support; +import static java.lang.invoke.MethodHandles.publicLookup; +import static java.lang.invoke.MethodType.methodType; import static xyz.zuoyx.multiyggdrasil.util.IOUtils.asBytes; import static xyz.zuoyx.multiyggdrasil.util.Logging.Level.DEBUG; import static org.objectweb.asm.Opcodes.ALOAD; import static org.objectweb.asm.Opcodes.ARETURN; import static org.objectweb.asm.Opcodes.ASM9; -import static org.objectweb.asm.Opcodes.GETFIELD; -import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL; import static org.objectweb.asm.Opcodes.IRETURN; import java.io.IOException; import java.io.InputStream; +import java.lang.invoke.MethodHandle; import java.security.GeneralSecurityException; import java.security.InvalidKeyException; import java.security.PrivateKey; @@ -61,7 +62,32 @@ private static PublicKey loadMojangPublicKey() { } @CallbackMethod - public static boolean verifyPropertySignature(String propertyValue, String base64Signature) { + public static boolean verifyPropertySignature(Object propertyObj) { + String base64Signature; + String propertyValue; + + try { + MethodHandle valueHandle; + try { + valueHandle = publicLookup().findVirtual(propertyObj.getClass(), "getValue", methodType(String.class)); + } catch (NoSuchMethodException ignored) { + valueHandle = publicLookup().findVirtual(propertyObj.getClass(), "value", methodType(String.class)); + } + + MethodHandle signatureHandle; + try { + signatureHandle = publicLookup().findVirtual(propertyObj.getClass(), "getSignature", methodType(String.class)); + } catch(NoSuchMethodException ignored) { + signatureHandle = publicLookup().findVirtual(propertyObj.getClass(), "signature", methodType(String.class)); + } + + base64Signature = (String) signatureHandle.invokeWithArguments(propertyObj); + propertyValue = (String) valueHandle.invokeWithArguments(propertyObj); + } catch (Throwable e) { + Logging.log(Level.ERROR, "Failed to get property attributes", e); + return false; + } + byte[] sig = Base64.getDecoder().decode(base64Signature); byte[] data = propertyValue.getBytes(); @@ -104,7 +130,6 @@ protected byte[] engineSign() { throw new UnsupportedOperationException(); } - @Override @Deprecated protected void engineSetParameter(String param, Object value) { @@ -146,9 +171,6 @@ public MethodVisitor visitMethod(int access, String name, String desc, String si MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions); mv.visitCode(); mv.visitVarInsn(ALOAD, 0); - mv.visitFieldInsn(GETFIELD, "com/mojang/authlib/properties/Property", "value", "Ljava/lang/String;"); - mv.visitVarInsn(ALOAD, 0); - mv.visitFieldInsn(GETFIELD, "com/mojang/authlib/properties/Property", "signature", "Ljava/lang/String;"); ctx.invokeCallback(mv, YggdrasilKeyTransformUnit.class, "verifyPropertySignature"); mv.visitInsn(IRETURN); mv.visitMaxs(-1, -1); @@ -171,9 +193,6 @@ public MethodVisitor visitMethod(int access, String name, String desc, String si MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions); mv.visitCode(); mv.visitVarInsn(ALOAD, 1); - mv.visitMethodInsn(INVOKEVIRTUAL, "com/mojang/authlib/properties/Property", "getValue", "()Ljava/lang/String;", false); - mv.visitVarInsn(ALOAD, 1); - mv.visitMethodInsn(INVOKEVIRTUAL, "com/mojang/authlib/properties/Property", "getSignature", "()Ljava/lang/String;", false); ctx.invokeCallback(mv, YggdrasilKeyTransformUnit.class, "verifyPropertySignature"); mv.visitInsn(IRETURN); mv.visitMaxs(-1, -1); From a050b713aad07f58211428171577b3f1f5a70355 Mon Sep 17 00:00:00 2001 From: Ethan Zuo Date: Fri, 17 Nov 2023 23:46:38 +0800 Subject: [PATCH 8/9] update dependencies --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 5707e84..62f6fc9 100644 --- a/build.gradle +++ b/build.gradle @@ -10,8 +10,8 @@ repositories { dependencies { implementation 'com.google.code.gson:gson:2.10.1' - implementation 'org.ow2.asm:asm:9.5' - testImplementation 'org.junit.jupiter:junit-jupiter:5.9.3' + implementation 'org.ow2.asm:asm:9.6' + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1' } test { From ad95c32298da11e6bfc11ca32c5c12f557dd0a5f Mon Sep 17 00:00:00 2001 From: Ethan Zuo Date: Fri, 24 Nov 2023 00:21:30 +0800 Subject: [PATCH 9/9] Fix namespace feature is broken in Minecraft 1.20.2 --- .../HasJoinedServerResponseTransformer.java | 112 ++++++++++++++++-- .../support/HasJoinedServerTransformer.java | 26 ++-- 2 files changed, 109 insertions(+), 29 deletions(-) diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/transform/support/HasJoinedServerResponseTransformer.java b/src/main/java/xyz/zuoyx/multiyggdrasil/transform/support/HasJoinedServerResponseTransformer.java index 312514b..7279fed 100644 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/transform/support/HasJoinedServerResponseTransformer.java +++ b/src/main/java/xyz/zuoyx/multiyggdrasil/transform/support/HasJoinedServerResponseTransformer.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 Ethan Zuo + * Copyright (C) 2023 Ethan Zuo * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -16,58 +16,143 @@ */ package xyz.zuoyx.multiyggdrasil.transform.support; -import static org.objectweb.asm.Opcodes.ASM9; import static org.objectweb.asm.Opcodes.ACC_PRIVATE; import static org.objectweb.asm.Opcodes.ACC_PUBLIC; import static org.objectweb.asm.Opcodes.ALOAD; -import static org.objectweb.asm.Opcodes.GETFIELD; import static org.objectweb.asm.Opcodes.ARETURN; +import static org.objectweb.asm.Opcodes.ASM9; +import static org.objectweb.asm.Opcodes.GETFIELD; +import static org.objectweb.asm.Opcodes.PUTFIELD; import java.util.Optional; import xyz.zuoyx.multiyggdrasil.transform.TransformContext; import xyz.zuoyx.multiyggdrasil.transform.TransformUnit; +import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.FieldVisitor; import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.RecordComponentVisitor; public class HasJoinedServerResponseTransformer implements TransformUnit { - private boolean isFieldPresent; - private boolean isMethodPresent; + private boolean isNameFieldPresent; + private boolean isNameMethodPresent; @Override public Optional transform(ClassLoader classLoader, String className, ClassVisitor writer, TransformContext context) { if ("com.mojang.authlib.yggdrasil.response.HasJoinedMinecraftServerResponse".equals(className)) { return Optional.of(new ClassVisitor(ASM9, writer) { + @Override + public RecordComponentVisitor visitRecordComponent(String name, String descriptor, String signature) { + if ("properties".equals(name)) { + context.markModified(); + RecordComponentVisitor rcv = super.visitRecordComponent("name", "Ljava/lang/String;", null); + if (rcv != null) { + rcv.visitAnnotation("Ljavax/annotation/Nullable;", true); + rcv.visitEnd(); + } + } + return super.visitRecordComponent(name, descriptor, signature); + } + @Override public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { if ("name".equals(name)) { - isFieldPresent = true; + isNameFieldPresent = true; } return super.visitField(access, name, descriptor, signature, value); } @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { - if ("getName".equals(name)) { - isMethodPresent = true; + if ("name".equals(name)) { + isNameMethodPresent = true; } + + if (access == ACC_PUBLIC && + "".equals(name) && + "(Ljava/util/UUID;Lcom/mojang/authlib/properties/PropertyMap;Ljava/util/Set;)V".equals(descriptor) && + "(Ljava/util/UUID;Lcom/mojang/authlib/properties/PropertyMap;Ljava/util/Set;)V".equals(signature)) { + return new MethodVisitor(ASM9, super.visitMethod(access, name, + "(Ljava/util/UUID;Ljava/lang/String;Lcom/mojang/authlib/properties/PropertyMap;Ljava/util/Set;)V", + "(Ljava/util/UUID;Ljava/lang/String;Lcom/mojang/authlib/properties/PropertyMap;Ljava/util/Set;)V", exceptions)) { + + // 0 - initial state + // aload_0 + // 1 - aload_1 + // 2 - putfield id:Ljava/util/UUID; + // aload_0 + // 3 - aload_2 + // putfield properties:Lcom/mojang/authlib/properties/PropertyMap; + // aload_0 + // 4 - aload_3 + // putfield profileActions:Ljava/util/Set; + int state = 0; + + @Override + public void visitVarInsn(int opcode, int varIndex) { + if (state == 0 && opcode == ALOAD && varIndex == 1) { + state++; + super.visitVarInsn(opcode, varIndex); + } else if (state == 2 && opcode == ALOAD && varIndex == 2) { + state++; + context.markModified(); + super.visitVarInsn(opcode, 3); + } else if (state == 3 && opcode == ALOAD && varIndex == 3) { + state++; + context.markModified(); + super.visitVarInsn(opcode, 4); + } else { + super.visitVarInsn(opcode, varIndex); + } + } + + @Override + public void visitFieldInsn(int opcode, String owner, String name, String descriptor) { + if (state == 1 && opcode == PUTFIELD && "id".equals(name) && "Ljava/util/UUID;".equals(descriptor)) { + state++; + context.markModified(); + super.visitFieldInsn(opcode, owner, name, descriptor); + super.visitVarInsn(ALOAD, 0); + super.visitVarInsn(ALOAD, 2); + super.visitFieldInsn(opcode, owner, "name", "Ljava/lang/String;"); + } else { + super.visitFieldInsn(opcode, owner, name, descriptor); + } + } + }; + } + return super.visitMethod(access, name, descriptor, signature, exceptions); } @Override public void visitEnd() { - if (!isFieldPresent & !isMethodPresent) { + if (!isNameFieldPresent) { context.markModified(); - - FieldVisitor fv = cv.visitField(ACC_PRIVATE, "name", "Ljava/lang/String;", null, null); + FieldVisitor fv = super.visitField(ACC_PRIVATE, "name", "Ljava/lang/String;", null, null); if (fv != null) { + AnnotationVisitor av = fv.visitAnnotation("Lcom/google/gson/annotations/SerializedName;", true); + if (av != null) { + av.visit("value", "name"); + av.visitEnd(); + } + fv.visitAnnotation("Ljavax/annotation/Nullable;", true); fv.visitEnd(); } + } - MethodVisitor mv = cv.visitMethod(ACC_PUBLIC, "getName", "()Ljava/lang/String;", null, null); + if (!isNameMethodPresent) { + context.markModified(); + MethodVisitor mv = super.visitMethod(ACC_PUBLIC, "name", "()Ljava/lang/String;", null, null); if (mv != null) { + AnnotationVisitor av = mv.visitAnnotation("Lcom/google/gson/annotations/SerializedName;", true); + if (av != null) { + av.visit("value", "name"); + av.visitEnd(); + } + mv.visitAnnotation("Ljavax/annotation/Nullable;", true); mv.visitCode(); mv.visitVarInsn(ALOAD, 0); mv.visitFieldInsn(GETFIELD, "com/mojang/authlib/yggdrasil/response/HasJoinedMinecraftServerResponse", "name", "Ljava/lang/String;"); @@ -76,7 +161,8 @@ public void visitEnd() { mv.visitEnd(); } } - cv.visitEnd(); + + super.visitEnd(); } }); } else { diff --git a/src/main/java/xyz/zuoyx/multiyggdrasil/transform/support/HasJoinedServerTransformer.java b/src/main/java/xyz/zuoyx/multiyggdrasil/transform/support/HasJoinedServerTransformer.java index 35b5b3b..759215d 100644 --- a/src/main/java/xyz/zuoyx/multiyggdrasil/transform/support/HasJoinedServerTransformer.java +++ b/src/main/java/xyz/zuoyx/multiyggdrasil/transform/support/HasJoinedServerTransformer.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 Ethan Zuo + * Copyright (C) 2023 Ethan Zuo * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -40,14 +40,13 @@ public Optional transform(ClassLoader classLoader, String classNam @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { if ("hasJoinedServer".equals(name) && - "(Lcom/mojang/authlib/GameProfile;Ljava/lang/String;Ljava/net/InetAddress;)Lcom/mojang/authlib/GameProfile;".equals(descriptor)) { + "(Ljava/lang/String;Ljava/lang/String;Ljava/net/InetAddress;)Lcom/mojang/authlib/yggdrasil/ProfileResult;".equals(descriptor)) { return new MethodVisitor(ASM9, super.visitMethod(access, name, descriptor, signature, exceptions)) { // States: // 0 - initial state - // 1 - invokevirtual com/mojang/authlib/yggdrasil/response/HasJoinedMinecraftServerResponse.getId:()Ljava/util/UUID; + // 1 - invokevirtual com/mojang/authlib/yggdrasil/response/HasJoinedMinecraftServerResponse.id:()Ljava/util/UUID; // 2 - aload_1 - // 3 - invokevirtual com/mojang/authlib/GameProfile.getName:()Ljava/lang/String; int state = 0; @Override @@ -55,21 +54,11 @@ public void visitMethodInsn(int opcode, String owner, String name, String descri if (state == 0 && opcode == INVOKEVIRTUAL && "com/mojang/authlib/yggdrasil/response/HasJoinedMinecraftServerResponse".equals(owner) && - "getId".equals(name) && + "id".equals(name) && "()Ljava/util/UUID;".equals(descriptor)) { state++; - super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); - } else if (state == 2 && - opcode == INVOKEVIRTUAL && - "com/mojang/authlib/GameProfile".equals(owner) && - "getName".equals(name) && - "()Ljava/lang/String;".equals(descriptor)) { - state++; - context.markModified(); - super.visitMethodInsn(opcode, "com/mojang/authlib/yggdrasil/response/HasJoinedMinecraftServerResponse", name, descriptor, isInterface); - } else { - super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); } + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); } @Override @@ -78,6 +67,11 @@ public void visitVarInsn(int opcode, int var) { state++; context.markModified(); super.visitVarInsn(opcode, 6); + super.visitMethodInsn(INVOKEVIRTUAL, + "com/mojang/authlib/yggdrasil/response/HasJoinedMinecraftServerResponse", + "name", + "()Ljava/lang/String;", + false); } else { super.visitVarInsn(opcode, var); }