Rewrite integrated webserver to be non-blocking

This commit is contained in:
Lukas Rieger (Blue) 2023-02-22 16:19:34 +01:00
parent 9917e5dfa5
commit f4c6adc685
No known key found for this signature in database
GPG Key ID: 2D09EC5ED2687FF2
25 changed files with 837 additions and 978 deletions

View File

@ -45,8 +45,6 @@ public class WebserverConfig {
private int port = 8100;
private int maxConnectionCount = 100;
public boolean isEnabled() {
return enabled;
}
@ -73,8 +71,4 @@ public class WebserverConfig {
return port;
}
public int getMaxConnectionCount() {
return maxConnectionCount;
}
}

View File

@ -37,10 +37,7 @@ import de.bluecolored.bluemap.common.rendermanager.MapUpdateTask;
import de.bluecolored.bluemap.common.rendermanager.RenderManager;
import de.bluecolored.bluemap.common.serverinterface.ServerEventListener;
import de.bluecolored.bluemap.common.serverinterface.ServerInterface;
import de.bluecolored.bluemap.common.web.FileRequestHandler;
import de.bluecolored.bluemap.common.web.MapRequestHandler;
import de.bluecolored.bluemap.common.web.RoutingRequestHandler;
import de.bluecolored.bluemap.common.webserver.WebServer;
import de.bluecolored.bluemap.common.web.*;
import de.bluecolored.bluemap.core.debug.StateDumper;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.map.BmMap;
@ -57,6 +54,7 @@ import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.file.Path;
import java.util.*;
@ -189,23 +187,26 @@ public class Plugin implements ServerEventListener {
routingRequestHandler.register(
"maps/" + Pattern.quote(id) + "/(.*)",
"$1",
mapRequestHandler
new BlueMapResponseModifier(mapRequestHandler)
);
}
webServer = new WebServer(routingRequestHandler);
webServer.start();
try {
webServer = new WebServer(
webServer.bind(new InetSocketAddress(
webserverConfig.resolveIp(),
webserverConfig.getPort(),
webserverConfig.getMaxConnectionCount(),
routingRequestHandler,
false
);
webserverConfig.getPort()
));
} catch (UnknownHostException ex) {
throw new ConfigurationException("BlueMap failed to resolve the ip in your webserver-config.\n" +
"Check if that is correctly configured.", ex);
} catch (IOException ex) {
throw new ConfigurationException("BlueMap failed to initialize the webserver.\n" +
"Check your webserver-config if everything is configured correctly.\n" +
"(Make sure you DON'T use the same port for bluemap that you also use for your minecraft server)", ex);
}
webServer.start();
}
//initialize render manager
@ -376,7 +377,13 @@ public class Plugin implements ServerEventListener {
}
renderManager = null;
if (webServer != null) webServer.close();
if (webServer != null) {
try {
webServer.close();
} catch (IOException ex) {
Logger.global.logError("Failed to close the webserver!", ex);
}
}
webServer = null;
//close bluemap

View File

