From 3ffc5dd95d4a3ccbf0566de11b3c27ca932b311d Mon Sep 17 00:00:00 2001 From: Rsl1122 Date: Wed, 13 Sep 2017 18:52:53 +0300 Subject: [PATCH] Restructured WebServer Response serving --- .../api/exceptions/WebUserAuthException.java | 27 ++ .../plan/database/tables/UserInfoTable.java | 5 +- .../plan/systems/cache/GeolocationCache.java | 11 +- .../plan/systems/webapi/WebAPIManager.java | 10 +- .../systems/webserver/APIResponseHandler.java | 122 ++++++ .../plan/systems/webserver/Request.java | 69 +++ .../systems/webserver/RequestHandler.java | 48 +++ .../systems/webserver/ResponseHandler.java | 210 +++++++++ .../plan/systems/webserver/WebServer.java | 399 +----------------- .../webserver/response/CSSResponse.java | 1 + .../response/JavaScriptResponse.java | 1 + .../systems/webserver/response/Response.java | 37 ++ .../webserver/response/ResponseType.java | 27 ++ .../webserver/response/api/JsonResponse.java | 2 + 14 files changed, 581 insertions(+), 388 deletions(-) create mode 100644 Plan/src/main/java/com/djrapitops/plan/api/exceptions/WebUserAuthException.java create mode 100644 Plan/src/main/java/com/djrapitops/plan/systems/webserver/APIResponseHandler.java create mode 100644 Plan/src/main/java/com/djrapitops/plan/systems/webserver/Request.java create mode 100644 Plan/src/main/java/com/djrapitops/plan/systems/webserver/RequestHandler.java create mode 100644 Plan/src/main/java/com/djrapitops/plan/systems/webserver/ResponseHandler.java create mode 100644 Plan/src/main/java/com/djrapitops/plan/systems/webserver/response/ResponseType.java diff --git a/Plan/src/main/java/com/djrapitops/plan/api/exceptions/WebUserAuthException.java b/Plan/src/main/java/com/djrapitops/plan/api/exceptions/WebUserAuthException.java new file mode 100644 index 000000000..6ce13c53a --- /dev/null +++ b/Plan/src/main/java/com/djrapitops/plan/api/exceptions/WebUserAuthException.java @@ -0,0 +1,27 @@ +/* + * Licence is provided in the jar as license.yml also here: + * https://github.com/Rsl1122/Plan-PlayerAnalytics/blob/master/Plan/src/main/resources/license.yml + */ +package main.java.com.djrapitops.plan.api.exceptions; + +/** + * Thrown when WebUser can not be authorized (WebServer). + * + * @author Rsl1122 + */ +public class WebUserAuthException extends Exception { + public WebUserAuthException() { + } + + public WebUserAuthException(String message) { + super(message); + } + + public WebUserAuthException(String message, Throwable cause) { + super(message, cause); + } + + public WebUserAuthException(Throwable cause) { + super(cause); + } +} \ No newline at end of file diff --git a/Plan/src/main/java/com/djrapitops/plan/database/tables/UserInfoTable.java b/Plan/src/main/java/com/djrapitops/plan/database/tables/UserInfoTable.java index ed8337edd..21ca68100 100644 --- a/Plan/src/main/java/com/djrapitops/plan/database/tables/UserInfoTable.java +++ b/Plan/src/main/java/com/djrapitops/plan/database/tables/UserInfoTable.java @@ -185,7 +185,10 @@ public class UserInfoTable extends UserIDTable { boolean banned = set.getBoolean(columnBanned); String name = set.getString("name"); UUID uuid = UUID.fromString(set.getString("uuid")); - userInfo.add(new UserInfo(uuid, name, registered, opped, banned)); + UserInfo info = new UserInfo(uuid, name, registered, opped, banned); + if (!userInfo.contains(info)) { + userInfo.add(info); + } } return userInfo; } finally { diff --git a/Plan/src/main/java/com/djrapitops/plan/systems/cache/GeolocationCache.java b/Plan/src/main/java/com/djrapitops/plan/systems/cache/GeolocationCache.java index 3e4cc48b1..8ec78b442 100644 --- a/Plan/src/main/java/com/djrapitops/plan/systems/cache/GeolocationCache.java +++ b/Plan/src/main/java/com/djrapitops/plan/systems/cache/GeolocationCache.java @@ -6,7 +6,7 @@ import com.maxmind.geoip2.DatabaseReader; import com.maxmind.geoip2.exception.GeoIp2Exception; import com.maxmind.geoip2.model.CountryResponse; import com.maxmind.geoip2.record.Country; -import main.java.com.djrapitops.plan.Plan; +import main.java.com.djrapitops.plan.utilities.MiscUtils; import java.io.File; import java.io.FileOutputStream; @@ -29,7 +29,7 @@ import java.util.zip.GZIPInputStream; */ public class GeolocationCache { - private static File geolocationDB = new File(Plan.getInstance().getDataFolder(), "GeoIP.dat"); + private static File geolocationDB = new File(MiscUtils.getIPlan().getDataFolder(), "GeoIP.dat"); private static final Cache geolocationCache = CacheBuilder.newBuilder() .build(); @@ -51,7 +51,7 @@ public class GeolocationCache { *

* An exception from that rule is when the country is unknown or the retrieval of the country failed in any way, * if that happens, "Not Known" will be returned. - * @see #getUncachedCountry(String) + * @see #getUnCachedCountry(String) */ public static String getCountry(String ipAddress) { String country = getCachedCountry(ipAddress); @@ -59,7 +59,7 @@ public class GeolocationCache { if (country != null) { return country; } else { - country = getUncachedCountry(ipAddress); + country = getUnCachedCountry(ipAddress); geolocationCache.put(ipAddress, country); return country; @@ -71,6 +71,7 @@ public class GeolocationCache { *

* This product includes GeoLite2 data created by MaxMind, available from * http://www.maxmind.com. + * * @param ipAddress The IP Address from which the country is retrieved * @return The name of the country in full length. *

@@ -79,7 +80,7 @@ public class GeolocationCache { * @see http://maxmind.com * @see #getCountry(String) */ - private static String getUncachedCountry(String ipAddress) { + private static String getUnCachedCountry(String ipAddress) { try { checkDB(); diff --git a/Plan/src/main/java/com/djrapitops/plan/systems/webapi/WebAPIManager.java b/Plan/src/main/java/com/djrapitops/plan/systems/webapi/WebAPIManager.java index ffce6e77d..79928c6c0 100644 --- a/Plan/src/main/java/com/djrapitops/plan/systems/webapi/WebAPIManager.java +++ b/Plan/src/main/java/com/djrapitops/plan/systems/webapi/WebAPIManager.java @@ -12,20 +12,20 @@ import java.util.Map; */ public class WebAPIManager { - private static final Map registry = new HashMap<>(); + private final Map registry; /** * Constructor used to hide the public constructor */ - private WebAPIManager() { - throw new IllegalStateException("Utility class"); + public WebAPIManager() { + registry = new HashMap<>(); } - public static void registerNewAPI(String method, WebAPI api) { + public void registerNewAPI(String method, WebAPI api) { registry.put(method.toLowerCase(), api); } - public static WebAPI getAPI(String method) { + public WebAPI getAPI(String method) { return registry.get(method.toLowerCase()); } } diff --git a/Plan/src/main/java/com/djrapitops/plan/systems/webserver/APIResponseHandler.java b/Plan/src/main/java/com/djrapitops/plan/systems/webserver/APIResponseHandler.java new file mode 100644 index 000000000..5ece41eaf --- /dev/null +++ b/Plan/src/main/java/com/djrapitops/plan/systems/webserver/APIResponseHandler.java @@ -0,0 +1,122 @@ +/* + * Licence is provided in the jar as license.yml also here: + * https://github.com/Rsl1122/Plan-PlayerAnalytics/blob/master/Plan/src/main/resources/license.yml + */ +package main.java.com.djrapitops.plan.systems.webserver; + +import main.java.com.djrapitops.plan.Plan; +import main.java.com.djrapitops.plan.systems.webapi.WebAPI; +import main.java.com.djrapitops.plan.systems.webapi.WebAPIManager; +import main.java.com.djrapitops.plan.systems.webserver.response.ForbiddenResponse; +import main.java.com.djrapitops.plan.systems.webserver.response.Response; +import main.java.com.djrapitops.plan.systems.webserver.response.api.BadRequestResponse; +import main.java.com.djrapitops.plan.utilities.MiscUtils; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * //TODO Class Javadoc Comment + * + * @author Rsl1122 + */ +public class APIResponseHandler { + + private final WebAPIManager webAPI; + + public APIResponseHandler(WebAPIManager webAPI) { + this.webAPI = webAPI; + } + + Response getAPIResponse(Request request) throws IOException { + String target = request.getTarget(); + String[] args = target.split("/"); + + if (args.length < 3) { + String error = "API Method not specified"; + return PageCache.loadPage(error, () -> new BadRequestResponse(error)); + } + + String method = args[2]; + String response = null; + try (InputStream inputStream = request.getRequestBody()) { + response = readPOSTRequest(inputStream); + } + + if (response == null) { + String error = "Error at reading the POST request." + + "Note that the Encoding must be ISO-8859-1."; + return PageCache.loadPage(error, () -> new BadRequestResponse(error)); + } + + Map variables = readVariables(response); + String key = variables.get("key"); + + if (!checkKey(key)) { + String error = "Server Key not given or invalid"; + return PageCache.loadPage(error, () -> { + ForbiddenResponse forbidden = new ForbiddenResponse(); + forbidden.setContent(error); + return forbidden; + }); + } + + WebAPI api = webAPI.getAPI(method); + + if (api == null) { + String error = "API Method not found"; + return PageCache.loadPage(error, () -> new BadRequestResponse(error)); + } + + return api.onResponse(Plan.getInstance(), variables); + } + + private String readPOSTRequest(InputStream in) throws IOException { + byte[] bytes; + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buf = new byte[4096]; + for (int n = in.read(buf); n > 0; n = in.read(buf)) { + out.write(buf, 0, n); + } + + bytes = out.toByteArray(); + + try { + return new String(bytes, StandardCharsets.ISO_8859_1); + } catch (Exception e) { + return null; + } + } + + private boolean checkKey(String key) { + if (key == null) { + return false; + } + + UUID uuid = MiscUtils.getIPlan().getServerInfoManager().getServerUUID(); + UUID keyUUID; + try { + keyUUID = UUID.fromString(key); + } catch (IllegalArgumentException e) { + return false; + } + + return uuid.equals(keyUUID); + } + + private Map readVariables(String response) { + String[] variables = response.split("&"); + + return Arrays.stream(variables) + .map(variable -> variable.split("=", 2)) + .filter(splittedVariables -> splittedVariables.length == 2) + .collect(Collectors.toMap(splittedVariables -> splittedVariables[0], splittedVariables -> splittedVariables[1], (a, b) -> b)); + } +} \ No newline at end of file diff --git a/Plan/src/main/java/com/djrapitops/plan/systems/webserver/Request.java b/Plan/src/main/java/com/djrapitops/plan/systems/webserver/Request.java new file mode 100644 index 000000000..accce4d59 --- /dev/null +++ b/Plan/src/main/java/com/djrapitops/plan/systems/webserver/Request.java @@ -0,0 +1,69 @@ +/* + * Licence is provided in the jar as license.yml also here: + * https://github.com/Rsl1122/Plan-PlayerAnalytics/blob/master/Plan/src/main/resources/license.yml + */ +package main.java.com.djrapitops.plan.systems.webserver; + +import com.djrapitops.plugin.utilities.Verify; +import com.sun.net.httpserver.Headers; +import com.sun.net.httpserver.HttpExchange; + +import java.io.InputStream; +import java.util.List; + +/** + * //TODO Class Javadoc Comment + * + * @author Rsl1122 + */ +public class Request { + private String auth; + private final String requestMethod; + private final String target; + + private final HttpExchange exchange; + + public Request(HttpExchange exchange) { + this.requestMethod = exchange.getRequestMethod(); + this.target = exchange.getRequestURI().toString(); + + this.exchange = exchange; + setAuth(exchange.getRequestHeaders()); + } + + public String getAuth() { + return auth; + } + + public void setAuth(Headers requestHeaders) { + List authorization = requestHeaders.get("Authorization"); + if (Verify.isEmpty(authorization)) { + return; + } + + String authLine = authorization.get(0); + if (authLine.contains("Basic ")) { + auth = authLine.split(" ")[1]; + } + } + + public boolean hasAuth() { + return auth != null; + } + + public String getRequestMethod() { + return requestMethod; + } + + public String getTarget() { + return target; + } + + public boolean isAPIRequest() { + return "POST".equals(requestMethod); + } + + public InputStream getRequestBody() { + return exchange.getRequestBody(); + } +} \ No newline at end of file diff --git a/Plan/src/main/java/com/djrapitops/plan/systems/webserver/RequestHandler.java b/Plan/src/main/java/com/djrapitops/plan/systems/webserver/RequestHandler.java new file mode 100644 index 000000000..fa8cc7fad --- /dev/null +++ b/Plan/src/main/java/com/djrapitops/plan/systems/webserver/RequestHandler.java @@ -0,0 +1,48 @@ +/* + * Licence is provided in the jar as license.yml also here: + * https://github.com/Rsl1122/Plan-PlayerAnalytics/blob/master/Plan/src/main/resources/license.yml + */ +package main.java.com.djrapitops.plan.systems.webserver; + +import com.sun.net.httpserver.Headers; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import main.java.com.djrapitops.plan.api.IPlan; +import main.java.com.djrapitops.plan.systems.webserver.response.PromptAuthorizationResponse; +import main.java.com.djrapitops.plan.systems.webserver.response.Response; + +import java.io.IOException; + +/** + * HttpHandler for webserver request management. + * + * @author Rsl1122 + */ +public class RequestHandler implements HttpHandler { + + + private final ResponseHandler responseHandler; + + RequestHandler(IPlan plugin, WebServer webServer) { + responseHandler = new ResponseHandler(plugin, webServer); + } + + @Override + public void handle(HttpExchange exchange) throws IOException { + Headers responseHeaders = exchange.getResponseHeaders(); + Request request = new Request(exchange); + + try { + Response response = responseHandler.getResponse(request); + if (response instanceof PromptAuthorizationResponse) { + responseHeaders.set("WWW-Authenticate", "Basic realm=\"/\";"); + } + response.setResponseHeaders(responseHeaders); + response.send(exchange); + } finally { + exchange.close(); + } + } + + +} \ No newline at end of file diff --git a/Plan/src/main/java/com/djrapitops/plan/systems/webserver/ResponseHandler.java b/Plan/src/main/java/com/djrapitops/plan/systems/webserver/ResponseHandler.java new file mode 100644 index 000000000..5acb4fe6b --- /dev/null +++ b/Plan/src/main/java/com/djrapitops/plan/systems/webserver/ResponseHandler.java @@ -0,0 +1,210 @@ +/* + * Licence is provided in the jar as license.yml also here: + * https://github.com/Rsl1122/Plan-PlayerAnalytics/blob/master/Plan/src/main/resources/license.yml + */ +package main.java.com.djrapitops.plan.systems.webserver; + +import main.java.com.djrapitops.plan.api.IPlan; +import main.java.com.djrapitops.plan.api.exceptions.WebUserAuthException; +import main.java.com.djrapitops.plan.data.WebUser; +import main.java.com.djrapitops.plan.database.tables.SecurityTable; +import main.java.com.djrapitops.plan.systems.webserver.response.*; +import main.java.com.djrapitops.plan.utilities.PassEncryptUtil; +import main.java.com.djrapitops.plan.utilities.html.HtmlUtils; +import main.java.com.djrapitops.plan.utilities.uuid.UUIDUtility; + +import java.sql.SQLException; +import java.util.Base64; +import java.util.UUID; + +/** + * //TODO Class Javadoc Comment + * + * @author Rsl1122 + */ +public class ResponseHandler extends APIResponseHandler { + + private final IPlan plugin; + private final WebServer webServer; + + private final boolean usingHttps; + + public ResponseHandler(IPlan plugin, WebServer webServer) { + super(webServer.getWebAPI()); + this.plugin = plugin; + this.webServer = webServer; + this.usingHttps = webServer.isUsingHTTPS(); + } + + public Response getResponse(Request request) { + String target = request.getTarget(); + String[] args = target.split("/"); + try { + if ("/favicon.ico".equals(target)) { + return PageCache.loadPage("Redirect: favicon", () -> new RedirectResponse("https://puu.sh/tK0KL/6aa2ba141b.ico")); + } + if (request.isAPIRequest()) { + return getAPIResponse(request); + } + if (target.endsWith(".css")) { + return PageCache.loadPage(target + "css", () -> new CSSResponse("main.css")); + } + + if (target.endsWith(".js")) { + String fileName = args[args.length - 1]; + return PageCache.loadPage(target + "js", () -> new JavaScriptResponse(fileName)); + } + if (usingHttps) { + if (!request.hasAuth()) { + throw new WebUserAuthException("No Authorization"); + } + + WebUser user = getUser(request.getAuth()); + int required = getRequiredPermLevel(target, user.getName()); + int permLevel = user.getPermLevel(); + + if (!isAuthorized(required, permLevel)) { + return forbiddenResponse(required, permLevel); + } + if (args.length < 2) { + return rootPageResponse(user); + } + } + + String page = args[1]; + switch (page) { + case "players": + return PageCache.loadPage("players", PlayersPageResponse::new); + case "player": + return playerResponse(args); + case "server": + return serverResponse(); + default: + return notFoundResponse(); + } + + } catch (WebUserAuthException e) { + return PageCache.loadPage("promptAuthorization", PromptAuthorizationResponse::new); + } catch (Exception e) { + return new InternalErrorResponse(e, request.getTarget()); + } + } + + private Response forbiddenResponse(int required, int permLevel) { + return PageCache.loadPage("forbidden", () -> + new ForbiddenResponse("Unauthorized User.
" + + "Make sure your user has the correct access level.
" + + "This page requires permission level of " + required + ",
" + + "This user has permission level of " + permLevel)); + } + + private boolean isAuthorized(int requiredPermLevel, int permLevel) throws Exception { + return permLevel <= requiredPermLevel; + } + + private WebUser getUser(String auth) throws SQLException, PassEncryptUtil.InvalidHashException, PassEncryptUtil.CannotPerformOperationException, WebUserAuthException { + Base64.Decoder decoder = Base64.getDecoder(); + byte[] decoded = decoder.decode(auth); + String[] userInfo = new String(decoded).split(":"); + if (userInfo.length != 2) { + throw new WebUserAuthException("User and Password not specified"); + } + + String user = userInfo[0]; + String passwordRaw = userInfo[1]; + + SecurityTable securityTable = plugin.getDB().getSecurityTable(); + if (!securityTable.userExists(user)) { + throw new WebUserAuthException("User Doesn't exist"); + } + + WebUser webUser = securityTable.getWebUser(user); + + boolean correctPass = PassEncryptUtil.verifyPassword(passwordRaw, webUser.getSaltedPassHash()); + if (!correctPass) { + throw new WebUserAuthException("User and Password do not match"); + } + return webUser; + } + + private int getRequiredPermLevel(String target, String user) { + String[] t = target.split("/"); + if (t.length < 2) { + return 100; + } + if (t.length > 3) { + return 0; + } + String page = t[1]; + switch (page) { + case "players": + return 1; + case "player": + // /player/ - 404 for perm lvl 1 + if (t.length < 3) { + return 1; + } + + final String wantedUser = t[2].toLowerCase().trim(); + final String theUser = user.trim().toLowerCase(); + + return wantedUser.equals(theUser) ? 2 : 1; + default: + return 0; + } + } + + private Response rootPageResponse(WebUser user) { + if (user == null) { + return notFoundResponse(); + } + + switch (user.getPermLevel()) { + case 0: + return serverResponse(); + case 1: + return PageCache.loadPage("players", PlayersPageResponse::new); + case 2: + return playerResponse(new String[]{"", "", user.getName()}); + default: + return forbiddenResponse(user.getPermLevel(), 0); + } + } + + private Response serverResponse() { + if (!plugin.getInfoManager().isAnalysisCached()) { + String error = "Analysis Data was not cached.
Use /plan analyze to cache the Data."; + PageCache.loadPage("notFound: " + error, () -> new NotFoundResponse(error)); + } + + return PageCache.loadPage("analysisPage", () -> new AnalysisPageResponse(plugin.getInfoManager())); + } + + private Response playerResponse(String[] args) { + if (args.length < 3) { + return PageCache.loadPage("notFound", NotFoundResponse::new); + } + + String playerName = args[2].trim(); + UUID uuid = UUIDUtility.getUUIDOf(playerName); + + if (uuid == null) { + String error = "Player has no UUID"; + return PageCache.loadPage("notFound: " + error, () -> new NotFoundResponse(error)); + } + + plugin.getInfoManager().cachePlayer(uuid); + return PageCache.loadPage("inspectPage: " + uuid, () -> new InspectPageResponse(plugin.getInfoManager(), uuid)); + } + + private Response notFoundResponse() { + String error = "404 Not Found"; + return PageCache.loadPage("notFound: " + error, () -> + new NotFoundResponse("Make sure you're accessing a link given by a command, Examples:

" + + "

" + webServer.getProtocol() + ":" + HtmlUtils.getInspectUrl("") + " or
" + + webServer.getProtocol() + ":" + HtmlUtils.getServerAnalysisUrl()) + ); + } + + +} \ No newline at end of file diff --git a/Plan/src/main/java/com/djrapitops/plan/systems/webserver/WebServer.java b/Plan/src/main/java/com/djrapitops/plan/systems/webserver/WebServer.java index e9b53575a..463d0ec4b 100644 --- a/Plan/src/main/java/com/djrapitops/plan/systems/webserver/WebServer.java +++ b/Plan/src/main/java/com/djrapitops/plan/systems/webserver/WebServer.java @@ -1,43 +1,33 @@ package main.java.com.djrapitops.plan.systems.webserver; -import com.djrapitops.plugin.utilities.Verify; -import com.sun.net.httpserver.*; +import com.sun.net.httpserver.HttpServer; +import com.sun.net.httpserver.HttpsConfigurator; +import com.sun.net.httpserver.HttpsParameters; +import com.sun.net.httpserver.HttpsServer; import main.java.com.djrapitops.plan.Log; -import main.java.com.djrapitops.plan.Plan; import main.java.com.djrapitops.plan.Settings; import main.java.com.djrapitops.plan.api.IPlan; -import main.java.com.djrapitops.plan.data.WebUser; -import main.java.com.djrapitops.plan.database.tables.SecurityTable; import main.java.com.djrapitops.plan.locale.Locale; import main.java.com.djrapitops.plan.locale.Msg; import main.java.com.djrapitops.plan.systems.info.InformationManager; -import main.java.com.djrapitops.plan.systems.webapi.WebAPI; import main.java.com.djrapitops.plan.systems.webapi.WebAPIManager; import main.java.com.djrapitops.plan.systems.webapi.bukkit.*; import main.java.com.djrapitops.plan.systems.webapi.universal.PingWebAPI; -import main.java.com.djrapitops.plan.systems.webserver.response.*; -import main.java.com.djrapitops.plan.systems.webserver.response.api.BadRequestResponse; -import main.java.com.djrapitops.plan.systems.webserver.response.api.JsonResponse; -import main.java.com.djrapitops.plan.utilities.Benchmark; -import main.java.com.djrapitops.plan.utilities.PassEncryptUtil; import main.java.com.djrapitops.plan.utilities.html.HtmlUtils; -import main.java.com.djrapitops.plan.utilities.uuid.UUIDUtility; import javax.net.ssl.*; -import java.io.*; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; import java.net.InetSocketAddress; -import java.net.URI; -import java.nio.charset.StandardCharsets; import java.nio.file.Paths; import java.security.*; import java.security.cert.Certificate; import java.security.cert.CertificateException; -import java.util.*; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; -import java.util.zip.GZIPOutputStream; /** * @author Rsl1122 @@ -46,6 +36,7 @@ public class WebServer { private final IPlan plugin; private InformationManager infoManager; + private final WebAPIManager webAPI; private final int port; private boolean enabled = false; @@ -59,7 +50,7 @@ public class WebServer { public WebServer(IPlan plugin) { this.plugin = plugin; this.port = Settings.WEBSERVER_PORT.getNumber(); - + webAPI = new WebAPIManager(); registerWebAPIs(); } @@ -68,16 +59,16 @@ public class WebServer { } private void registerWebAPIs() { - WebAPIManager.registerNewAPI("analytics", new AnalyticsWebAPI()); - WebAPIManager.registerNewAPI("analyze", new AnalyzeWebAPI()); - WebAPIManager.registerNewAPI("configure", new ConfigureWebAPI()); - WebAPIManager.registerNewAPI("inspect", new InspectWebAPI()); - WebAPIManager.registerNewAPI("onlineplayers", new OnlinePlayersWebAPI()); - WebAPIManager.registerNewAPI("ping", new PingWebAPI()); + webAPI.registerNewAPI("analytics", new AnalyticsWebAPI()); + webAPI.registerNewAPI("analyze", new AnalyzeWebAPI()); + webAPI.registerNewAPI("configure", new ConfigureWebAPI()); + webAPI.registerNewAPI("inspect", new InspectWebAPI()); + webAPI.registerNewAPI("onlineplayers", new OnlinePlayersWebAPI()); + webAPI.registerNewAPI("ping", new PingWebAPI()); } /** - * Starts up the Webserver in a Asynchronous thread. + * Starts up the WebServer in a new Thread Pool. */ public void initServer() { //Server is already enabled stop code @@ -96,53 +87,7 @@ public class WebServer { server = HttpServer.create(new InetSocketAddress(port), 10); } - server.createContext("/", exchange -> { - Headers responseHeaders = exchange.getResponseHeaders(); - URI uri = exchange.getRequestURI(); - String target = uri.toString(); - try { - boolean apiRequest = "POST".equals(exchange.getRequestMethod()); - Response response = null; - - String type = "text/html;"; - - if (apiRequest) { - response = getAPIResponse(target, exchange); - - if (response instanceof JsonResponse) { - type = "application/json;"; - } - } - - responseHeaders.set("Content-Type", type); - - if (apiRequest) { - sendData(responseHeaders, exchange, response); - return; - } - - WebUser user = null; - - if (usingHttps) { - user = getUser(exchange.getRequestHeaders()); - - // Prompt authorization - if (user == null) { - responseHeaders.set("WWW-Authenticate", "Basic realm=\"/\";"); - } - } - - response = getResponse(target, user); - if (response instanceof CSSResponse) { - responseHeaders.set("Content-Type", "text/css"); - } - sendData(responseHeaders, exchange, response); - } catch (Exception e) { - internalErrorResponse(exchange, responseHeaders, target, e); - } finally { - exchange.close(); - } - }); + server.createContext("/", new RequestHandler(plugin, this)); server.setExecutor(new ThreadPoolExecutor(4, 8, 30, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100))); server.start(); @@ -156,77 +101,6 @@ public class WebServer { } } - private void sendData(Headers header, HttpExchange exchange, Response response) throws IOException { - header.set("Content-Encoding", "gzip"); - exchange.sendResponseHeaders(response.getCode(), 0); - - try (GZIPOutputStream out = new GZIPOutputStream(exchange.getResponseBody()); - ByteArrayInputStream bis = new ByteArrayInputStream(response.getContent().getBytes())) { - byte[] buffer = new byte[2048]; - int count; - while ((count = bis.read(buffer)) != -1) { - out.write(buffer, 0, count); - } - } - } - - private void internalErrorResponse(HttpExchange exchange, Headers responseHeaders, String target, Exception e) { - try { - Log.toLog(target, e); - sendData(responseHeaders, exchange, new InternalErrorResponse(e, target)); - } catch (IOException e1) { - Log.toLog(this.getClass().getName(), e1); - } - } - - private WebUser getUser(Headers requestHeaders) { - Benchmark.start("getUser"); - try { - List authorization = requestHeaders.get("Authorization"); - if (Verify.isEmpty(authorization)) { - return null; - } - - String auth = authorization.get(0); - if (auth.contains("Basic ")) { - auth = auth.split(" ")[1]; - } else { - throw new IllegalArgumentException("Wrong format of Auth"); - } - - Base64.Decoder decoder = Base64.getDecoder(); - byte[] decoded = decoder.decode(auth); - String[] userInfo = new String(decoded).split(":"); - if (userInfo.length != 2) { - throw new IllegalArgumentException("User and Password not specified"); - } - - String user = userInfo[0]; - String passwordRaw = userInfo[1]; - - SecurityTable securityTable = plugin.getDB().getSecurityTable(); - if (!securityTable.userExists(user)) { - throw new IllegalArgumentException("User Doesn't exist"); - } - - WebUser webUser = securityTable.getWebUser(user); - - boolean correctPass = PassEncryptUtil.verifyPassword(passwordRaw, webUser.getSaltedPassHash()); - if (!correctPass) { - throw new IllegalArgumentException("User and Password do not match"); - } - - Benchmark.stop("getUser: " + requestHeaders); - return webUser; - } catch (IllegalArgumentException e) { - Log.debug("WebServer: " + e.getMessage()); - return null; - } catch (Exception e) { - Log.toLog(this.getClass().getName(), e); - return null; - } - } - private boolean startHttpsServer() { String keyStorePath = Settings.WEBSERVER_CERTIFICATE_PATH.toString(); if (!Paths.get(keyStorePath).isAbsolute()) { @@ -286,212 +160,6 @@ public class WebServer { return startSuccessful; } - private String readPOSTRequest(HttpExchange exchange) throws IOException { - byte[] bytes; - - try (InputStream in = exchange.getRequestBody()) { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - byte[] buf = new byte[4096]; - for (int n = in.read(buf); n > 0; n = in.read(buf)) { - out.write(buf, 0, n); - } - - bytes = out.toByteArray(); - } - - try { - return new String(bytes, StandardCharsets.ISO_8859_1); - } catch (Exception e) { - return null; - } - } - - private Response getAPIResponse(String target, HttpExchange exchange) throws IOException { - String[] args = target.split("/"); - - if (args.length < 3) { - String error = "API Method not specified"; - return PageCache.loadPage(error, () -> new BadRequestResponse(error)); - } - - String method = args[2]; - String response = readPOSTRequest(exchange); - - if (response == null) { - String error = "Error at reading the POST request." + - "Note that the Encoding must be ISO-8859-1."; - return PageCache.loadPage(error, () -> new BadRequestResponse(error)); - } - - Map variables = readVariables(response); - String key = variables.get("key"); - - if (!checkKey(key)) { - String error = "Server Key not given or invalid"; - return PageCache.loadPage(error, () -> { - ForbiddenResponse forbidden = new ForbiddenResponse(); - forbidden.setContent(error); - return forbidden; - }); - } - - WebAPI api = WebAPIManager.getAPI(method); - - if (api == null) { - String error = "API Method not found"; - return PageCache.loadPage(error, () -> new BadRequestResponse(error)); - } - - try { - return api.onResponse(Plan.getInstance(), variables); - } catch (Exception ex) { - Log.toLog("WebServer.getAPIResponse", ex); - return new InternalErrorResponse(ex, "An error while processing the request happened"); - } - } - - private boolean checkKey(String key) { - if (key == null) { - return false; - } - - UUID uuid = Plan.getServerUUID(); - UUID keyUUID; - try { - keyUUID = UUID.fromString(key); - } catch (IllegalArgumentException e) { - return false; - } - - return uuid.equals(keyUUID); - } - - private Map readVariables(String response) { - String[] variables = response.split("&"); - - return Arrays.stream(variables) - .map(variable -> variable.split("=", 2)) - .filter(splittedVariables -> splittedVariables.length == 2) - .collect(Collectors.toMap(splittedVariables -> splittedVariables[0], splittedVariables -> splittedVariables[1], (a, b) -> b)); - } - - private Response getResponse(String target, WebUser user) { - if ("/favicon.ico".equals(target)) { - return PageCache.loadPage("Redirect: favicon", () -> new RedirectResponse("https://puu.sh/tK0KL/6aa2ba141b.ico")); - } - - if (usingHttps) { - if (user == null) { - return PageCache.loadPage("promptAuthorization", PromptAuthorizationResponse::new); - } - - int permLevel = user.getPermLevel(); // Lower number has higher clearance. - int required = getRequiredPermLevel(target, user.getName()); - if (permLevel > required) { - return forbiddenResponse(permLevel, required); - } - } - - boolean javaScriptRequest = target.endsWith(".js"); - boolean cssRequest = target.endsWith(".css"); - - String[] args = target.split("/"); - if (args.length < 2) { - return rootPageResponse(user); - } - - if (javaScriptRequest) { - return getJSResponse(args[args.length - 1]); - } - if (cssRequest) { - try { - return new CSSResponse("main.css"); - } catch (Exception e) { - return new InternalErrorResponse(e, target); - } - } - - String page = args[1]; - switch (page) { - case "players": - return PageCache.loadPage("players", PlayersPageResponse::new); - case "player": - return playerResponse(args); - case "server": - return serverResponse(); - default: - return notFoundResponse(); - } - } - - private Response getJSResponse(String fileName) { - try { - return new JavaScriptResponse(fileName); - } catch (Exception e) { - return new InternalErrorResponse(e, fileName); - } - } - - private Response forbiddenResponse(int permLevel, int required) { - return PageCache.loadPage("forbidden", () -> - new ForbiddenResponse("Unauthorized User.
" - + "Make sure your user has the correct access level.
" - + "This page requires permission level of " + required + ",
" - + "This user has permission level of " + permLevel)); - } - - private Response rootPageResponse(WebUser user) { - if (user == null) { - return notFoundResponse(); - } - - switch (user.getPermLevel()) { - case 0: - return serverResponse(); - case 1: - return PageCache.loadPage("players", PlayersPageResponse::new); - case 2: - return playerResponse(new String[]{"", "", user.getName()}); - default: - return forbiddenResponse(user.getPermLevel(), 0); - } - } - - private Response serverResponse() { - if (!infoManager.isAnalysisCached()) { - String error = "Analysis Data was not cached.
Use /plan analyze to cache the Data."; - PageCache.loadPage("notFound: " + error, () -> new NotFoundResponse(error)); - } - - return PageCache.loadPage("analysisPage", () -> new AnalysisPageResponse(infoManager)); - } - - private Response playerResponse(String[] args) { - if (args.length < 3) { - return PageCache.loadPage("notFound", NotFoundResponse::new); - } - - String playerName = args[2].trim(); - UUID uuid = UUIDUtility.getUUIDOf(playerName); - - if (uuid == null) { - String error = "Player has no UUID"; - return PageCache.loadPage("notFound: " + error, () -> new NotFoundResponse(error)); - } - - plugin.getInfoManager().cachePlayer(uuid); - return PageCache.loadPage("inspectPage: " + uuid, () -> new InspectPageResponse(infoManager, uuid)); - } - - private Response notFoundResponse() { - String error = "404 Not Found"; - return PageCache.loadPage("notFound: " + error, () -> - new NotFoundResponse("Make sure you're accessing a link given by a command, Examples:

" - + "

" + getProtocol() + ":" + HtmlUtils.getInspectUrl("") + " or
" - + getProtocol() + ":" + HtmlUtils.getServerAnalysisUrl()) - ); - } - /** * @return if the WebServer is enabled */ @@ -509,33 +177,6 @@ public class WebServer { } } - private int getRequiredPermLevel(String target, String user) { - String[] t = target.split("/"); - if (t.length < 2) { - return 100; - } - if (t.length > 3) { - return 0; - } - String page = t[1]; - switch (page) { - case "players": - return 1; - case "player": - // /player/ - 404 for perm lvl 1 - if (t.length < 3) { - return 1; - } - - final String wantedUser = t[2].toLowerCase().trim(); - final String theUser = user.trim().toLowerCase(); - - return wantedUser.equals(theUser) ? 2 : 1; - default: - return 0; - } - } - public String getProtocol() { return usingHttps ? "https" : "http"; } @@ -551,4 +192,8 @@ public class WebServer { public String getAccessAddress() { return getProtocol() + ":/" + HtmlUtils.getIP(); } + + public WebAPIManager getWebAPI() { + return webAPI; + } } diff --git a/Plan/src/main/java/com/djrapitops/plan/systems/webserver/response/CSSResponse.java b/Plan/src/main/java/com/djrapitops/plan/systems/webserver/response/CSSResponse.java index 4b689903c..3f8104f96 100644 --- a/Plan/src/main/java/com/djrapitops/plan/systems/webserver/response/CSSResponse.java +++ b/Plan/src/main/java/com/djrapitops/plan/systems/webserver/response/CSSResponse.java @@ -10,6 +10,7 @@ public class CSSResponse extends FileResponse { public CSSResponse(String fileName) { super(fileName); + super.setType(ResponseType.CSS); setContent(Theme.replaceColors(getContent())); } } diff --git a/Plan/src/main/java/com/djrapitops/plan/systems/webserver/response/JavaScriptResponse.java b/Plan/src/main/java/com/djrapitops/plan/systems/webserver/response/JavaScriptResponse.java index 0d3622b26..6f928f75f 100644 --- a/Plan/src/main/java/com/djrapitops/plan/systems/webserver/response/JavaScriptResponse.java +++ b/Plan/src/main/java/com/djrapitops/plan/systems/webserver/response/JavaScriptResponse.java @@ -8,5 +8,6 @@ public class JavaScriptResponse extends FileResponse { public JavaScriptResponse(String fileName) { super(fileName); + super.setType(ResponseType.JAVASCRIPT); } } diff --git a/Plan/src/main/java/com/djrapitops/plan/systems/webserver/response/Response.java b/Plan/src/main/java/com/djrapitops/plan/systems/webserver/response/Response.java index 57a1266af..8cfab8c78 100644 --- a/Plan/src/main/java/com/djrapitops/plan/systems/webserver/response/Response.java +++ b/Plan/src/main/java/com/djrapitops/plan/systems/webserver/response/Response.java @@ -1,6 +1,12 @@ package main.java.com.djrapitops.plan.systems.webserver.response; +import com.sun.net.httpserver.Headers; +import com.sun.net.httpserver.HttpExchange; + +import java.io.ByteArrayInputStream; +import java.io.IOException; import java.util.Objects; +import java.util.zip.GZIPOutputStream; /** * @author Rsl1122 @@ -8,13 +14,21 @@ import java.util.Objects; */ public abstract class Response { + private String type; private String header; private String content; + private Headers responseHeaders; + /** * Class Constructor. */ + public Response(ResponseType type) { + this.type = type.get(); + } + public Response() { + this.type = ResponseType.HTML.get(); } public String getResponse() { @@ -54,4 +68,27 @@ public abstract class Response { public int hashCode() { return Objects.hash(header, content); } + + protected void setType(ResponseType type) { + this.type = type.get(); + } + + public void setResponseHeaders(Headers responseHeaders) { + this.responseHeaders = responseHeaders; + } + + public void send(HttpExchange exchange) throws IOException { + responseHeaders.set("Content-Type", type); + responseHeaders.set("Content-Encoding", "gzip"); + exchange.sendResponseHeaders(getCode(), 0); + + try (GZIPOutputStream out = new GZIPOutputStream(exchange.getResponseBody()); + ByteArrayInputStream bis = new ByteArrayInputStream(content.getBytes())) { + byte[] buffer = new byte[2048]; + int count; + while ((count = bis.read(buffer)) != -1) { + out.write(buffer, 0, count); + } + } + } } diff --git a/Plan/src/main/java/com/djrapitops/plan/systems/webserver/response/ResponseType.java b/Plan/src/main/java/com/djrapitops/plan/systems/webserver/response/ResponseType.java new file mode 100644 index 000000000..66a2a4263 --- /dev/null +++ b/Plan/src/main/java/com/djrapitops/plan/systems/webserver/response/ResponseType.java @@ -0,0 +1,27 @@ +/* + * Licence is provided in the jar as license.yml also here: + * https://github.com/Rsl1122/Plan-PlayerAnalytics/blob/master/Plan/src/main/resources/license.yml + */ +package main.java.com.djrapitops.plan.systems.webserver.response; + +/** + * //TODO Class Javadoc Comment + * + * @author Rsl1122 + */ +public enum ResponseType { + HTML("text/html"), + CSS("text/css"), + JSON("application/json"), + JAVASCRIPT("application/javascript"); + + private final String type; + + ResponseType(String type) { + this.type = type; + } + + public String get() { + return type; + } +} diff --git a/Plan/src/main/java/com/djrapitops/plan/systems/webserver/response/api/JsonResponse.java b/Plan/src/main/java/com/djrapitops/plan/systems/webserver/response/api/JsonResponse.java index aa7b8fa87..b225a22b2 100644 --- a/Plan/src/main/java/com/djrapitops/plan/systems/webserver/response/api/JsonResponse.java +++ b/Plan/src/main/java/com/djrapitops/plan/systems/webserver/response/api/JsonResponse.java @@ -6,6 +6,7 @@ package main.java.com.djrapitops.plan.systems.webserver.response.api; import com.google.gson.Gson; import main.java.com.djrapitops.plan.systems.webserver.response.Response; +import main.java.com.djrapitops.plan.systems.webserver.response.ResponseType; /** * @author Fuzzlemann @@ -13,6 +14,7 @@ import main.java.com.djrapitops.plan.systems.webserver.response.Response; public class JsonResponse extends Response { public JsonResponse(T object) { + super(ResponseType.JSON); Gson gson = new Gson(); super.setHeader("HTTP/1.1 200 OK");