diff --git a/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/HTTPSession.java b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/HTTPSession.java new file mode 100644 index 0000000..33e527f --- /dev/null +++ b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/HTTPSession.java @@ -0,0 +1,405 @@ +/* + * 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 moe.yushi.authlibinjector.internal.fi.iki.elonen; + +import static java.nio.charset.StandardCharsets.ISO_8859_1; +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; +import java.util.logging.Level; + +class HTTPSession implements IHTTPSession { + + 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 SocketException("NanoHttpd Shutdown"); + } + if (read == -1) { + // socket was been closed + NanoHTTPD.safeClose(this.inputStream); + NanoHTTPD.safeClose(this.outputStream); + throw new SocketException("NanoHttpd Shutdown"); + } + 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()) { + protocolVersion = st.nextToken(); + } else { + protocolVersion = "HTTP/1.1"; + NanoHTTPD.LOG.log(Level.FINE, "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 connection = this.headers.get("connection"); + boolean keepAlive = "HTTP/1.1".equals(protocolVersion) && (connection == null || !connection.matches("(?i).*close.*")); + + String transferEncoding = this.headers.get("transfer-encoding"); + String contentLengthStr = this.headers.get("content-length"); + if (transferEncoding != null && contentLengthStr == null) { + if ("chunked".equals(transferEncoding)) { + parsedInputStream = new ChunkedInputStream(inputStream); + } else { + throw new ResponseException(Status.NOT_IMPLEMENTED, "Unsupported Transfer-Encoding"); + } + + } else if (transferEncoding == null && contentLengthStr != null) { + int contentLength = -1; + try { + contentLength = Integer.parseInt(contentLengthStr); + } catch (NumberFormatException e) { + } + if (contentLength < 0) { + throw new ResponseException(Status.BAD_REQUEST, "The request has an invalid Content-Length header."); + } + parsedInputStream = new FixedLengthInputStream(inputStream, contentLength); + + } else if (transferEncoding != null && contentLengthStr != null) { + throw new ResponseException(Status.BAD_REQUEST, "Content-Length and Transfer-Encoding cannot exist at the same time."); + + } else /* if both are null */ { + // no request payload + parsedInputStream = null; + } + + expect100Continue = "HTTP/1.1".equals(protocolVersion) + && "100-continue".equals(this.headers.get("expect")) + && parsedInputStream != null; + continueSent = false; + + // Ok, now do the serve() + this.isServing = true; + try { + r = handler.apply(this); + } finally { + synchronized (servingLock) { + this.isServing = false; + } + } + + if (!(parsedInputStream == null || (expect100Continue && !continueSent))) { + // consume the input + while (parsedInputStream.read() != -1) + ; + } + + if (r == null) { + throw new ResponseException(Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response."); + } else { + r.setRequestMethod(this.method); + r.setKeepAlive(keepAlive); + r.send(this.outputStream); + } + if (!keepAlive || r.isCloseConnection()) { + throw new SocketException("NanoHttpd Shutdown"); + } + } catch (SocketException e) { + // throw it out to close socket object (finalAccept) + throw e; + } catch (SocketTimeoutException ste) { + // treat socket timeouts the same way we treat socket exceptions + // i.e. close the stream & finalAccept object by throwing the + // exception up the call stack. + throw ste; + } catch (IOException ioe) { + Response resp = Response.newFixedLength(Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); + resp.send(this.outputStream); + NanoHTTPD.safeClose(this.outputStream); + } catch (ResponseException re) { + Response resp = Response.newFixedLength(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, 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 (servingLock) { + if (!isServing) { + throw new IllegalStateException(); + } + if (expect100Continue && !continueSent) { + 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) { + String decoded = null; + try { + decoded = URLDecoder.decode(str, "UTF8"); + } catch (UnsupportedEncodingException ignored) { + NanoHTTPD.LOG.log(Level.WARNING, "Encoding not supported, ignored", ignored); + } + return decoded; + } + + /** + * 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/moe/yushi/authlibinjector/internal/fi/iki/elonen/NanoHTTPD.java b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/NanoHTTPD.java index 65850cf..ce761d6 100644 --- a/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/NanoHTTPD.java +++ b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/NanoHTTPD.java @@ -46,29 +46,18 @@ */ package moe.yushi.authlibinjector.internal.fi.iki.elonen; -import static java.nio.charset.StandardCharsets.ISO_8859_1; -import java.io.BufferedInputStream; -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.io.OutputStream; -import java.io.UnsupportedEncodingException; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; import java.net.SocketTimeoutException; -import java.net.URLDecoder; import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.StringTokenizer; import java.util.logging.Level; import java.util.logging.Logger; @@ -111,7 +100,7 @@ public abstract class NanoHTTPD { outputStream = this.acceptSocket.getOutputStream(); HTTPSession session = new HTTPSession(this.inputStream, outputStream, (InetSocketAddress) this.acceptSocket.getRemoteSocketAddress()); while (!this.acceptSocket.isClosed()) { - session.execute(); + session.execute(NanoHTTPD.this::serve); } } catch (Exception e) { // When the socket is closed by the client, @@ -169,291 +158,6 @@ public abstract class NanoHTTPD { } } - private class HTTPSession implements IHTTPSession { - - 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, HTTPSession.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[HTTPSession.BUFSIZE]; - int splitbyte = 0; - int rlen = 0; - - int read = -1; - this.inputStream.mark(HTTPSession.BUFSIZE); - try { - read = this.inputStream.read(buf, 0, HTTPSession.BUFSIZE); - } catch (IOException e) { - safeClose(this.inputStream); - safeClose(this.outputStream); - throw new SocketException("NanoHttpd Shutdown"); - } - if (read == -1) { - // socket was been closed - safeClose(this.inputStream); - safeClose(this.outputStream); - throw new SocketException("NanoHttpd Shutdown"); - } - while (read > 0) { - rlen += read; - splitbyte = findHeaderEnd(buf, rlen); - if (splitbyte > 0) { - break; - } - read = this.inputStream.read(buf, rlen, HTTPSession.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()) { - protocolVersion = st.nextToken(); - } else { - protocolVersion = "HTTP/1.1"; - NanoHTTPD.LOG.log(Level.FINE, "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); - } - } - - @SuppressWarnings("resource") - public void execute() throws IOException { - Response r = null; - try { - parseHeader(new BufferedReader(new InputStreamReader(readHeader(), ISO_8859_1))); - - String connection = this.headers.get("connection"); - boolean keepAlive = "HTTP/1.1".equals(protocolVersion) && (connection == null || !connection.matches("(?i).*close.*")); - - String transferEncoding = this.headers.get("transfer-encoding"); - String contentLengthStr = this.headers.get("content-length"); - if (transferEncoding != null && contentLengthStr == null) { - if ("chunked".equals(transferEncoding)) { - parsedInputStream = new ChunkedInputStream(inputStream); - } else { - throw new ResponseException(Status.NOT_IMPLEMENTED, "Unsupported Transfer-Encoding"); - } - - } else if (transferEncoding == null && contentLengthStr != null) { - int contentLength = -1; - try { - contentLength = Integer.parseInt(contentLengthStr); - } catch (NumberFormatException e) { - } - if (contentLength < 0) { - throw new ResponseException(Status.BAD_REQUEST, "The request has an invalid Content-Length header."); - } - parsedInputStream = new FixedLengthInputStream(inputStream, contentLength); - - } else if (transferEncoding != null && contentLengthStr != null) { - throw new ResponseException(Status.BAD_REQUEST, "Content-Length and Transfer-Encoding cannot exist at the same time."); - - } else /* if both are null */ { - // no request payload - parsedInputStream = null; - } - - expect100Continue = "HTTP/1.1".equals(protocolVersion) - && "100-continue".equals(this.headers.get("expect")) - && parsedInputStream != null; - continueSent = false; - - // Ok, now do the serve() - this.isServing = true; - try { - r = serve(this); - } finally { - synchronized (servingLock) { - this.isServing = false; - } - } - - if (!(parsedInputStream == null || (expect100Continue && !continueSent))) { - // consume the input - while (parsedInputStream.read() != -1) - ; - } - - if (r == null) { - throw new ResponseException(Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response."); - } else { - r.setRequestMethod(this.method); - r.setKeepAlive(keepAlive); - r.send(this.outputStream); - } - if (!keepAlive || r.isCloseConnection()) { - throw new SocketException("NanoHttpd Shutdown"); - } - } catch (SocketException e) { - // throw it out to close socket object (finalAccept) - throw e; - } catch (SocketTimeoutException ste) { - // treat socket timeouts the same way we treat socket exceptions - // i.e. close the stream & finalAccept object by throwing the - // exception up the call stack. - throw ste; - } catch (IOException ioe) { - Response resp = Response.newFixedLength(Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage()); - resp.send(this.outputStream); - safeClose(this.outputStream); - } catch (ResponseException re) { - Response resp = Response.newFixedLength(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage()); - resp.send(this.outputStream); - safeClose(this.outputStream); - } finally { - safeClose(r); - } - } - - /** - * Find byte index separating header from body. It must be the last byte - * of the first two sequential new lines. - */ - private int findHeaderEnd(final byte[] buf, int rlen) { - int splitbyte = 0; - while (splitbyte + 1 < rlen) { - - // RFC2616 - if (buf[splitbyte] == '\r' && buf[splitbyte + 1] == '\n' && splitbyte + 3 < rlen && buf[splitbyte + 2] == '\r' && buf[splitbyte + 3] == '\n') { - return splitbyte + 4; - } - - // tolerance - if (buf[splitbyte] == '\n' && buf[splitbyte + 1] == '\n') { - return splitbyte + 2; - } - splitbyte++; - } - return 0; - } - - @Override - public final Map getHeaders() { - return this.headers; - } - - @Override - public final InputStream getInputStream() throws IOException { - synchronized (servingLock) { - if (!isServing) { - throw new IllegalStateException(); - } - if (expect100Continue && !continueSent) { - continueSent = true; - this.outputStream.write("HTTP/1.1 100 Continue\r\n\r\n".getBytes(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; - } - } - /** * The runnable that will be used for the main listening thread. */ @@ -575,57 +279,6 @@ public abstract class NanoHTTPD { stop(); } - /** - * 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) { - String decoded = null; - try { - decoded = URLDecoder.decode(str, "UTF8"); - } catch (UnsupportedEncodingException ignored) { - NanoHTTPD.LOG.log(Level.WARNING, "Encoding not supported, ignored", ignored); - } - return decoded; - } - - /** - * 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; - } - public final int getListeningPort() { return this.myServerSocket == null ? -1 : this.myServerSocket.getLocalPort(); }