diff --git a/Plan/src/main/java/com/djrapitops/plan/Plan.java b/Plan/src/main/java/com/djrapitops/plan/Plan.java index f8af8f992..6a41ab820 100644 --- a/Plan/src/main/java/com/djrapitops/plan/Plan.java +++ b/Plan/src/main/java/com/djrapitops/plan/Plan.java @@ -180,6 +180,9 @@ public class Plan extends BukkitPlugin { if (webserverIsEnabled) { uiServer = new WebSocketServer(this); uiServer.initServer(); + if (!uiServer.isEnabled()) { + Log.error("WebServer was not Initialized."); + } // Prevent passwords showing up on console. Bukkit.getLogger().setFilter(new RegisterCommandFilter()); } else if (!hasDataViewCapability) { diff --git a/Plan/src/main/java/com/djrapitops/plan/Settings.java b/Plan/src/main/java/com/djrapitops/plan/Settings.java index 519bb62e1..adc62ee66 100644 --- a/Plan/src/main/java/com/djrapitops/plan/Settings.java +++ b/Plan/src/main/java/com/djrapitops/plan/Settings.java @@ -49,6 +49,10 @@ public enum Settings { LOCALE("Settings.Locale"), WEBSERVER_IP("Settings.WebServer.InternalIP"), ANALYSIS_EXPORT_PATH("Settings.Analysis.Export.DestinationFolder"), + WEBSERVER_CERTIFICATE_PATH("Settings.WebServer.Security.Certificate.KeyStorePath"), + WEBSERVER_CERTIFICATE_KEYPASS("Settings.WebServer.Security.Certificate.KeyPass"), + WEBSERVER_CERTIFICATE_STOREPASS("Settings.WebServer.Security.Certificate.KeyPass"), + WEBSERVER_CERTIFICATE_ALIAS("Settings.WebServer.Security.Certificate.Alias"), LINK_PROTOCOL("Settings.WebServer.LinkProtocol"), // SERVER_NAME("Customization.ServerName"), diff --git a/Plan/src/main/java/com/djrapitops/plan/ui/webserver/Authenticator.java b/Plan/src/main/java/com/djrapitops/plan/ui/webserver/Authenticator.java new file mode 100644 index 000000000..c4f623ed6 --- /dev/null +++ b/Plan/src/main/java/com/djrapitops/plan/ui/webserver/Authenticator.java @@ -0,0 +1,67 @@ +package main.java.com.djrapitops.plan.ui.webserver; + +import com.sun.net.httpserver.BasicAuthenticator; +import main.java.com.djrapitops.plan.Log; +import main.java.com.djrapitops.plan.Plan; +import main.java.com.djrapitops.plan.data.WebUser; +import main.java.com.djrapitops.plan.database.tables.SecurityTable; +import main.java.com.djrapitops.plan.utilities.PassEncryptUtil; + +import java.sql.SQLException; + +public class Authenticator extends BasicAuthenticator { + + private final Plan plugin; + + public Authenticator(Plan plugin, String realm) { + super(realm); + this.plugin = plugin; + } + + @Override + public boolean checkCredentials(String user, String pwd) { + try { + return isAuthorized(user, pwd, this.realm); + } catch (Exception e) { + Log.toLog(this.getClass().getName(), e); + return false; + } + } + + private boolean isAuthorized(String user, String passwordRaw, String target) throws IllegalArgumentException, PassEncryptUtil.CannotPerformOperationException, PassEncryptUtil.InvalidHashException, SQLException { + + SecurityTable securityTable = plugin.getDB().getSecurityTable(); + if (!securityTable.userExists(user)) { + throw new IllegalArgumentException("User Doesn't exist"); + } + WebUser securityInfo = securityTable.getSecurityInfo(user); + + boolean correctPass = PassEncryptUtil.verifyPassword(passwordRaw, securityInfo.getSaltedPassHash()); + if (!correctPass) { + throw new IllegalArgumentException("User and Password do not match"); + } + int permLevel = securityInfo.getPermLevel(); // Lower number has higher clearance. + int required = getRequiredPermLevel(target, securityInfo.getName()); + return permLevel <= required; + } + + private int getRequiredPermLevel(String target, String user) { + String[] t = target.split("/"); + if (t.length < 3) { + return 0; + } + final String wantedUser = t[2].toLowerCase().trim(); + final String theUser = user.trim().toLowerCase(); + if (t[1].equals("players")) { + return 1; + } + if (t[1].equals("player")) { + if (wantedUser.equals(theUser)) { + return 2; + } else { + return 1; + } + } + return 0; + } +} diff --git a/Plan/src/main/java/com/djrapitops/plan/ui/webserver/WebSocketServer.java b/Plan/src/main/java/com/djrapitops/plan/ui/webserver/WebSocketServer.java index f6815e8a6..992f90b74 100644 --- a/Plan/src/main/java/com/djrapitops/plan/ui/webserver/WebSocketServer.java +++ b/Plan/src/main/java/com/djrapitops/plan/ui/webserver/WebSocketServer.java @@ -1,7 +1,7 @@ package main.java.com.djrapitops.plan.ui.webserver; -import com.djrapitops.plugin.task.AbsRunnable; import com.djrapitops.plugin.utilities.Verify; +import com.sun.net.httpserver.*; import main.java.com.djrapitops.plan.Log; import main.java.com.djrapitops.plan.Phrase; import main.java.com.djrapitops.plan.Plan; @@ -10,18 +10,21 @@ import main.java.com.djrapitops.plan.data.WebUser; import main.java.com.djrapitops.plan.database.tables.SecurityTable; import main.java.com.djrapitops.plan.ui.html.DataRequestHandler; import main.java.com.djrapitops.plan.ui.webserver.response.*; -import main.java.com.djrapitops.plan.utilities.Benchmark; import main.java.com.djrapitops.plan.utilities.HtmlUtils; -import main.java.com.djrapitops.plan.utilities.MiscUtils; import main.java.com.djrapitops.plan.utilities.PassEncryptUtil; import main.java.com.djrapitops.plan.utilities.uuid.UUIDUtility; +import javax.net.ssl.*; +import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; -import java.io.InputStream; import java.io.OutputStream; import java.net.InetAddress; -import java.net.ServerSocket; +import java.net.InetSocketAddress; import java.net.Socket; +import java.security.*; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; import java.sql.SQLException; import java.util.Base64; import java.util.UUID; @@ -32,13 +35,17 @@ import java.util.UUID; public class WebSocketServer { private final int PORT; + private final Plan plugin; private final DataRequestHandler dataReqHandler; private boolean enabled = false; private Socket sslServer; - private ServerSocket server; + private HttpServer server; private boolean shutdown; + private KeyManagerFactory keyManagerFactory; + private TrustManagerFactory trustManagerFactory; + /** * Class Constructor. *

@@ -64,43 +71,118 @@ public class WebSocketServer { Log.info(Phrase.WEBSERVER_INIT.toString()); try { InetAddress ip = InetAddress.getByName(Settings.WEBSERVER_IP.toString()); -// SSLServerSocketFactory ssl = (SSLServerSocketFactory) SSLServerSocketFactory.getDefault(); -// server = ssl.createServerSocket(PORT, 1, ip); - server = new ServerSocket(PORT, 1, ip); - plugin.getRunnableFactory().createNew(new AbsRunnable("WebServerTask") { - @Override - public void run() { - while (!shutdown) { - /*SSL*/ - Socket socket = null; - InputStream input = null; - OutputStream output = null; - Request request = null; + String keyStorePath = Settings.WEBSERVER_CERTIFICATE_PATH.toString(); + if (!keyStorePath.contains(":")) { + keyStorePath = plugin.getDataFolder() + keyStorePath; + } + char[] storepass = Settings.WEBSERVER_CERTIFICATE_STOREPASS.toString().toCharArray(); + char[] keypass = Settings.WEBSERVER_CERTIFICATE_KEYPASS.toString().toCharArray(); + String alias = Settings.WEBSERVER_CERTIFICATE_ALIAS.toString(); + + boolean startSuccessful = false; + try { + FileInputStream fIn = new FileInputStream(keyStorePath); + KeyStore keystore = KeyStore.getInstance("JKS"); + + keystore.load(fIn, storepass); + Certificate cert = keystore.getCertificate(alias); + + Log.info("Found Certificate: " + cert); + + keyManagerFactory = KeyManagerFactory.getInstance("SunX509"); + keyManagerFactory.init(keystore, keypass); + + trustManagerFactory = TrustManagerFactory.getInstance("SunX509"); + trustManagerFactory.init(keystore); + + server = HttpsServer.create(new InetSocketAddress(PORT), 0); + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null); + + ((HttpsServer) server).setHttpsConfigurator(new HttpsConfigurator(sslContext) { + public void configure(HttpsParameters params) { try { - socket = /*(SSLSocket)*/ server.accept(); - Log.debug("New Socket Connection: " + socket.getInetAddress()); - input = socket.getInputStream(); - output = socket.getOutputStream(); - request = new Request(input); - Benchmark.start("Webserver Response"); - request.parse(); - Response response = getResponse(request, output); - Log.debug("Parsed response: " + response.getClass().getSimpleName()); - response.sendStaticResource(); - } catch (IOException | IllegalArgumentException e) { - } finally { - MiscUtils.close(input, request, output, socket); - Benchmark.stop("Webserver Response"); + SSLContext c = SSLContext.getDefault(); + SSLEngine engine = c.createSSLEngine(); + + params.setNeedClientAuth(false); + params.setCipherSuites(engine.getEnabledCipherSuites()); + params.setProtocols(engine.getEnabledProtocols()); + + SSLParameters defaultSSLParameters = c.getDefaultSSLParameters(); + params.setSSLParameters(defaultSSLParameters); + } catch (NoSuchAlgorithmException e) { + Log.error("WebServer: SSL Engine loading Failed."); + Log.toLog(this.getClass().getName(), e); } } - this.cancel(); + }); + startSuccessful = true; + } catch (KeyManagementException e) { + Log.error("WebServer: SSL Context Initialization Failed."); + Log.toLog(this.getClass().getName(), e); + } catch (NoSuchAlgorithmException e) { + Log.error("WebServer: SSL Context Initialization Failed."); + Log.toLog(this.getClass().getName(), e); + } catch (FileNotFoundException e) { + Log.error("WebServer: SSL Certificate KeyStore File not Found: " + keyStorePath); + } catch (KeyStoreException | CertificateException | UnrecoverableKeyException e) { + Log.error("WebServer: SSL Certificate loading Failed."); + Log.toLog(this.getClass().getName(), e); + } + + if (!startSuccessful) { + return; // TODO Http Server + } + + HttpContext analysisPage = server.createContext("/server", serverResponse(null)); + HttpContext playersPage = server.createContext("/players", new PlayersPageResponse(null, plugin)); + HttpContext inspectPage = server.createContext("/player", new InspectPageResponse(null, dataReqHandler, UUID.randomUUID())); // TODO + + if (startSuccessful) { + for (HttpContext c : new HttpContext[]{analysisPage, playersPage, inspectPage}) { + c.setAuthenticator(new Authenticator(plugin, c.getPath())); } - }).runTaskAsynchronously(); + } + + server.start(); + +// server = new ServerSocket(PORT, 1, ip); +// +// plugin.getRunnableFactory().createNew(new AbsRunnable("WebServerTask") { +// @Override +// public void run() { +// while (!shutdown) { +// /*SSL*/ +// Socket socket = null; +// InputStream input = null; +// OutputStream output = null; +// Request request = null; +// try { +// socket = /*(SSLSocket)*/ server.accept(); +// Log.debug("New Socket Connection: " + socket.getInetAddress()); +// input = socket.getInputStream(); +// output = socket.getOutputStream(); +// request = new Request(input); +// Benchmark.start("Webserver Response"); +// request.parse(); +// Response response = getResponse(request, output); +// Log.debug("Parsed response: " + response.getClass().getSimpleName()); +// response.sendStaticResource(); +// } catch (IOException | IllegalArgumentException e) { +// } finally { +// MiscUtils.close(input, request, output, socket); +// Benchmark.stop("Webserver Response"); +// } +// } +// this.cancel(); +// } +// }).runTaskAsynchronously(); enabled = true; - Log.info(Phrase.WEBSERVER_RUNNING.parse(server.getLocalPort() + "")); + Log.info(Phrase.WEBSERVER_RUNNING.parse(server.getAddress().getPort() + "")); } catch (IllegalArgumentException | IllegalStateException | IOException e) { Log.toLog(this.getClass().getName(), e); enabled = false; @@ -117,22 +199,22 @@ public class WebSocketServer { return new RedirectResponse(output, "https://puu.sh/tK0KL/6aa2ba141b.ico"); } -// if (!request.hasAuthorization()) { -// return new PromptAuthorizationResponse(output); -// } -// try { -// if (!isAuthorized(request)) { -// ForbiddenResponse response403 = new ForbiddenResponse(output); -// String content = "

403 Forbidden - Access Denied

" -// + "

Unauthorized User.
" -// + "Make sure your user has the correct access level.
" -// + "You can use /plan web check to check the permission level.

"; -// response403.setContent(content); -// return response403; -// } -// } catch (IllegalArgumentException e) { -// return new PromptAuthorizationResponse(output); -// } + if (!request.hasAuthorization()) { + return new PromptAuthorizationResponse(output); + } + try { + if (!isAuthorized(request)) { + ForbiddenResponse response403 = new ForbiddenResponse(output); + String content = "

403 Forbidden - Access Denied

" + + "

Unauthorized User.
" + + "Make sure your user has the correct access level.
" + + "You can use /plan web check to check the permission level.

"; + response403.setContent(content); + return response403; + } + } catch (IllegalArgumentException e) { + return new PromptAuthorizationResponse(output); + } String req = request.getRequest(); String target = request.getTarget(); if (!req.equals("GET") || target.equals("/")) { @@ -204,12 +286,8 @@ public class WebSocketServer { public void stop() { Log.info(Phrase.WEBSERVER_CLOSE.toString()); shutdown = true; - try { - if (server != null) { - server.close(); - } - } catch (IOException e) { - Log.toLog(this.getClass().getName(), e); + if (server != null) { + server.stop(0); } } @@ -229,23 +307,28 @@ public class WebSocketServer { throw new IllegalArgumentException("User and Password not specified"); } String user = userInfo[0]; + String passwordRaw = userInfo[1]; + return isAuthorized(user, passwordRaw, request.getTarget()); + } + + private boolean isAuthorized(String user, String passwordRaw, String target) throws IllegalArgumentException, PassEncryptUtil.CannotPerformOperationException, PassEncryptUtil.InvalidHashException, SQLException { + SecurityTable securityTable = plugin.getDB().getSecurityTable(); if (!securityTable.userExists(user)) { throw new IllegalArgumentException("User Doesn't exist"); } WebUser securityInfo = securityTable.getSecurityInfo(user); - String passwordRaw = userInfo[1]; + boolean correctPass = PassEncryptUtil.verifyPassword(passwordRaw, securityInfo.getSaltedPassHash()); if (!correctPass) { throw new IllegalArgumentException("User and Password do not match"); } int permLevel = securityInfo.getPermLevel(); // Lower number has higher clearance. - int required = getRequiredPermLevel(request, securityInfo.getName()); + int required = getRequiredPermLevel(target, securityInfo.getName()); return permLevel <= required; } - private int getRequiredPermLevel(Request request, String user) { - String target = request.getTarget(); + private int getRequiredPermLevel(String target, String user) { String[] t = target.split("/"); if (t.length < 3) { return 0; diff --git a/Plan/src/main/java/com/djrapitops/plan/ui/webserver/response/Response.java b/Plan/src/main/java/com/djrapitops/plan/ui/webserver/response/Response.java index e28579b8c..c834a2023 100644 --- a/Plan/src/main/java/com/djrapitops/plan/ui/webserver/response/Response.java +++ b/Plan/src/main/java/com/djrapitops/plan/ui/webserver/response/Response.java @@ -1,5 +1,8 @@ package main.java.com.djrapitops.plan.ui.webserver.response; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; + import java.io.IOException; import java.io.OutputStream; @@ -7,7 +10,8 @@ import java.io.OutputStream; * @author Rsl1122 * @since 3.5.2 */ -public abstract class Response { +public abstract class Response implements HttpHandler { + private final OutputStream output; @@ -29,14 +33,18 @@ public abstract class Response { * @throws IOException */ public void sendStaticResource() throws IOException { - String response = header + "\r\n" + String response = getResponse(); +// Log.debug("Response: " + response); // Responses should not be logged, html content large. + output.write(response.getBytes()); + output.flush(); + } + + public String getResponse() { + return header + "\r\n" + "Content-Type: text/html;\r\n" + "Content-Length: " + content.length() + "\r\n" + "\r\n" + content; -// Log.debug("Response: " + response); // Responses should not be logged, html content large. - output.write(response.getBytes()); - output.flush(); } public void setHeader(String header) { @@ -46,4 +54,21 @@ public abstract class Response { public void setContent(String content) { this.content = content; } + + @Override + public void handle(HttpExchange exchange) throws IOException { + exchange.sendResponseHeaders(getCode(), content.length()); + + OutputStream os = exchange.getResponseBody(); + os.write(content.getBytes()); + os.close(); + } + + private int getCode() { + if (header == null) { + return 500; + } else { + return Integer.parseInt(header.split(" ")[1]); + } + } } diff --git a/Plan/src/main/resources/analysis.html b/Plan/src/main/resources/analysis.html index 2e830630d..ee4098107 100644 --- a/Plan/src/main/resources/analysis.html +++ b/Plan/src/main/resources/analysis.html @@ -1776,8 +1776,6 @@ }; Plotly.plot(CLOROPLETH, data, layout, {showLink: false}); - diff --git a/Plan/src/main/resources/config.yml b/Plan/src/main/resources/config.yml index 170a26c83..7b039dea9 100644 --- a/Plan/src/main/resources/config.yml +++ b/Plan/src/main/resources/config.yml @@ -37,6 +37,11 @@ Settings: LinkProtocol: http Security: DisplayIPsAndUUIDs: true + Certificate: + KeyStorePath: '\SSLCertificate.keystore' + KeyPass: 'default' + StorePass: 'default' + Alias: 'alias' Customization: ServerName: 'Plan'