Rewrite integrated webserver to be non-blocking
This commit is contained in:
parent
9917e5dfa5
commit
f4c6adc685
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {
|
||||
|
|
@ -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> {}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue