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 port = 8100;
|
||||||
|
|
||||||
private int maxConnectionCount = 100;
|
|
||||||
|
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
return enabled;
|
return enabled;
|
||||||
}
|
}
|
||||||
|
@ -73,8 +71,4 @@ public class WebserverConfig {
|
||||||
return port;
|
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.rendermanager.RenderManager;
|
||||||
import de.bluecolored.bluemap.common.serverinterface.ServerEventListener;
|
import de.bluecolored.bluemap.common.serverinterface.ServerEventListener;
|
||||||
import de.bluecolored.bluemap.common.serverinterface.ServerInterface;
|
import de.bluecolored.bluemap.common.serverinterface.ServerInterface;
|
||||||
import de.bluecolored.bluemap.common.web.FileRequestHandler;
|
import de.bluecolored.bluemap.common.web.*;
|
||||||
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.core.debug.StateDumper;
|
import de.bluecolored.bluemap.core.debug.StateDumper;
|
||||||
import de.bluecolored.bluemap.core.logger.Logger;
|
import de.bluecolored.bluemap.core.logger.Logger;
|
||||||
import de.bluecolored.bluemap.core.map.BmMap;
|
import de.bluecolored.bluemap.core.map.BmMap;
|
||||||
|
@ -57,6 +54,7 @@ import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.OutputStreamWriter;
|
import java.io.OutputStreamWriter;
|
||||||
import java.io.Writer;
|
import java.io.Writer;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
@ -189,23 +187,26 @@ public class Plugin implements ServerEventListener {
|
||||||
routingRequestHandler.register(
|
routingRequestHandler.register(
|
||||||
"maps/" + Pattern.quote(id) + "/(.*)",
|
"maps/" + Pattern.quote(id) + "/(.*)",
|
||||||
"$1",
|
"$1",
|
||||||
mapRequestHandler
|
new BlueMapResponseModifier(mapRequestHandler)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
webServer = new WebServer(routingRequestHandler);
|
||||||
|
webServer.start();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
webServer = new WebServer(
|
webServer.bind(new InetSocketAddress(
|
||||||
webserverConfig.resolveIp(),
|
webserverConfig.resolveIp(),
|
||||||
webserverConfig.getPort(),
|
webserverConfig.getPort()
|
||||||
webserverConfig.getMaxConnectionCount(),
|
));
|
||||||
routingRequestHandler,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
} catch (UnknownHostException ex) {
|
} catch (UnknownHostException ex) {
|
||||||
throw new ConfigurationException("BlueMap failed to resolve the ip in your webserver-config.\n" +
|
throw new ConfigurationException("BlueMap failed to resolve the ip in your webserver-config.\n" +
|
||||||
"Check if that is correctly configured.", ex);
|
"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
|
//initialize render manager
|
||||||
|
@ -376,7 +377,13 @@ public class Plugin implements ServerEventListener {
|
||||||
}
|
}
|
||||||
renderManager = null;
|
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;
|
webServer = null;
|
||||||
|
|
||||||
//close bluemap
|
//close bluemap
|
||||||
|
|
|
@ -24,10 +24,10 @@
|
||||||
*/
|
*/
|
||||||
package de.bluecolored.bluemap.common.web;
|
package de.bluecolored.bluemap.common.web;
|
||||||
|
|
||||||
import de.bluecolored.bluemap.common.webserver.HttpRequest;
|
import de.bluecolored.bluemap.common.web.http.HttpRequest;
|
||||||
import de.bluecolored.bluemap.common.webserver.HttpRequestHandler;
|
import de.bluecolored.bluemap.common.web.http.HttpRequestHandler;
|
||||||
import de.bluecolored.bluemap.common.webserver.HttpResponse;
|
import de.bluecolored.bluemap.common.web.http.HttpResponse;
|
||||||
import de.bluecolored.bluemap.common.webserver.HttpStatusCode;
|
import de.bluecolored.bluemap.common.web.http.HttpStatusCode;
|
||||||
import de.bluecolored.bluemap.core.BlueMap;
|
import de.bluecolored.bluemap.core.BlueMap;
|
||||||
|
|
||||||
public class BlueMapResponseModifier implements HttpRequestHandler {
|
public class BlueMapResponseModifier implements HttpRequestHandler {
|
||||||
|
@ -37,7 +37,7 @@ public class BlueMapResponseModifier implements HttpRequestHandler {
|
||||||
|
|
||||||
public BlueMapResponseModifier(HttpRequestHandler delegate) {
|
public BlueMapResponseModifier(HttpRequestHandler delegate) {
|
||||||
this.delegate = delegate;
|
this.delegate = delegate;
|
||||||
this.serverName = "BlueMap " + BlueMap.VERSION + " " + BlueMap.GIT_HASH;
|
this.serverName = "BlueMap/" + BlueMap.VERSION;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -24,10 +24,7 @@
|
||||||
*/
|
*/
|
||||||
package de.bluecolored.bluemap.common.web;
|
package de.bluecolored.bluemap.common.web;
|
||||||
|
|
||||||
import de.bluecolored.bluemap.common.webserver.HttpRequest;
|
import de.bluecolored.bluemap.common.web.http.*;
|
||||||
import de.bluecolored.bluemap.common.webserver.HttpRequestHandler;
|
|
||||||
import de.bluecolored.bluemap.common.webserver.HttpResponse;
|
|
||||||
import de.bluecolored.bluemap.common.webserver.HttpStatusCode;
|
|
||||||
import org.apache.commons.lang3.time.DateFormatUtils;
|
import org.apache.commons.lang3.time.DateFormatUtils;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
@ -35,7 +32,10 @@ import java.io.FileInputStream;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.nio.file.InvalidPathException;
|
import java.nio.file.InvalidPathException;
|
||||||
import java.nio.file.Path;
|
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;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public class FileRequestHandler implements HttpRequestHandler {
|
public class FileRequestHandler implements HttpRequestHandler {
|
||||||
|
@ -50,13 +50,9 @@ public class FileRequestHandler implements HttpRequestHandler {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HttpResponse handle(HttpRequest request) {
|
public HttpResponse handle(HttpRequest request) {
|
||||||
if (
|
if (!request.getMethod().equalsIgnoreCase("GET"))
|
||||||
!request.getMethod().equalsIgnoreCase("GET")
|
return new HttpResponse(HttpStatusCode.BAD_REQUEST);
|
||||||
) return new HttpResponse(HttpStatusCode.BAD_REQUEST);
|
return generateResponse(request);
|
||||||
|
|
||||||
HttpResponse response = generateResponse(request);
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private HttpResponse generateResponse(HttpRequest request) {
|
private HttpResponse generateResponse(HttpRequest request) {
|
||||||
|
@ -113,10 +109,10 @@ public class FileRequestHandler implements HttpRequestHandler {
|
||||||
|
|
||||||
// check modified
|
// check modified
|
||||||
long lastModified = file.lastModified();
|
long lastModified = file.lastModified();
|
||||||
Set<String> modStringSet = request.getHeader("If-Modified-Since");
|
HttpHeader modHeader = request.getHeader("If-Modified-Since");
|
||||||
if (!modStringSet.isEmpty()){
|
if (modHeader != null){
|
||||||
try {
|
try {
|
||||||
long since = stringToTimestamp(modStringSet.iterator().next());
|
long since = stringToTimestamp(modHeader.getValue());
|
||||||
if (since + 1000 >= lastModified){
|
if (since + 1000 >= lastModified){
|
||||||
return new HttpResponse(HttpStatusCode.NOT_MODIFIED);
|
return new HttpResponse(HttpStatusCode.NOT_MODIFIED);
|
||||||
}
|
}
|
||||||
|
@ -125,9 +121,9 @@ public class FileRequestHandler implements HttpRequestHandler {
|
||||||
|
|
||||||
//check ETag
|
//check ETag
|
||||||
String eTag = Long.toHexString(file.length()) + Integer.toHexString(file.hashCode()) + Long.toHexString(lastModified);
|
String eTag = Long.toHexString(file.length()) + Integer.toHexString(file.hashCode()) + Long.toHexString(lastModified);
|
||||||
Set<String> etagStringSet = request.getHeader("If-None-Match");
|
HttpHeader etagHeader = request.getHeader("If-None-Match");
|
||||||
if (!etagStringSet.isEmpty()){
|
if (etagHeader != null){
|
||||||
if(etagStringSet.iterator().next().equals(eTag)) {
|
if(etagHeader.getValue().equals(eTag)) {
|
||||||
return new HttpResponse(HttpStatusCode.NOT_MODIFIED);
|
return new HttpResponse(HttpStatusCode.NOT_MODIFIED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,10 +24,10 @@
|
||||||
*/
|
*/
|
||||||
package de.bluecolored.bluemap.common.web;
|
package de.bluecolored.bluemap.common.web;
|
||||||
|
|
||||||
import de.bluecolored.bluemap.common.webserver.HttpRequest;
|
import de.bluecolored.bluemap.common.web.http.HttpRequest;
|
||||||
import de.bluecolored.bluemap.common.webserver.HttpRequestHandler;
|
import de.bluecolored.bluemap.common.web.http.HttpRequestHandler;
|
||||||
import de.bluecolored.bluemap.common.webserver.HttpResponse;
|
import de.bluecolored.bluemap.common.web.http.HttpResponse;
|
||||||
import de.bluecolored.bluemap.common.webserver.HttpStatusCode;
|
import de.bluecolored.bluemap.common.web.http.HttpStatusCode;
|
||||||
import de.bluecolored.bluemap.core.BlueMap;
|
import de.bluecolored.bluemap.core.BlueMap;
|
||||||
|
|
||||||
import java.util.function.Supplier;
|
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 com.flowpowered.math.vector.Vector2i;
|
||||||
import de.bluecolored.bluemap.api.ContentTypeRegistry;
|
import de.bluecolored.bluemap.api.ContentTypeRegistry;
|
||||||
import de.bluecolored.bluemap.common.webserver.HttpRequest;
|
import de.bluecolored.bluemap.common.web.http.*;
|
||||||
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.core.logger.Logger;
|
import de.bluecolored.bluemap.core.logger.Logger;
|
||||||
import de.bluecolored.bluemap.core.map.BmMap;
|
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.io.IOUtils;
|
||||||
import org.apache.commons.lang3.time.DateFormatUtils;
|
import org.apache.commons.lang3.time.DateFormatUtils;
|
||||||
|
|
||||||
|
@ -82,19 +82,19 @@ public class MapStorageRequestHandler implements HttpRequestHandler {
|
||||||
|
|
||||||
// check e-tag
|
// check e-tag
|
||||||
String eTag = calculateETag(path, tileInfo);
|
String eTag = calculateETag(path, tileInfo);
|
||||||
Set<String> etagStringSet = request.getHeader("If-None-Match");
|
HttpHeader etagHeader = request.getHeader("If-None-Match");
|
||||||
if (!etagStringSet.isEmpty()){
|
if (etagHeader != null){
|
||||||
if(etagStringSet.iterator().next().equals(eTag)) {
|
if(etagHeader.getValue().equals(eTag)) {
|
||||||
return new HttpResponse(HttpStatusCode.NOT_MODIFIED);
|
return new HttpResponse(HttpStatusCode.NOT_MODIFIED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// check modified-since
|
// check modified-since
|
||||||
long lastModified = tileInfo.getLastModified();
|
long lastModified = tileInfo.getLastModified();
|
||||||
Set<String> modStringSet = request.getHeader("If-Modified-Since");
|
HttpHeader modHeader = request.getHeader("If-Modified-Since");
|
||||||
if (!modStringSet.isEmpty()){
|
if (modHeader != null){
|
||||||
try {
|
try {
|
||||||
long since = stringToTimestamp(modStringSet.iterator().next());
|
long since = stringToTimestamp(modHeader.getValue());
|
||||||
if (since + 1000 >= lastModified){
|
if (since + 1000 >= lastModified){
|
||||||
return new HttpResponse(HttpStatusCode.NOT_MODIFIED);
|
return new HttpResponse(HttpStatusCode.NOT_MODIFIED);
|
||||||
}
|
}
|
||||||
|
@ -144,14 +144,14 @@ public class MapStorageRequestHandler implements HttpRequestHandler {
|
||||||
Compression compression = data.getCompression();
|
Compression compression = data.getCompression();
|
||||||
if (
|
if (
|
||||||
compression != Compression.NONE &&
|
compression != Compression.NONE &&
|
||||||
request.getLowercaseHeader("Accept-Encoding").contains(compression.getTypeId())
|
request.hasHeaderValue("Accept-Encoding", compression.getTypeId())
|
||||||
) {
|
) {
|
||||||
response.addHeader("Content-Encoding", compression.getTypeId());
|
response.addHeader("Content-Encoding", compression.getTypeId());
|
||||||
response.setData(data);
|
response.setData(data);
|
||||||
} else if (
|
} else if (
|
||||||
compression != Compression.GZIP &&
|
compression != Compression.GZIP &&
|
||||||
!response.getHeader("Content-Type").contains("image/png") &&
|
!response.hasHeaderValue("Content-Type", "image/png") &&
|
||||||
request.getLowercaseHeader("Accept-Encoding").contains(Compression.GZIP.getTypeId())
|
request.hasHeaderValue("Accept-Encoding", Compression.GZIP.getTypeId())
|
||||||
) {
|
) {
|
||||||
response.addHeader("Content-Encoding", Compression.GZIP.getTypeId());
|
response.addHeader("Content-Encoding", Compression.GZIP.getTypeId());
|
||||||
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
|
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
|
||||||
|
|
|
@ -24,7 +24,10 @@
|
||||||
*/
|
*/
|
||||||
package de.bluecolored.bluemap.common.web;
|
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 org.intellij.lang.annotations.Language;
|
||||||
|
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
|
@ -61,14 +64,13 @@ public class RoutingRequestHandler implements HttpRequestHandler {
|
||||||
|
|
||||||
// normalize path
|
// normalize path
|
||||||
if (path.startsWith("/")) path = path.substring(1);
|
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) {
|
for (Route route : routes) {
|
||||||
Matcher matcher = route.getRoutePattern().matcher(path);
|
Matcher matcher = route.getRoutePattern().matcher(path);
|
||||||
if (matcher.matches()) {
|
if (matcher.matches()) {
|
||||||
RewrittenHttpRequest rewrittenRequest = new RewrittenHttpRequest(request);
|
request.setPath(matcher.replaceFirst(route.getReplacementRoute()));
|
||||||
rewrittenRequest.setPath(matcher.replaceFirst(route.getReplacementRoute()));
|
return route.getHandler().handle(request);
|
||||||
return route.handler.handle(rewrittenRequest);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
* THE SOFTWARE.
|
* THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
package de.bluecolored.bluemap.common.webserver;
|
package de.bluecolored.bluemap.common.web.http;
|
||||||
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface HttpRequestHandler {
|
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
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
* THE SOFTWARE.
|
* THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
package de.bluecolored.bluemap.common.webserver;
|
package de.bluecolored.bluemap.common.web.http;
|
||||||
|
|
||||||
public enum HttpStatusCode {
|
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.
|
# The port that the webserver listens to.
|
||||||
# Default is 8100
|
# Default is 8100
|
||||||
port: 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.ServerEventListener;
|
||||||
import de.bluecolored.bluemap.common.serverinterface.ServerInterface;
|
import de.bluecolored.bluemap.common.serverinterface.ServerInterface;
|
||||||
import de.bluecolored.bluemap.common.serverinterface.ServerWorld;
|
import de.bluecolored.bluemap.common.serverinterface.ServerWorld;
|
||||||
import de.bluecolored.bluemap.common.web.FileRequestHandler;
|
import de.bluecolored.bluemap.common.web.*;
|
||||||
import de.bluecolored.bluemap.common.web.MapRequestHandler;
|
import de.bluecolored.bluemap.common.web.http.HttpRequestHandler;
|
||||||
import de.bluecolored.bluemap.common.web.RoutingRequestHandler;
|
|
||||||
import de.bluecolored.bluemap.common.webserver.WebServer;
|
|
||||||
import de.bluecolored.bluemap.core.MinecraftVersion;
|
import de.bluecolored.bluemap.core.MinecraftVersion;
|
||||||
import de.bluecolored.bluemap.core.logger.Logger;
|
import de.bluecolored.bluemap.core.logger.Logger;
|
||||||
import de.bluecolored.bluemap.core.logger.LoggerLogger;
|
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.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.TimeUnit;
|
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.resolveIp(),
|
||||||
config.getPort(),
|
config.getPort()
|
||||||
config.getMaxConnectionCount(),
|
));
|
||||||
routingRequestHandler,
|
|
||||||
verbose
|
|
||||||
);
|
|
||||||
webServer.start();
|
webServer.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue