diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/config/WebserverConfig.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/config/WebserverConfig.java index 22cebff7..20631ca3 100644 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/config/WebserverConfig.java +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/config/WebserverConfig.java @@ -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; - } - } diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/Plugin.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/Plugin.java index af2b71ad..3a63688d 100644 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/Plugin.java +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/plugin/Plugin.java @@ -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 diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/BlueMapResponseModifier.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/BlueMapResponseModifier.java index d82b0c5e..c11d07f5 100644 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/BlueMapResponseModifier.java +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/BlueMapResponseModifier.java @@ -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 diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/FileRequestHandler.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/FileRequestHandler.java index 8fd771f5..0373ed5a 100644 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/FileRequestHandler.java +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/FileRequestHandler.java @@ -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 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 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); } } diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/JsonDataRequestHandler.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/JsonDataRequestHandler.java index 30403bc0..dcde43f0 100644 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/JsonDataRequestHandler.java +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/JsonDataRequestHandler.java @@ -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; diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/LoggingRequestHandler.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/LoggingRequestHandler.java new file mode 100644 index 00000000..aea4f645 --- /dev/null +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/LoggingRequestHandler.java @@ -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; + } + +} diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/MapStorageRequestHandler.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/MapStorageRequestHandler.java index 6da44346..803281da 100644 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/MapStorageRequestHandler.java +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/MapStorageRequestHandler.java @@ -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 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 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(); diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/RoutingRequestHandler.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/RoutingRequestHandler.java index 36c2bd05..30eeadb5 100644 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/RoutingRequestHandler.java +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/RoutingRequestHandler.java @@ -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); } } diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/WebServer.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/WebServer.java new file mode 100644 index 00000000..fcfda292 --- /dev/null +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/WebServer.java @@ -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); + } + +} diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/http/HttpConnection.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/http/HttpConnection.java new file mode 100644 index 00000000..3a4a7ba8 --- /dev/null +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/http/HttpConnection.java @@ -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(); + } + } + +} diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/http/HttpHeader.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/http/HttpHeader.java new file mode 100644 index 00000000..b04c80c6 --- /dev/null +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/http/HttpHeader.java @@ -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 values; + private Set 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 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); + } + +} diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/http/HttpRequest.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/http/HttpRequest.java new file mode 100644 index 00000000..32221c16 --- /dev/null +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/http/HttpRequest.java @@ -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 headerLines = new ArrayList<>(20); + + // request data + private final InetAddress source; + private String method, address, version; + private final Map headers = new HashMap<>(); + private byte[] data; + + // lazy parsed + private String path = null; + private String getParamString = null; + private Map 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 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 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 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; + } + +} diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/webserver/HttpRequestHandler.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/http/HttpRequestHandler.java similarity index 96% rename from BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/webserver/HttpRequestHandler.java rename to BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/http/HttpRequestHandler.java index d0fd0823..39ad4abd 100644 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/webserver/HttpRequestHandler.java +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/http/HttpRequestHandler.java @@ -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 { diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/http/HttpResponse.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/http/HttpResponse.java new file mode 100644 index 00000000..3303e738 --- /dev/null +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/http/HttpResponse.java @@ -0,0 +1,210 @@ +/* + * This file is part of BlueMap, licensed under the MIT License (MIT). + * + * Copyright (c) Blue (Lukas Rieger) + * 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 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 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); + } + +} diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/webserver/HttpStatusCode.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/http/HttpStatusCode.java similarity index 97% rename from BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/webserver/HttpStatusCode.java rename to BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/http/HttpStatusCode.java index cc504e79..3d7ba7e5 100644 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/webserver/HttpStatusCode.java +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/http/HttpStatusCode.java @@ -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 { diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/http/SelectionConsumer.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/http/SelectionConsumer.java new file mode 100644 index 00000000..af09c95e --- /dev/null +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/http/SelectionConsumer.java @@ -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 {} diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/http/Server.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/http/Server.java new file mode 100644 index 00000000..c7adfb13 --- /dev/null +++ b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/web/http/Server.java @@ -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 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; + } + +} diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/webserver/HttpConnection.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/webserver/HttpConnection.java deleted file mode 100644 index 1e918c7b..00000000 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/webserver/HttpConnection.java +++ /dev/null @@ -1,153 +0,0 @@ -/* - * This file is part of BlueMap, licensed under the MIT License (MIT). - * - * Copyright (c) Blue (Lukas Rieger) - * 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; - } - -} diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/webserver/HttpRequest.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/webserver/HttpRequest.java deleted file mode 100644 index 654a02c4..00000000 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/webserver/HttpRequest.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * This file is part of BlueMap, licensed under the MIT License (MIT). - * - * Copyright (c) Blue (Lukas Rieger) - * 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 getParams = null; - private String getParamString = null; - - public abstract String getMethod(); - - public abstract String getAddress(); - - public abstract String getVersion(); - - public abstract Map> getHeader(); - - public abstract Map> getLowercaseHeader(); - - public abstract Set getHeader(String key); - - public abstract Set getLowercaseHeader(String key); - - public String getPath() { - if (path == null) parseAddress(); - return path; - } - - public Map 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 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); - } - -} diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/webserver/HttpResponse.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/webserver/HttpResponse.java deleted file mode 100644 index 25390f07..00000000 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/webserver/HttpResponse.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * This file is part of BlueMap, licensed under the MIT License (MIT). - * - * Copyright (c) Blue (Lukas Rieger) - * 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> 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 valueSet = header.computeIfAbsent(key, k -> new HashSet<>()); - valueSet.add(value); - } - - public void removeHeader(String key, String value){ - Set 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.
- *
- * 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> 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> getHeader() { - return header; - } - - public Set getHeader(String key){ - Set headerValues = header.get(key); - if (headerValues == null) return Collections.emptySet(); - return headerValues; - } - -} diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/webserver/OriginalHttpRequest.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/webserver/OriginalHttpRequest.java deleted file mode 100644 index 2a0bd728..00000000 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/webserver/OriginalHttpRequest.java +++ /dev/null @@ -1,202 +0,0 @@ -/* - * This file is part of BlueMap, licensed under the MIT License (MIT). - * - * Copyright (c) Blue (Lukas Rieger) - * 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> header; - private final Map> headerLC; - private byte[] data; - - public OriginalHttpRequest(String method, String address, String version, Map> header) { - this.method = method; - this.address = address; - this.version = version; - this.header = header; - this.headerLC = new HashMap<>(); - - for (Entry> e : header.entrySet()){ - Set 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> getHeader() { - return header; - } - - @Override - public Map> getLowercaseHeader() { - return headerLC; - } - - @Override - public Set getHeader(String key){ - Set headerValues = header.get(key); - if (headerValues == null) return Collections.emptySet(); - return headerValues; - } - - @Override - public Set getLowercaseHeader(String key){ - Set 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 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> headerMap = new HashMap<>(); - for (String line : header) { - if (line.trim().isEmpty()) continue; - - String[] kv = line.split(":", 2); - if (kv.length < 2) continue; - - Set 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 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; - } - } - } - } - -} diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/webserver/RewrittenHttpRequest.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/webserver/RewrittenHttpRequest.java deleted file mode 100644 index 6b2f6db6..00000000 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/webserver/RewrittenHttpRequest.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * This file is part of BlueMap, licensed under the MIT License (MIT). - * - * Copyright (c) Blue (Lukas Rieger) - * 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> header = null; - private Map> 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> header) { - this.header = header; - this.headerLC = new HashMap<>(); - for (Map.Entry> e : header.entrySet()){ - Set values = new HashSet<>(); - for (String v : e.getValue()){ - values.add(v.toLowerCase()); - } - - headerLC.put(e.getKey().toLowerCase(), values); - } - } - - @Override - public Map> getHeader() { - return header != null ? header : originalRequest.getHeader(); - } - - @Override - public Map> 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 getHeader(String key) { - return header != null ? header.get(key) : originalRequest.getHeader(key); - } - - @Override - public Set 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 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; - } - -} diff --git a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/webserver/WebServer.java b/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/webserver/WebServer.java deleted file mode 100644 index 9198098f..00000000 --- a/BlueMapCommon/src/main/java/de/bluecolored/bluemap/common/webserver/WebServer.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * This file is part of BlueMap, licensed under the MIT License (MIT). - * - * Copyright (c) Blue (Lukas Rieger) - * 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(); - } - } - } - -} diff --git a/BlueMapCommon/src/main/resources/de/bluecolored/bluemap/config/webserver.conf b/BlueMapCommon/src/main/resources/de/bluecolored/bluemap/config/webserver.conf index 1c74a2cc..5332e952 100644 --- a/BlueMapCommon/src/main/resources/de/bluecolored/bluemap/config/webserver.conf +++ b/BlueMapCommon/src/main/resources/de/bluecolored/bluemap/config/webserver.conf @@ -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 diff --git a/implementations/cli/src/main/java/de/bluecolored/bluemap/cli/BlueMapCLI.java b/implementations/cli/src/main/java/de/bluecolored/bluemap/cli/BlueMapCLI.java index 0bd0e116..57643ad8 100644 --- a/implementations/cli/src/main/java/de/bluecolored/bluemap/cli/BlueMapCLI.java +++ b/implementations/cli/src/main/java/de/bluecolored/bluemap/cli/BlueMapCLI.java @@ -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(); }