mirror of
https://github.com/yushijinhun/authlib-injector.git
synced 2024-11-15 06:11:09 +01:00
nanohttpd: refactor: move HTTPSession to a new file
This commit is contained in:
parent
d2ee1cb282
commit
f0e635bff3
2 changed files with 406 additions and 348 deletions
|
@ -0,0 +1,405 @@
|
|||
/*
|
||||
* Copyright (C) 2020 Haowei Wen <yushijinhun@gmail.com> 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
/*
|
||||
* 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<String, List<String>> parms;
|
||||
private Map<String, String> 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<String, String> 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<IHTTPSession, Response> 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<String, String> getHeaders() {
|
||||
return this.headers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final InputStream getInputStream() throws IOException {
|
||||
synchronized (servingLock) {
|
||||
if (!isServing) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
if (expect100Continue && !continueSent) {
|
||||
continueSent = true;
|
||||
this.outputStream.write("HTTP/1.1 100 Continue\r\n\r\n".getBytes(ISO_8859_1));
|
||||
}
|
||||
}
|
||||
return this.parsedInputStream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String getMethod() {
|
||||
return this.method;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final Map<String, List<String>> getParameters() {
|
||||
return this.parms;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getQueryParameterString() {
|
||||
return this.queryParameterString;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String getUri() {
|
||||
return this.uri;
|
||||
}
|
||||
|
||||
@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 <code>String</code> values.
|
||||
*
|
||||
* @param str
|
||||
* the percent encoded <code>String</code>
|
||||
* @return expanded form of the input, for example "foo%20bar" becomes
|
||||
* "foo bar"
|
||||
*/
|
||||
private static String decodePercent(String str) {
|
||||
String decoded = null;
|
||||
try {
|
||||
decoded = URLDecoder.decode(str, "UTF8");
|
||||
} catch (UnsupportedEncodingException ignored) {
|
||||
NanoHTTPD.LOG.log(Level.WARNING, "Encoding not supported, ignored", ignored);
|
||||
}
|
||||
return decoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes parameters in percent-encoded URI-format ( e.g.
|
||||
* "name=Jack%20Daniels&pass=Single%20Malt" ) and adds them to given
|
||||
* Map.
|
||||
*/
|
||||
private static Map<String, List<String>> decodeParms(String parms) {
|
||||
Map<String, List<String>> 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<String> values = result.get(key);
|
||||
if (values == null) {
|
||||
values = new ArrayList<>();
|
||||
result.put(key, values);
|
||||
}
|
||||
|
||||
values.add(value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -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<String, List<String>> parms;
|
||||
private Map<String, String> 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<String, String> 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<String, String> getHeaders() {
|
||||
return this.headers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final InputStream getInputStream() throws IOException {
|
||||
synchronized (servingLock) {
|
||||
if (!isServing) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
if (expect100Continue && !continueSent) {
|
||||
continueSent = true;
|
||||
this.outputStream.write("HTTP/1.1 100 Continue\r\n\r\n".getBytes(ISO_8859_1));
|
||||
}
|
||||
}
|
||||
return this.parsedInputStream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String getMethod() {
|
||||
return this.method;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final Map<String, List<String>> getParameters() {
|
||||
return this.parms;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getQueryParameterString() {
|
||||
return this.queryParameterString;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String getUri() {
|
||||
return this.uri;
|
||||
}
|
||||
|
||||
@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 <code>String</code> values.
|
||||
*
|
||||
* @param str
|
||||
* the percent encoded <code>String</code>
|
||||
* @return expanded form of the input, for example "foo%20bar" becomes
|
||||
* "foo bar"
|
||||
*/
|
||||
private static String decodePercent(String str) {
|
||||
String decoded = null;
|
||||
try {
|
||||
decoded = URLDecoder.decode(str, "UTF8");
|
||||
} catch (UnsupportedEncodingException ignored) {
|
||||
NanoHTTPD.LOG.log(Level.WARNING, "Encoding not supported, ignored", ignored);
|
||||
}
|
||||
return decoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes parameters in percent-encoded URI-format ( e.g.
|
||||
* "name=Jack%20Daniels&pass=Single%20Malt" ) and adds them to given
|
||||
* Map.
|
||||
*/
|
||||
private static Map<String, List<String>> decodeParms(String parms) {
|
||||
Map<String, List<String>> 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<String> 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();
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue