diff --git a/res/webserver/authenticated.html b/res/webserver/authenticated.html new file mode 100644 index 000000000..f5ba90e5b --- /dev/null +++ b/res/webserver/authenticated.html @@ -0,0 +1,81 @@ + + + Game Server Diagnostics + + + + + + +
+
+
+ Resource Usage:
+ +
+ +
+ +
+ + diff --git a/res/webserver/index.html b/res/webserver/index.html index f5ba90e5b..7303a387d 100644 --- a/res/webserver/index.html +++ b/res/webserver/index.html @@ -7,75 +7,26 @@ background-color: #FFFFFF; } .main_container { - width: 75%; - color: #000000; - margin: 0 auto; - } - .memory_container { - width: 50%; - max-width: 50%; - max-height: 500px; - float: left; - color: #000000; - display: inline-block; - } - .log_container { - width: 100%; - height: 500px; - max-height: 500px; - color: #000000; - font-family: 'Courier New', Courier, 'Lucida Sans Typewriter', 'Lucida Typewriter', monospace; - font-size: 13px; - overflow: visible; - } - .server_info { - width: 50%; - max-width: 50%; - max-height: 500px; - float: right; - display: inline-block; - overflow: scroll; + width: 300px; + height: 75px; + background-color: #000000; + color: #FFFFFF; + margin: auto; + padding: 15px; + text-align: center; + position: relative; + top: 50%; + transform: translateY(-50%); } - -
-
-
- Resource Usage:
- -
- -
- +
+ Username:
+ Password:
+ +
diff --git a/src/services/admin/OnlineInterfaceService.java b/src/services/admin/OnlineInterfaceService.java index 5a94b1cca..2105c45c5 100644 --- a/src/services/admin/OnlineInterfaceService.java +++ b/src/services/admin/OnlineInterfaceService.java @@ -6,10 +6,16 @@ import java.io.IOException; import java.net.InetAddress; import java.net.URL; import java.net.UnknownHostException; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import network.encryption.MD5; import resources.config.ConfigFile; import resources.control.Intent; import resources.control.Service; @@ -19,17 +25,19 @@ import services.admin.http.HttpServer; import services.admin.http.HttpServer.HttpServerCallback; import services.admin.http.HttpSocket; import services.admin.http.HttpSocket.HttpRequest; -import services.admin.http.HttpStatusCode; +import services.admin.http.HttpSession; import services.admin.http.HttpsServer; import utilities.ThreadUtilities; public class OnlineInterfaceService extends Service implements HttpServerCallback { private static final String TAG = "OnlineInterfaceService"; + private static final String GET_USER_SQL = "SELECT password, password_salt, banned FROM users WHERE username = ? AND password = ?"; private final WebserverData data; private final WebserverHandler handler; private final Runnable dataCollectionRunnable; + private final PreparedStatement getUser; private ScheduledExecutorService executor; private HttpsServer httpsServer; private HttpServer httpServer; @@ -39,6 +47,7 @@ public class OnlineInterfaceService extends Service implements HttpServerCallbac data = new WebserverData(); handler = new WebserverHandler(data); dataCollectionRunnable = () -> collectData(); + getUser = getLocalDatabase().prepareStatement(GET_USER_SQL); authorized = false; } @@ -99,9 +108,18 @@ public class OnlineInterfaceService extends Service implements HttpServerCallbac @Override public void onRequestReceived(HttpSocket socket, HttpRequest request) { try { - if (!request.getType().equals("GET")) { - socket.send(HttpStatusCode.METHOD_NOT_ALLOWED); - return; + if (request.getType().equals("POST")) { + String [] variables = request.getBody().split("&"); + if (variables.length == 2) { + Map varMap = new HashMap<>(); + for (String str : variables) { + String [] var = str.split("="); + if (var.length == 2) + varMap.put(var[0], var[1]); + } + if (varMap.containsKey("username") && varMap.containsKey("password")) + login(socket, varMap.get("username"), varMap.get("password")); + } } if (!socket.isSecure()) { socket.redirect(new URL("https", httpsServer.getBindAddress().getHostName(), httpsServer.getBindPort(), request.getURI().getPath()).toString()); @@ -148,4 +166,43 @@ public class OnlineInterfaceService extends Service implements HttpServerCallbac return null; } + private void login(HttpSocket socket, String username, String password) { + HttpSession session = socket.getSession(); + if (session == null) + return; + synchronized (getUser) { + try { + getUser.setString(1, username); + getUser.setString(2, password); + session.setAuthenticated(false); + try (ResultSet cursor = getUser.executeQuery()) { + session.setAuthenticated(cursor.next() && isUserValid(cursor, password)); + if (session.isAuthenticated()) { + Log.i(TAG, "[%s] Successfully logged in to online interface", username); + } else { + Log.w(TAG, "[%s] Failed to login to online interface. Incorrect user/pass", username); + socket.redirect(new URL("https", httpsServer.getBindAddress().getHostName(), httpsServer.getBindPort(), "/").toString()); + } + } catch (IOException e) { + e.printStackTrace(); + } + } catch (SQLException e) { + e.printStackTrace(); + } + } + } + + private boolean isUserValid(ResultSet set, String password) throws SQLException { + if (password.isEmpty()) + return false; + if (set.getBoolean("banned")) + return false; + String psqlPass = set.getString("password"); + String psqlSalt = set.getString("password_salt"); + if (psqlPass.length() != 32 && psqlSalt.length() == 0) + return psqlPass.equals(password); + password = MD5.digest(MD5.digest(psqlSalt) + MD5.digest(password)); + return psqlPass.equals(password); + } + } diff --git a/src/services/admin/WebserverData.java b/src/services/admin/WebserverData.java index 77473e1b2..2f989087d 100644 --- a/src/services/admin/WebserverData.java +++ b/src/services/admin/WebserverData.java @@ -58,7 +58,7 @@ class WebserverData { public double [] getCpuUsage() { return Arrays.copyOf(cpuUsage, cpuUsage.length); } - + public double [] getSystemMemoryUsage() { return Arrays.copyOf(systemMemoryUsage, systemMemoryUsage.length); } diff --git a/src/services/admin/WebserverHandler.java b/src/services/admin/WebserverHandler.java index 96ba02355..a88c9c776 100644 --- a/src/services/admin/WebserverHandler.java +++ b/src/services/admin/WebserverHandler.java @@ -22,7 +22,9 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import resources.player.Player; +import resources.server_info.Log; import services.admin.http.HttpImageType; +import services.admin.http.HttpSession; import services.admin.http.HttpSocket; import services.admin.http.HttpSocket.HttpRequest; import services.admin.http.HttpStatusCode; @@ -43,12 +45,17 @@ class WebserverHandler { String file = request.getURI().toASCIIString(); if (file.contains("?")) file = file.substring(0, file.indexOf('?')); + if (file.contains("#")) + file = file.substring(0, file.indexOf('#')); switch (file) { case "/memory_usage.png": - socket.send(createMemoryUsage(), HttpImageType.PNG); + if (socket.getSession().isAuthenticated()) + socket.send(createMemoryUsage(), HttpImageType.PNG); + else + socket.send(HttpStatusCode.NOT_FOUND, request.getURI() + " is not found!"); break; default: { - byte [] response = parseFile(file); + byte [] response = parseFile(socket.getSession(), file); if (response == null) socket.send(HttpStatusCode.NOT_FOUND, request.getURI() + " is not found!"); else @@ -82,7 +89,7 @@ class WebserverHandler { g.setColor(Color.WHITE); x += drawStr(g, image.getHeight(), x, " Sys:"); x += drawDataSet(g, Color.YELLOW, " M:", image.getWidth(), image.getHeight(), graphHeight, x, data.getSystemMemoryUsage()); - x += drawDataSet(g, Color.GREEN, " C:", image.getWidth(), image.getHeight(), graphHeight, x, data.getSystemCpuUsage()); + drawDataSet(g, Color.GREEN, " C:", image.getWidth(), image.getHeight(), graphHeight, x, data.getSystemCpuUsage()); return image; } @@ -152,12 +159,20 @@ class WebserverHandler { } } - private byte [] parseFile(String filepath) throws IOException { + private byte [] parseFile(HttpSession session, String filepath) throws IOException { File file = new File("res/webserver" + filepath); if (file.isDirectory()) file = new File(file, "index.html"); String type = getFileType(filepath); - if (!verifyPath(file)) + if (!verifyPath(file)) { + Log.e("WebserverHandler", "Cannot access %s - not a valid path", file); + return null; + } + if (file.toString().equals("res/webserver/index.html")) { + if (session.isAuthenticated()) { + file = new File("res/webserver/authenticated.html"); + } + } else if (!session.isAuthenticated()) return null; if (type.equalsIgnoreCase("text/html")) return parseHtmlFile(file).getBytes(ASCII); diff --git a/src/services/admin/http/HttpSocket.java b/src/services/admin/http/HttpSocket.java index 11b793dd5..9c5b19f9e 100644 --- a/src/services/admin/http/HttpSocket.java +++ b/src/services/admin/http/HttpSocket.java @@ -24,6 +24,7 @@ import java.util.Set; import javax.imageio.ImageIO; +import resources.server_info.Log; import services.admin.http.HttpCookie.CookieFlag; public class HttpSocket implements Closeable { @@ -59,6 +60,7 @@ public class HttpSocket implements Closeable { String [] req = null; Map params = new HashMap<>(); Set cookies = new HashSet<>(); + StringBuilder body = new StringBuilder(""); while (line != null && !line.isEmpty()) { if (req == null) req = line.split(" ", 3); @@ -73,6 +75,19 @@ public class HttpSocket implements Closeable { hasData = !line.isEmpty(); line = readLine(); } + if (params.containsKey("Content-Length")) { + Integer i = Integer.valueOf(params.get("Content-Length")); + if (i.intValue() > 0) { + String bodyStr = readBytes(i.intValue()); + if (bodyStr == null) + Log.e("HttpSocket", "Failed to read data in %s request: %s - returned null", req.length==0?"null":req[0], req.length<2?"null":req[1]); + else { + if (bodyStr.length() != i.intValue()) + Log.w("HttpSocket", "Failed to read all data in %s request: %s", req.length==0?"null":req[0], req.length<2?"null":req[1]); + body.append(bodyStr); + } + } + } if (hasData) { String type = null; URI uri = null; @@ -83,7 +98,7 @@ public class HttpSocket implements Closeable { uri = URI.create(req[1]); if (req.length >= 3) version = req[2]; - return new HttpRequest(type, uri, version, params, cookies); + return new HttpRequest(type, uri, version, params, cookies, body.toString()); } if (line == null) break; @@ -95,6 +110,10 @@ public class HttpSocket implements Closeable { this.session = session; } + public HttpSession getSession() { + return session; + } + public void redirect(String url) throws IOException { Map params = new HashMap<>(); params.put("Location", url); @@ -174,6 +193,21 @@ public class HttpSocket implements Closeable { } } + private String readBytes(int length) { + try { + if (length == 0) + return ""; + char [] data = new char[length]; + int read = reader.read(data); + if (read == -1) + return null; + return new String(data, 0, read); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + @Override public void close() throws IOException { socket.close(); @@ -236,13 +270,15 @@ public class HttpSocket implements Closeable { private final String httpVersion; private final Map params; private final Set cookies; + private final String body; - private HttpRequest(String requestType, URI uri, String httpVersion, Map params, Set cookies) { + private HttpRequest(String requestType, URI uri, String httpVersion, Map params, Set cookies, String body) { this.type = requestType; this.uri = uri; this.httpVersion = httpVersion; this.params = params; this.cookies = cookies; + this.body = body; } /** @@ -286,6 +322,14 @@ public class HttpSocket implements Closeable { public Set getCookies() { return Collections.unmodifiableSet(cookies); } + + /** + * Gets the request body + * @return the request body + */ + public String getBody() { + return body; + } } }