@ -24,10 +24,10 @@
*/
package de.bluecolored.bluemap.common.web;
import de.bluecolored.bluemap.common.webserver.HttpRequest;
import de.bluecolored.bluemap.common.webserver.HttpRequestHandler;
import de.bluecolored.bluemap.common.webserver.HttpResponse;
import de.bluecolored.bluemap.common.webserver.HttpStatusCode;
import de.bluecolored.bluemap.common.web.http.HttpRequest;
import de.bluecolored.bluemap.common.web.http.HttpRequestHandler;
import de.bluecolored.bluemap.common.web.http.HttpResponse;
import de.bluecolored.bluemap.common.web.http.HttpStatusCode;
import de.bluecolored.bluemap.core.BlueMap;
public class BlueMapResponseModifier implements HttpRequestHandler {
@ -37,7 +37,7 @@ public class BlueMapResponseModifier implements HttpRequestHandler {
public BlueMapResponseModifier(HttpRequestHandler delegate) {
this.delegate = delegate;
this.serverName = "BlueMap " + BlueMap.VERSION + " " + BlueMap.GIT_HASH;
this.serverName = "BlueMap/" + BlueMap.VERSION;
}
@Override

View File

@ -24,10 +24,7 @@
*/
package de.bluecolored.bluemap.common.web;
import de.bluecolored.bluemap.common.webserver.HttpRequest;
import de.bluecolored.bluemap.common.webserver.HttpRequestHandler;
import de.bluecolored.bluemap.common.webserver.HttpResponse;
import de.bluecolored.bluemap.common.webserver.HttpStatusCode;
import de.bluecolored.bluemap.common.web.http.*;
import org.apache.commons.lang3.time.DateFormatUtils;
import java.io.File;
@ -35,7 +32,10 @@ import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.*;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
public class FileRequestHandler implements HttpRequestHandler {
@ -50,13 +50,9 @@ public class FileRequestHandler implements HttpRequestHandler {
@Override
public HttpResponse handle(HttpRequest request) {
if (
!request.getMethod().equalsIgnoreCase("GET")
) return new HttpResponse(HttpStatusCode.BAD_REQUEST);
HttpResponse response = generateResponse(request);
return response;
if (!request.getMethod().equalsIgnoreCase("GET"))
return new HttpResponse(HttpStatusCode.BAD_REQUEST);
return generateResponse(request);
}
private HttpResponse generateResponse(HttpRequest request) {
@ -113,10 +109,10 @@ public class FileRequestHandler implements HttpRequestHandler {
// check modified
long lastModified = file.lastModified();
Set<String> modStringSet = request.getHeader("If-Modified-Since");
if (!modStringSet.isEmpty()){
HttpHeader modHeader = request.getHeader("If-Modified-Since");
if (modHeader != null){
try {
long since = stringToTimestamp(modStringSet.iterator().next());
long since = stringToTimestamp(modHeader.getValue());
if (since + 1000 >= lastModified){
return new HttpResponse(HttpStatusCode.NOT_MODIFIED);
}
@ -125,9 +121,9 @@ public class FileRequestHandler implements HttpRequestHandler {
//check ETag
String eTag = Long.toHexString(file.length()) + Integer.toHexString(file.hashCode()) + Long.toHexString(lastModified);
Set<String> etagStringSet = request.getHeader("If-None-Match");
if (!etagStringSet.isEmpty()){
if(etagStringSet.iterator().next().equals(eTag)) {
HttpHeader etagHeader = request.getHeader("If-None-Match");
if (etagHeader != null){
if(etagHeader.getValue().equals(eTag)) {
return new HttpResponse(HttpStatusCode.NOT_MODIFIED);
}
}

View File

@ -24,10 +24,10 @@
*/
package de.bluecolored.bluemap.common.web;
import de.bluecolored.bluemap.common.webserver.HttpRequest;
import de.bluecolored.bluemap.common.webserver.HttpRequestHandler;
import de.bluecolored.bluemap.common.webserver.HttpResponse;
import de.bluecolored.bluemap.common.webserver.HttpStatusCode;
import de.bluecolored.bluemap.common.web.http.HttpRequest;
import de.bluecolored.bluemap.common.web.http.HttpRequestHandler;
import de.bluecolored.bluemap.common.web.http.HttpResponse;
import de.bluecolored.bluemap.common.web.http.HttpStatusCode;
import de.bluecolored.bluemap.core.BlueMap;
import java.util.function.Supplier;

View File

@ -0,0 +1,42 @@
package de.bluecolored.bluemap.common.web;
import de.bluecolored.bluemap.common.web.http.HttpRequest;
import de.bluecolored.bluemap.common.web.http.HttpRequestHandler;
import de.bluecolored.bluemap.common.web.http.HttpResponse;
import de.bluecolored.bluemap.core.logger.Logger;
public class LoggingRequestHandler implements HttpRequestHandler {
private final HttpRequestHandler delegate;
private final Logger logger;
public LoggingRequestHandler(HttpRequestHandler delegate) {
this(delegate, Logger.global);
}
public LoggingRequestHandler(HttpRequestHandler delegate, Logger logger) {
this.delegate = delegate;
this.logger = logger;
}
@Override
public HttpResponse handle(HttpRequest request) {
String log = request.getSource() + " \""
+ request.getMethod()
+ " " + request.getAddress()
+ " " + request.getVersion()
+ "\" ";
HttpResponse response = delegate.handle(request);
log += response.getStatusCode().toString();
if (response.getStatusCode().getCode() < 400) {
logger.logInfo(log);
} else {
logger.logWarning(log);
}
return response;
}
}

View File

@ -26,13 +26,13 @@ package de.bluecolored.bluemap.common.web;
import com.flowpowered.math.vector.Vector2i;
import de.bluecolored.bluemap.api.ContentTypeRegistry;
import de.bluecolored.bluemap.common.webserver.HttpRequest;
import de.bluecolored.bluemap.common.webserver.HttpRequestHandler;
import de.bluecolored.bluemap.common.webserver.HttpResponse;
import de.bluecolored.bluemap.common.webserver.HttpStatusCode;
import de.bluecolored.bluemap.common.web.http.*;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.map.BmMap;
import de.bluecolored.bluemap.core.storage.*;
import de.bluecolored.bluemap.core.storage.CompressedInputStream;
import de.bluecolored.bluemap.core.storage.Compression;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.storage.TileInfo;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
@ -82,19 +82,19 @@ public class MapStorageRequestHandler implements HttpRequestHandler {
// check e-tag
String eTag = calculateETag(path, tileInfo);
Set<String> etagStringSet = request.getHeader("If-None-Match");
if (!etagStringSet.isEmpty()){
if(etagStringSet.iterator().next().equals(eTag)) {
HttpHeader etagHeader = request.getHeader("If-None-Match");
if (etagHeader != null){
if(etagHeader.getValue().equals(eTag)) {
return new HttpResponse(HttpStatusCode.NOT_MODIFIED);
}
}
// check modified-since
long lastModified = tileInfo.getLastModified();
Set<String> modStringSet = request.getHeader("If-Modified-Since");
if (!modStringSet.isEmpty()){
HttpHeader modHeader = request.getHeader("If-Modified-Since");
if (modHeader != null){
try {
long since = stringToTimestamp(modStringSet.iterator().next());
long since = stringToTimestamp(modHeader.getValue());
if (since + 1000 >= lastModified){
return new HttpResponse(HttpStatusCode.NOT_MODIFIED);
}
@ -144,14 +144,14 @@ public class MapStorageRequestHandler implements HttpRequestHandler {
Compression compression = data.getCompression();
if (
compression != Compression.NONE &&
request.getLowercaseHeader("Accept-Encoding").contains(compression.getTypeId())
request.hasHeaderValue("Accept-Encoding", compression.getTypeId())
) {
response.addHeader("Content-Encoding", compression.getTypeId());
response.setData(data);
} else if (
compression != Compression.GZIP &&
!response.getHeader("Content-Type").contains("image/png") &&
request.getLowercaseHeader("Accept-Encoding").contains(Compression.GZIP.getTypeId())
!response.hasHeaderValue("Content-Type", "image/png") &&
request.hasHeaderValue("Accept-Encoding", Compression.GZIP.getTypeId())
) {
response.addHeader("Content-Encoding", Compression.GZIP.getTypeId());
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();

View File

@ -24,7 +24,10 @@
*/
package de.bluecolored.bluemap.common.web;
import de.bluecolored.bluemap.common.webserver.*;
import de.bluecolored.bluemap.common.web.http.HttpRequest;
import de.bluecolored.bluemap.common.web.http.HttpRequestHandler;
import de.bluecolored.bluemap.common.web.http.HttpResponse;
import de.bluecolored.bluemap.common.web.http.HttpStatusCode;
import org.intellij.lang.annotations.Language;
import java.util.LinkedList;
@ -61,14 +64,13 @@ public class RoutingRequestHandler implements HttpRequestHandler {
// normalize path
if (path.startsWith("/")) path = path.substring(1);
if (path.endsWith("/")) path = path.substring(0, path.length() - 1);
if (path.isEmpty()) path = "/";
for (Route route : routes) {
Matcher matcher = route.getRoutePattern().matcher(path);
if (matcher.matches()) {
RewrittenHttpRequest rewrittenRequest = new RewrittenHttpRequest(request);
rewrittenRequest.setPath(matcher.replaceFirst(route.getReplacementRoute()));
return route.handler.handle(rewrittenRequest);
request.setPath(matcher.replaceFirst(route.getReplacementRoute()));
return route.getHandler().handle(request);
}
}

View File

@ -0,0 +1,23 @@
package de.bluecolored.bluemap.common.web;
import de.bluecolored.bluemap.common.web.http.HttpConnection;
import de.bluecolored.bluemap.common.web.http.HttpRequestHandler;
import de.bluecolored.bluemap.common.web.http.SelectionConsumer;
import de.bluecolored.bluemap.common.web.http.Server;
import java.io.IOException;
public class WebServer extends Server {
private final HttpRequestHandler requestHandler;
public WebServer(HttpRequestHandler requestHandler) throws IOException {
this.requestHandler = requestHandler;
}
@Override
public SelectionConsumer createConnectionHandler() {
return new HttpConnection(requestHandler);
}
}

View File

@ -0,0 +1,87 @@
package de.bluecolored.bluemap.common.web.http;
import de.bluecolored.bluemap.core.logger.Logger;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.util.concurrent.locks.ReentrantLock;
public class HttpConnection implements SelectionConsumer {
private final ReentrantLock processingLock = new ReentrantLock();
private final HttpRequestHandler requestHandler;
private HttpRequest request;
private HttpResponse response;
public HttpConnection(HttpRequestHandler requestHandler) {
this.requestHandler = requestHandler;
}
@Override
public void accept(SelectionKey selectionKey) {
if (!selectionKey.isValid()) return;
if (!processingLock.tryLock()) return;
try {
SelectableChannel selChannel = selectionKey.channel();
if (!(selChannel instanceof SocketChannel)) return;
SocketChannel channel = (SocketChannel) selChannel;
try {
if (request == null) {
SocketAddress remote = channel.getRemoteAddress();
InetAddress remoteInet = null;
if (remote instanceof InetSocketAddress)
remoteInet = ((InetSocketAddress) remote).getAddress();
request = new HttpRequest(remoteInet);
}
// receive request
if (!request.write(channel)) {
if (!selectionKey.isValid()) return;
selectionKey.interestOps(SelectionKey.OP_READ);
return;
}
// process request
if (response == null) {
this.response = requestHandler.handle(request);
}
if (!selectionKey.isValid()) return;
// send response
if (!response.read(channel)){
selectionKey.interestOps(SelectionKey.OP_WRITE);
return;
}
// reset to accept new request
request.clear();
response.close();
response = null;
selectionKey.interestOps(SelectionKey.OP_READ);
} catch (IOException e) {
Logger.global.logDebug("Failed to process selection: " + e);
try {
channel.close();
} catch (IOException e2) {
Logger.global.logWarning("Failed to close channel" + e2);
}
}
} finally {
processingLock.unlock();
}
}
}

View File

@ -0,0 +1,47 @@
package de.bluecolored.bluemap.common.web.http;
import java.util.*;
public class HttpHeader {
private final String key;
private final String value;
private List<String> values;
private Set<String> valuesLC;
public HttpHeader(String key, String value) {
this.key = key;
this.value = value;
}
public String getKey() {
return key;
}
public String getValue() {
return value;
}
public List<String> getValues() {
if (values == null) {
values = new ArrayList<>();
for (String v : value.split(",")) {
values.add(v.trim());
}
}
return values;
}
public boolean contains(String value) {
if (valuesLC == null) {
valuesLC = new HashSet<>();
for (String v : getValues()) {
valuesLC.add(v.toLowerCase(Locale.ROOT));
}
}
return valuesLC.contains(value);
}
}

View File

@ -0,0 +1,257 @@
package de.bluecolored.bluemap.common.web.http;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class HttpRequest {
private static final Pattern REQUEST_PATTERN = Pattern.compile("^(\\w+) (\\S+) (.+)$");
// reading helper
private final ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
private final StringBuffer lineBuffer = new StringBuffer();
private boolean complete = false;
private boolean headerComplete = false;
private final List<String> headerLines = new ArrayList<>(20);
// request data
private final InetAddress source;
private String method, address, version;
private final Map<String, HttpHeader> headers = new HashMap<>();
private byte[] data;
// lazy parsed
private String path = null;
private String getParamString = null;
private Map<String, String> getParams = null;
public HttpRequest(InetAddress source) {
this.source = source;
}
public boolean write(ReadableByteChannel channel) throws IOException {
if (complete) return true;
int read = channel.read(byteBuffer);
if (read == 0) return false;
if (read == -1) {
channel.close();
return false;
}
byteBuffer.flip();
try {
// read headers
while (!headerComplete) {
if (!writeLine()) return false;
String line = lineBuffer.toString().stripTrailing();
lineBuffer.setLength(0);
if (line.isEmpty()) {
headerComplete = true;
parseHeaders();
} else {
headerLines.add(line);
}
}
if (hasHeaderValue("transfer-encoding", "chunked")) {
writeChunkedBody();
} else {
HttpHeader contentLengthHeader = getHeader("content-length");
int contentLength = 0;
if (contentLengthHeader != null) {
try {
contentLength = Integer.parseInt(contentLengthHeader.getValue().trim());
} catch (NumberFormatException ex) {
throw new IOException("Invalid HTTP Request: content-length is not a number", ex);
}
}
if (contentLength > 0) {
writeBody(contentLength);
}
}
complete = true;
return true;
} finally {
byteBuffer.compact();
}
}
private void writeChunkedBody() {
// TODO
}
private void writeBody(int length) {
// TODO
}
private void parseHeaders() throws IOException {
if (headerLines.isEmpty()) throw new IOException("Invalid HTTP Request: No Header");
Matcher m = REQUEST_PATTERN.matcher(headerLines.get(0));
if (!m.find()) throw new IOException("Invalid HTTP Request: Request-Pattern not matching");
method = m.group(1);
if (method == null) throw new IOException("Invalid HTTP Request: Request-Pattern not matching (method)");
address = m.group(2);
if (address == null) throw new IOException("Invalid HTTP Request: Request-Pattern not matching (address)");
version = m.group(3);
if (version == null) throw new IOException("Invalid HTTP Request: Request-Pattern not matching (version)");
headers.clear();
for (int i = 1; i < headerLines.size(); i++) {
String line = headerLines.get(i);
if (line.trim().isEmpty()) continue;
String[] kv = line.split(":", 2);
if (kv.length < 2) continue;
headers.put(kv[0].trim().toLowerCase(Locale.ROOT), new HttpHeader(kv[0], kv[1]));
}
}
private boolean writeLine() {
while (lineBuffer.length() <= 0 || lineBuffer.charAt(lineBuffer.length() - 1) != '\n'){
if (!byteBuffer.hasRemaining()) return false;
lineBuffer.append((char) byteBuffer.get());
}
return true;
}
public InetAddress getSource() {
return source;
}
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
this.path = null;
this.getParams = null;
this.getParamString = null;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public Map<String, HttpHeader> getHeaders() {
return headers;
}
public HttpHeader getHeader(String header) {
return this.headers.get(header.toLowerCase(Locale.ROOT));
}
public boolean hasHeaderValue(String key, String value) {
HttpHeader header = getHeader(key);
if (header == null) return false;
return header.contains(value);
}
public byte[] getData() {
return data;
}
public InputStream getDataStream() {
return new ByteArrayInputStream(data);
}
public String getPath() {
if (path == null) parseAddress();
return path;
}
public void setPath(String path) {
this.path = path;
}
public Map<String, String> getGETParams() {
if (getParams == null) parseGetParams();
return getParams;
}
public String getGETParamString() {
if (getParamString == null) parseAddress();
return getParamString;
}
public void setGetParamString(String getParamString) {
this.getParamString = getParamString;
this.getParams = null;
}
private void parseAddress() {
String address = this.getAddress();
if (address.isEmpty()) address = "/";
String[] addressParts = address.split("\\?", 2);
String path = addressParts[0];
this.getParamString = addressParts.length > 1 ? addressParts[1] : "";
this.path = path;
}
private void parseGetParams() {
Map<String, String> getParams = new HashMap<>();
for (String getParam : this.getGETParamString().split("&")){
if (getParam.isEmpty()) continue;
String[] kv = getParam.split("=", 2);
String key = kv[0];
String value = kv.length > 1 ? kv[1] : "";
getParams.put(key, value);
}
this.getParams = getParams;
}
public boolean isComplete() {
return complete;
}
public void clear() {
byteBuffer.clear();
lineBuffer.setLength(0);
complete = false;
headerComplete = false;
headerLines.clear();
method = null;
address = null;
version = null;
headers.clear();
data = null;
path = null;
getParamString = null;
getParams = null;
}
}

View File

@ -22,7 +22,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.common.webserver;
package de.bluecolored.bluemap.common.web.http;
@FunctionalInterface
public interface HttpRequestHandler {

View File

@ -0,0 +1,210 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.common.web.http;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
public class HttpResponse implements Closeable {
private static final byte[] CHUNK_SUFFIX = "\r\n".getBytes(StandardCharsets.UTF_8);
private String version;
private HttpStatusCode statusCode;
private final Map<String, HttpHeader> headers;
private ReadableByteChannel data;
private ByteBuffer headerData;
private ByteBuffer dataBuffer;
private boolean complete = false;
private boolean headerComplete = false;
private boolean dataChannelComplete = false;
private boolean dataComplete = false;
public HttpResponse(HttpStatusCode statusCode) {
this.version = "HTTP/1.1";
this.statusCode = statusCode;
this.headers = new HashMap<>();
}
public boolean read(WritableByteChannel channel) throws IOException {
if (complete) return true;
// send headers
if (!headerComplete) {
if (headerData == null) writeHeaderData();
if (headerData.hasRemaining()) {
channel.write(headerData);
}
if (headerData.hasRemaining()) return false;
headerComplete = true;
headerData = null; // free ram
}
if (!hasData()){
complete = true;
return true;
}
// send data chunked
if (dataBuffer == null) dataBuffer = ByteBuffer.allocate(1024 + 200).flip(); // 200 extra bytes
while (true) {
if (dataBuffer.hasRemaining()) channel.write(dataBuffer);
if (dataBuffer.hasRemaining()) return false;
if (dataComplete) break; // nothing more to do
// fill data buffer from channel
dataBuffer.clear();
dataBuffer.position(100); // keep 100 space in front
dataBuffer.limit(1124); // keep 100 space at the end
int readTotal = 0;
if (!dataChannelComplete) {
int read = 0;
while (dataBuffer.hasRemaining() && (read = data.read(dataBuffer)) != -1) {
readTotal += read;
}
if (read == -1) dataChannelComplete = true;
}
if (readTotal == 0) dataComplete = true;
byte[] chunkPrefix = (Integer.toHexString(readTotal) + "\r\n")
.getBytes(StandardCharsets.UTF_8);
dataBuffer.limit(dataBuffer.capacity());
dataBuffer.put(CHUNK_SUFFIX);
dataBuffer.limit(dataBuffer.position());
int startPos = 100 - chunkPrefix.length;
dataBuffer.position(startPos);
dataBuffer.put(chunkPrefix);
dataBuffer.position(startPos);
}
complete = true;
return true;
}
private void writeHeaderData() {
ByteArrayOutputStream headerDataOut = new ByteArrayOutputStream();
if (hasData()){
headers.put("Transfer-Encoding", new HttpHeader("Transfer-Encoding", "chunked"));
} else {
headers.put("Content-Length", new HttpHeader("Content-Length", "0"));
}
headerDataOut.writeBytes((version + " " + statusCode.getCode() + " " + statusCode.getMessage() + "\r\n")
.getBytes(StandardCharsets.UTF_8));
for (HttpHeader header : headers.values()){
headerDataOut.writeBytes((header.getKey() + ": " + header.getValue() + "\r\n")
.getBytes(StandardCharsets.UTF_8));
}
headerDataOut.writeBytes(("\r\n")
.getBytes(StandardCharsets.UTF_8));
headerData = ByteBuffer.allocate(headerDataOut.size())
.put(headerDataOut.toByteArray())
.flip();
}
public void addHeader(String key, String value){
HttpHeader header;
HttpHeader existing = getHeader(key);
if (existing != null) {
header = new HttpHeader(existing.getKey(), existing.getValue() + ", " + value);
} else {
header = new HttpHeader(key, value);
}
this.headers.put(key.toLowerCase(Locale.ROOT), header);
}
public void setData(ReadableByteChannel channel){
this.data = channel;
}
public void setData(InputStream dataStream){
this.data = Channels.newChannel(dataStream);
}
public void setData(String data){
setData(new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)));
}
public boolean hasData() {
return this.data != null;
}
public boolean isComplete() {
return complete;
}
@Override
public void close() throws IOException {
if (data != null) data.close();
}
public HttpStatusCode getStatusCode(){
return statusCode;
}
public void setStatusCode(HttpStatusCode statusCode) {
this.statusCode = statusCode;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public Map<String, HttpHeader> getHeaders() {
return headers;
}
public HttpHeader getHeader(String header) {
return this.headers.get(header.toLowerCase(Locale.ROOT));
}
public boolean hasHeaderValue(String key, String value) {
HttpHeader header = getHeader(key);
if (header == null) return false;
return header.contains(value);
}
}

View File

@ -22,7 +22,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.common.webserver;
package de.bluecolored.bluemap.common.web.http;
public enum HttpStatusCode {

View File

@ -0,0 +1,6 @@
package de.bluecolored.bluemap.common.web.http;
import java.nio.channels.SelectionKey;
import java.util.function.Consumer;
public interface SelectionConsumer extends Consumer<SelectionKey> {}

View File

@ -0,0 +1,89 @@
package de.bluecolored.bluemap.common.web.http;
import de.bluecolored.bluemap.core.logger.Logger;
import java.io.Closeable;
import java.io.IOException;
import java.net.SocketAddress;
import java.nio.channels.*;
import java.util.ArrayList;
import java.util.Collection;
public abstract class Server extends Thread implements Closeable, Runnable {
private final Selector selector;
private final Collection<ServerSocketChannel> server;
public Server() throws IOException {
this.selector = Selector.open();
this.server = new ArrayList<>();
}
public abstract SelectionConsumer createConnectionHandler();
public void bind(SocketAddress address) throws IOException {
final ServerSocketChannel server = ServerSocketChannel.open();
server.configureBlocking(false);
server.register(selector, SelectionKey.OP_ACCEPT, (SelectionConsumer) this::accept);
server.bind(address);
this.server.add(server);
Logger.global.logInfo("WebServer bound to: " + server.getLocalAddress());
}
@Override
public void run() {
Logger.global.logInfo("WebServer started.");
while (this.selector.isOpen()) {
try {
this.selector.select(this::selection);
} catch (IOException e) {
Logger.global.logDebug("Failed to select channel: " + e);
} catch (ClosedSelectorException ignore) {}
}
}
private void selection(SelectionKey selectionKey) {
Object attachment = selectionKey.attachment();
if (attachment instanceof SelectionConsumer) {
((SelectionConsumer) attachment).accept(selectionKey);
}
}
private void accept(SelectionKey selectionKey) {
try {
//noinspection resource
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
SocketChannel channel = serverSocketChannel.accept();
if (channel == null) return;
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE, createConnectionHandler());
} catch (IOException e) {
Logger.global.logDebug("Failed to accept connection: " + e);
}
}
@Override
public void close() throws IOException {
IOException exception = null;
try {
this.selector.close();
this.selector.wakeup();
} catch (IOException ex) {
exception = ex;
}
for (ServerSocketChannel server : this.server) {
try {
server.close();
} catch (IOException ex) {
if (exception == null) exception = ex;
else exception.addSuppressed(ex);
}
}
if (exception != null) throw exception;
}
}

View File

@ -1,153 +0,0 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.common.webserver;
import de.bluecolored.bluemap.core.logger.Logger;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class HttpConnection implements Runnable {
private final HttpRequestHandler handler;
private final ServerSocket server;
private final Socket connection;
private final InputStream in;
private final OutputStream out;
private final Semaphore processingSemaphore;
private final boolean verbose;
public HttpConnection(ServerSocket server, Socket connection, HttpRequestHandler handler, Semaphore processingSemaphore, int timeout, TimeUnit timeoutUnit, boolean verbose) throws IOException {
this.server = server;
this.connection = connection;
this.handler = handler;
this.verbose = verbose;
this.processingSemaphore = processingSemaphore;
if (isClosed()){
throw new IOException("Socket already closed!");
}
connection.setSoTimeout((int) timeoutUnit.toMillis(timeout));
in = new BufferedInputStream(this.connection.getInputStream());
out = new BufferedOutputStream(this.connection.getOutputStream());
}
@Override
public void run() {
while (!isClosed() && !server.isClosed()){
try {
HttpRequest request = acceptRequest();
boolean hasPermit = false;
try {
//just slow down processing if limit is reached
hasPermit = processingSemaphore.tryAcquire(1, TimeUnit.SECONDS);
try (HttpResponse response = handler.handle(request)) {
sendResponse(response);
if (verbose) log(request, response);
}
} finally {
if (hasPermit) processingSemaphore.release();
}
} catch (InvalidRequestException e){
try {
sendResponse(new HttpResponse(HttpStatusCode.BAD_REQUEST));
} catch (IOException ignored) {}
break;
} catch (SocketTimeoutException | ConnectionClosedException | SocketException e) {
break;
} catch (IOException e) {
Logger.global.logError("Unexpected error while processing a HttpRequest!", e);
break;
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
try {
close();
} catch (IOException e){
Logger.global.logError("Error while closing HttpConnection!", e);
}
}
private void log(HttpRequest request, HttpResponse response) {
DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
Date date = new Date();
Logger.global.logInfo(
connection.getInetAddress().toString()
+ " [ "
+ dateFormat.format(date)
+ " ] \""
+ request.getMethod()
+ " " + request.getPath()
+ " " + request.getVersion()
+ "\" "
+ response.getStatusCode().toString());
}
private void sendResponse(HttpResponse response) throws IOException {
response.write(out);
out.flush();
}
private HttpRequest acceptRequest() throws ConnectionClosedException, InvalidRequestException, IOException {
return HttpRequest.read(in);
}
public boolean isClosed(){
return !connection.isBound() || connection.isClosed() || !connection.isConnected() || connection.isOutputShutdown() || connection.isInputShutdown();
}
public void close() throws IOException {
connection.close();
}
public static class ConnectionClosedException extends IOException {
private static final long serialVersionUID = 1L;
}
public static class InvalidRequestException extends IOException {
private static final long serialVersionUID = 1L;
}
}

View File

@ -1,95 +0,0 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.common.webserver;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public abstract class HttpRequest {
private String path = null;
private Map<String, String> getParams = null;
private String getParamString = null;
public abstract String getMethod();
public abstract String getAddress();
public abstract String getVersion();
public abstract Map<String, Set<String>> getHeader();
public abstract Map<String, Set<String>> getLowercaseHeader();
public abstract Set<String> getHeader(String key);
public abstract Set<String> getLowercaseHeader(String key);
public String getPath() {
if (path == null) parseAddress();
return path;
}
public Map<String, String> getGETParams() {
if (getParams == null) parseAddress();
return Collections.unmodifiableMap(getParams);
}
public String getGETParamString() {
if (getParamString == null) parseAddress();
return getParamString;
}
protected void parseAddress() {
String address = this.getAddress();
if (address.isEmpty()) address = "/";
String[] addressParts = address.split("\\?", 2);
String path = addressParts[0];
this.getParamString = addressParts.length > 1 ? addressParts[1] : "";
Map<String, String> getParams = new HashMap<>();
for (String getParam : this.getParamString.split("&")){
if (getParam.isEmpty()) continue;
String[] kv = getParam.split("=", 2);
String key = kv[0];
String value = kv.length > 1 ? kv[1] : "";
getParams.put(key, value);
}
this.path = path;
this.getParams = getParams;
}
public abstract InputStream getData();
static HttpRequest read(InputStream in) throws IOException {
return OriginalHttpRequest.read(in);
}
}

View File

@ -1,139 +0,0 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.common.webserver;
import org.apache.commons.lang3.StringUtils;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.Map.Entry;
public class HttpResponse implements Closeable {
private String version;
private HttpStatusCode statusCode;
private Map<String, Set<String>> header;
private InputStream data;
public HttpResponse(HttpStatusCode statusCode) {
this.version = "HTTP/1.1";
this.statusCode = statusCode;
this.header = new HashMap<>();
addHeader("Connection", "keep-alive");
}
public void addHeader(String key, String value){
Set<String> valueSet = header.computeIfAbsent(key, k -> new HashSet<>());
valueSet.add(value);
}
public void removeHeader(String key, String value){
Set<String> valueSet = header.computeIfAbsent(key, k -> new HashSet<>());
valueSet.remove(value);
}
public void setData(InputStream dataStream){
this.data = dataStream;
}
public void setData(String data){
setData(new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)));
}
public boolean hasData() {
return this.data != null;
}
/**
* Writes this Response to an Output-Stream.<br>
* <br>
* This method closes the data-Stream of this response so it can't be used again!
*/
public void write(OutputStream out) throws IOException {
OutputStreamWriter writer = new OutputStreamWriter(out, StandardCharsets.UTF_8);
if (hasData()){
addHeader("Transfer-Encoding", "chunked");
} else {
addHeader("Content-Length", "0");
}
writeLine(writer, version + " " + statusCode.getCode() + " " + statusCode.getMessage());
for (Entry<String, Set<String>> e : header.entrySet()){
if (e.getValue().isEmpty()) continue;
writeLine(writer, e.getKey() + ": " + StringUtils.join(e.getValue(), ", "));
}
writeLine(writer, "");
writer.flush();
if(hasData()){
chunkedPipe(data, out);
out.flush();
}
}
@Override
public void close() throws IOException {
if (data != null) data.close();
}
private void writeLine(OutputStreamWriter writer, String line) throws IOException {
writer.write(line + "\r\n");
}
private void chunkedPipe(InputStream input, OutputStream output) throws IOException {
byte[] buffer = new byte[1024];
int byteCount;
while ((byteCount = input.read(buffer)) != -1) {
output.write((Integer.toHexString(byteCount) + "\r\n").getBytes());
output.write(buffer, 0, byteCount);
output.write("\r\n".getBytes());
}
output.write("0\r\n\r\n".getBytes());
}
public HttpStatusCode getStatusCode(){
return statusCode;
}
public String getVersion(){
return version;
}
public Map<String, Set<String>> getHeader() {
return header;
}
public Set<String> getHeader(String key){
Set<String> headerValues = header.get(key);
if (headerValues == null) return Collections.emptySet();
return headerValues;
}
}

View File

@ -1,202 +0,0 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.common.webserver;
import de.bluecolored.bluemap.common.webserver.HttpConnection.ConnectionClosedException;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class OriginalHttpRequest extends HttpRequest {
private static final Pattern REQUEST_PATTERN = Pattern.compile("^(\\w+) (\\S+) (.+)$");
private final String method;
private final String address;
private final String version;
private final Map<String, Set<String>> header;
private final Map<String, Set<String>> headerLC;
private byte[] data;
public OriginalHttpRequest(String method, String address, String version, Map<String, Set<String>> header) {
this.method = method;
this.address = address;
this.version = version;
this.header = header;
this.headerLC = new HashMap<>();
for (Entry<String, Set<String>> e : header.entrySet()){
Set<String> values = new HashSet<>();
for (String v : e.getValue()){
values.add(v.toLowerCase());
}
headerLC.put(e.getKey().toLowerCase(), values);
}
this.data = new byte[0];
}
@Override
public String getMethod() {
return method;
}
@Override
public String getAddress(){
return address;
}
@Override
public String getVersion() {
return version;
}
@Override
public Map<String, Set<String>> getHeader() {
return header;
}
@Override
public Map<String, Set<String>> getLowercaseHeader() {
return headerLC;
}
@Override
public Set<String> getHeader(String key){
Set<String> headerValues = header.get(key);
if (headerValues == null) return Collections.emptySet();
return headerValues;
}
@Override
public Set<String> getLowercaseHeader(String key){
Set<String> headerValues = headerLC.get(key.toLowerCase());
if (headerValues == null) return Collections.emptySet();
return headerValues;
}
@Override
public InputStream getData(){
return new ByteArrayInputStream(data);
}
private static String readLine(BufferedReader in) throws IOException {
String line = in.readLine();
if (line == null){
throw new ConnectionClosedException();
}
return line;
}
public static HttpRequest read(InputStream in) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
List<String> header = new ArrayList<>(20);
while (header.size() < 1000) {
String headerLine = OriginalHttpRequest.readLine(reader);
if (headerLine.isEmpty()) break;
header.add(headerLine);
}
if (header.isEmpty()) throw new HttpConnection.InvalidRequestException();
Matcher m = OriginalHttpRequest.REQUEST_PATTERN.matcher(header.remove(0));
if (!m.find()) throw new HttpConnection.InvalidRequestException();
String method = m.group(1);
if (method == null) throw new HttpConnection.InvalidRequestException();
String address = m.group(2);
if (address == null) throw new HttpConnection.InvalidRequestException();
String version = m.group(3);
if (version == null) throw new HttpConnection.InvalidRequestException();
Map<String, Set<String>> headerMap = new HashMap<>();
for (String line : header) {
if (line.trim().isEmpty()) continue;
String[] kv = line.split(":", 2);
if (kv.length < 2) continue;
Set<String> values = new HashSet<>();
if (kv[0].trim().equalsIgnoreCase("If-Modified-Since")) {
values.add(kv[1].trim());
} else {
for (String v : kv[1].split(",")) {
values.add(v.trim());
}
}
headerMap.put(kv[0].trim(), values);
}
OriginalHttpRequest request = new OriginalHttpRequest(method, address, version, headerMap);
if (request.getLowercaseHeader("Transfer-Encoding").contains("chunked")) {
try {
ByteArrayOutputStream dataStream = new ByteArrayOutputStream();
while (dataStream.size() < 1000000) {
String hexSize = reader.readLine();
int chunkSize = Integer.parseInt(hexSize, 16);
if (chunkSize <= 0) break;
byte[] data = new byte[chunkSize];
in.read(data);
dataStream.write(data);
}
if (dataStream.size() >= 1000000) {
throw new HttpConnection.InvalidRequestException();
}
request.data = dataStream.toByteArray();
return request;
} catch (NumberFormatException ex) {
return request;
}
} else {
Set<String> clSet = request.getLowercaseHeader("Content-Length");
if (clSet.isEmpty()) {
return request;
} else {
try {
int cl = Integer.parseInt(clSet.iterator().next());
byte[] data = new byte[cl];
in.read(data);
request.data = data;
return request;
} catch (NumberFormatException ex) {
return request;
}
}
}
}
}

View File

@ -1,173 +0,0 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.common.webserver;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class RewrittenHttpRequest extends HttpRequest {
private final HttpRequest originalRequest;
private String method = null;
private String address = null;
private String version = null;
private Map<String, Set<String>> header = null;
private Map<String, Set<String>> headerLC = null;
private byte[] data = null;
public RewrittenHttpRequest(HttpRequest originalRequest) {
this.originalRequest = originalRequest;
}
public void setMethod(String method) {
this.method = method;
}
@Override
public String getMethod() {
return method != null ? method : originalRequest.getMethod();
}
public void setAddress(String address) {
this.address = address;
parseAddress();
}
@Override
public String getAddress() {
return address != null ? address : originalRequest.getAddress();
}
public void setVersion(String version) {
this.version = version;
}
@Override
public String getVersion() {
return version != null ? version : originalRequest.getVersion();
}
public void setHeader(Map<String, Set<String>> header) {
this.header = header;
this.headerLC = new HashMap<>();
for (Map.Entry<String, Set<String>> e : header.entrySet()){
Set<String> values = new HashSet<>();
for (String v : e.getValue()){
values.add(v.toLowerCase());
}
headerLC.put(e.getKey().toLowerCase(), values);
}
}
@Override
public Map<String, Set<String>> getHeader() {
return header != null ? header : originalRequest.getHeader();
}
@Override
public Map<String, Set<String>> getLowercaseHeader() {
return headerLC != null ? headerLC : originalRequest.getLowercaseHeader();
}
public void setHeader(String key, String value) {
if (header == null || headerLC == null) {
header = new HashMap<>(originalRequest.getHeader());
headerLC = new HashMap<>(originalRequest.getLowercaseHeader());
}
header.computeIfAbsent(key, k -> new HashSet<>()).add(value);
headerLC.computeIfAbsent(key.toLowerCase(), k -> new HashSet<>()).add(value.toLowerCase());
}
public void removeHeader(String key) {
if (header == null || headerLC == null) {
header = new HashMap<>(originalRequest.getHeader());
headerLC = new HashMap<>(originalRequest.getLowercaseHeader());
}
header.remove(key);
headerLC.remove(key.toLowerCase());
}
@Override
public Set<String> getHeader(String key) {
return header != null ? header.get(key) : originalRequest.getHeader(key);
}
@Override
public Set<String> getLowercaseHeader(String key) {
return headerLC != null ? headerLC.get(key.toLowerCase()) : originalRequest.getLowercaseHeader(key);
}
public void setPath(String path) {
if (getGETParamString().isEmpty()) this.address = path;
else this.address = path + "?" + getGETParamString();
parseAddress();
}
@Override
public String getPath() {
if (address == null) return originalRequest.getPath();
return super.getPath();
}
@Override
public Map<String, String> getGETParams() {
if (address == null) return originalRequest.getGETParams();
return super.getGETParams();
}
public void setGETParamString(String paramString) {
this.address = getAddress() + "?" + paramString;
parseAddress();
}
@Override
public String getGETParamString() {
if (address == null) return originalRequest.getGETParamString();
return super.getGETParamString();
}
public void setData(byte[] data) {
this.data = data;
}
@Override
public InputStream getData() {
return data != null ? new ByteArrayInputStream(data) : originalRequest.getData();
}
public HttpRequest getOriginalRequest() {
return originalRequest;
}
}

View File

@ -1,135 +0,0 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.common.webserver;
import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.logger.Logger;
import java.io.IOException;
import java.net.*;
import java.util.concurrent.*;
@DebugDump
public class WebServer extends Thread {
private final int port;
private final int maxConnections;
private final InetAddress bindAddress;
private final boolean verbose;
private final HttpRequestHandler handler;
private final Semaphore processingSemaphore;
private ThreadPoolExecutor connectionThreads;
private ServerSocket server;
public WebServer(InetAddress bindAddress, int port, int maxConnections, HttpRequestHandler handler) {
this(bindAddress, port, maxConnections, handler, false);
}
public WebServer(InetAddress bindAddress, int port, int maxConnections, HttpRequestHandler handler, boolean verbose) {
this.port = port;
this.maxConnections = maxConnections;
this.bindAddress = bindAddress;
this.verbose = verbose;
this.handler = handler;
this.processingSemaphore = new Semaphore(24);
connectionThreads = null;
}
@Override
public synchronized void start() {
close();
connectionThreads = new ThreadPoolExecutor(Math.min(maxConnections, 8), maxConnections, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
try {
server = new ServerSocket(port, maxConnections, bindAddress);
server.setSoTimeout(1000);
} catch (IOException e){
Logger.global.logError("Error while starting the WebServer!", e);
return;
}
super.start();
}
@Override
public void run(){
if (server == null) return;
Logger.global.logInfo("WebServer bound to: " + server.getLocalSocketAddress());
Logger.global.logInfo("WebServer started.");
while (!server.isClosed() && server.isBound()){
try {
Socket connection = server.accept();
try {
connectionThreads.execute(new HttpConnection(server, connection, handler, processingSemaphore, 10, TimeUnit.SECONDS, verbose));
} catch (RejectedExecutionException e){
connection.close();
Logger.global.logWarning("Dropped an incoming HttpConnection! (Too many connections?)");
}
} catch (SocketTimeoutException ignore) {
// will be thrown regularly if no connection is coming in
} catch (SocketException ignore){
// this mainly occurs if the socket got closed, so we ignore this error
} catch (IOException e){
Logger.global.logError("Error while creating a new HttpConnection!", e);
}
}
Logger.global.logInfo("WebServer closed.");
}
public synchronized void close(){
try {
if (server != null && !server.isClosed()){
server.close();
}
} catch (IOException e) {
Logger.global.logError("Error while closing WebServer!", e);
}
if (connectionThreads != null) {
connectionThreads.shutdown();
try {
if (!connectionThreads.awaitTermination(10, TimeUnit.SECONDS)) {
Logger.global.logWarning("Webserver connections didn't close after 10 seconds!");
}
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
}

View File

@ -16,7 +16,3 @@ webroot: "${webroot}"
# The port that the webserver listens to.
# Default is 8100
port: 8100
# Max number of simultaneous connections that the webserver allows
# Default is 100
max-connection-count: 100

View File

@ -38,10 +38,8 @@ import de.bluecolored.bluemap.common.serverinterface.Player;
import de.bluecolored.bluemap.common.serverinterface.ServerEventListener;
import de.bluecolored.bluemap.common.serverinterface.ServerInterface;
import de.bluecolored.bluemap.common.serverinterface.ServerWorld;
import de.bluecolored.bluemap.common.web.FileRequestHandler;
import de.bluecolored.bluemap.common.web.MapRequestHandler;
import de.bluecolored.bluemap.common.web.RoutingRequestHandler;
import de.bluecolored.bluemap.common.webserver.WebServer;
import de.bluecolored.bluemap.common.web.*;
import de.bluecolored.bluemap.common.web.http.HttpRequestHandler;
import de.bluecolored.bluemap.core.MinecraftVersion;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.logger.LoggerLogger;
@ -54,6 +52,7 @@ import org.apache.commons.lang3.time.DurationFormatUtils;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.TimeUnit;
@ -200,13 +199,14 @@ public class BlueMapCLI implements ServerInterface {
);
}
WebServer webServer = new WebServer(
HttpRequestHandler handler = new BlueMapResponseModifier(routingRequestHandler);
if (verbose) handler = new LoggingRequestHandler(handler);
WebServer webServer = new WebServer(handler);
webServer.bind(new InetSocketAddress(
config.resolveIp(),
config.getPort(),
config.getMaxConnectionCount(),
routingRequestHandler,
verbose
);
config.getPort()
));
webServer.start();
}