diff --git a/res/webserver/favicon.ico b/res/webserver/favicon.ico
new file mode 100644
index 00000000..5d39b59f
Binary files /dev/null and b/res/webserver/favicon.ico differ
diff --git a/res/webserver/index.html b/res/webserver/index.html
new file mode 100644
index 00000000..fafa95f7
--- /dev/null
+++ b/res/webserver/index.html
@@ -0,0 +1,81 @@
+
+
+ Game Server Diagnostics
+
+
+
+
+
+
+
+
+
+ Memory Usage:
+
+
+
+
+
+
+
+
diff --git a/res/webserver/log.html b/res/webserver/log.html
new file mode 100644
index 00000000..0b63a57f
--- /dev/null
+++ b/res/webserver/log.html
@@ -0,0 +1 @@
+${LOG}
diff --git a/res/webserver/online_players.html b/res/webserver/online_players.html
new file mode 100644
index 00000000..e69de29b
diff --git a/res/webserver/server_info.html b/res/webserver/server_info.html
new file mode 100644
index 00000000..b3397eda
--- /dev/null
+++ b/res/webserver/server_info.html
@@ -0,0 +1,12 @@
+
+
+${ONLINE_PLAYERS}
+
diff --git a/server.keystore b/server.keystore
new file mode 100644
index 00000000..45136856
Binary files /dev/null and b/server.keystore differ
diff --git a/src/resources/control/Manager.java b/src/resources/control/Manager.java
index 6b1b0174..c4219e4e 100644
--- a/src/resources/control/Manager.java
+++ b/src/resources/control/Manager.java
@@ -37,7 +37,6 @@ import java.util.List;
*/
public abstract class Manager extends Service {
- private static final ServerManager serverManager = ServerManager.getInstance();
private List children;
public Manager() {
@@ -52,19 +51,13 @@ public abstract class Manager extends Service {
*/
@Override
public boolean initialize() {
- boolean success = super.initialize(), cSuccess = true;
- long start = 0, end = 0;
+ boolean success = super.initialize();
synchronized (children) {
for (Service child : children) {
- if (!success)
- break;
- start = System.nanoTime();
- cSuccess = child.initialize();
- end = System.nanoTime();
- serverManager.setServiceInitTime(child, (end-start)/1E6, cSuccess);
- if (!cSuccess) {
+ if (!child.initialize()) {
System.err.println(child.getClass().getSimpleName() + " failed to initialize!");
success = false;
+ break;
}
}
}
@@ -79,19 +72,13 @@ public abstract class Manager extends Service {
*/
@Override
public boolean start() {
- boolean success = super.start(), cSuccess = true;
- long start = 0, end = 0;
+ boolean success = super.start();
synchronized (children) {
for (Service child : children) {
- if (!success)
- break;
- start = System.nanoTime();
- cSuccess = child.start();
- end = System.nanoTime();
- serverManager.setServiceStartTime(child, (end-start)/1E6, cSuccess);
- if (!cSuccess) {
+ if (!child.start()) {
System.err.println(child.getClass().getSimpleName() + " failed to start!");
success = false;
+ break;
}
}
}
@@ -129,15 +116,10 @@ public abstract class Manager extends Service {
*/
@Override
public boolean terminate() {
- boolean success = super.terminate(), cSuccess = true;
- long start = 0, end = 0;
+ boolean success = super.terminate();
synchronized (children) {
for (Service child : children) {
- start = System.nanoTime();
- cSuccess = child.terminate();
- end = System.nanoTime();
- serverManager.setServiceTerminateTime(child, (end-start)/1E6, cSuccess);
- if (!cSuccess)
+ if (!child.terminate())
success = false;
}
}
@@ -174,7 +156,6 @@ public abstract class Manager extends Service {
if (s == child || s.equals(child))
return;
}
- serverManager.addChild(this, s);
children.add(s);
}
}
@@ -187,7 +168,6 @@ public abstract class Manager extends Service {
if (s == null)
return;
synchronized (children) {
- serverManager.removeChild(this, s);
children.remove(s);
}
}
diff --git a/src/resources/control/ServerManager.java b/src/resources/control/ServerManager.java
deleted file mode 100644
index a1cb2041..00000000
--- a/src/resources/control/ServerManager.java
+++ /dev/null
@@ -1,222 +0,0 @@
-/***********************************************************************************
-* Copyright (c) 2015 /// Project SWG /// www.projectswg.com *
-* *
-* ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on *
-* July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. *
-* Our goal is to create an emulator which will provide a server for players to *
-* continue playing a game similar to the one they used to play. We are basing *
-* it on the final publish of the game prior to end-game events. *
-* *
-* This file is part of Holocore. *
-* *
-* -------------------------------------------------------------------------------- *
-* *
-* Holocore is free software: you can redistribute it and/or modify *
-* it under the terms of the GNU Affero General Public License as *
-* published by the Free Software Foundation, either version 3 of the *
-* License, or (at your option) any later version. *
-* *
-* Holocore is distributed in the hope that it will be useful, *
-* but WITHOUT ANY WARRANTY; without even the implied warranty of *
-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
-* GNU Affero General Public License for more details. *
-* *
-* You should have received a copy of the GNU Affero General Public License *
-* along with Holocore. If not, see . *
-* *
-***********************************************************************************/
-package resources.control;
-
-import intents.server.ServerStatusIntent;
-
-import java.nio.ByteBuffer;
-import java.nio.charset.Charset;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import resources.control.Service;
-
-/**
- * Might be best to keep this protected, from a server security standpoint
- */
-final class ServerManager implements IntentReceiver {
-
- private static final ServerManager INSTANCE = new ServerManager();
-
- private final Map serviceStats;
- private ServerStatus status;
-
- private ServerManager() {
- serviceStats = new HashMap();
- status = ServerStatus.OFFLINE;
- ServerPublicInterface.initialize(this);
- IntentManager.getInstance().registerForIntent(ServerStatusIntent.TYPE, this);
- }
-
- public static final ServerManager getInstance() {
- return INSTANCE;
- }
-
- public void onIntentReceived(Intent i) {
- if (i instanceof ServerStatusIntent)
- processServerStatusIntent((ServerStatusIntent) i);
- }
-
- public boolean initialize() {
- ServerPublicInterface.initialize(this);
- return true;
- }
-
- public boolean terminate() {
- ServerPublicInterface.terminate();
- return true;
- }
-
- public void addChild(Service parent, Service child) {
- getServiceStats(parent).addChild(getServiceStats(child));
- }
-
- public void removeChild(Service parent, Service child) {
- getServiceStats(parent).removeChild(getServiceStats(child));
- }
-
- public void setServiceInitTime(Service service, double timeMilliseconds, boolean success) {
- getServiceStats(service).addInitTime(timeMilliseconds, success);
- }
-
- public void setServiceStartTime(Service service, double timeMilliseconds, boolean success) {
- getServiceStats(service).addStartTime(timeMilliseconds, success);
- }
-
- public void setServiceTerminateTime(Service service, double timeMilliseconds, boolean success) {
- getServiceStats(service).addTerminateTime(timeMilliseconds, success);
- }
-
- private void processServerStatusIntent(ServerStatusIntent i) {
- status = i.getStatus();
- }
-
- public byte [] serializeControlTimes() {
- synchronized (serviceStats) {
- int size = 0;
- for (ServiceStats stats : serviceStats.values())
- size += stats.getSerializeSize();
- ByteBuffer data = ByteBuffer.allocate(size);
- for (ServiceStats stats : serviceStats.values())
- data.put(stats.serialize());
- return data.array();
- }
- }
-
- public ServerStatus getServerStatus() {
- return status;
- }
-
- private ServiceStats getServiceStats(Service service) {
- synchronized (serviceStats) {
- ServiceStats stats = serviceStats.get(service.getClass().getName());
- if (stats == null) {
- stats = new ServiceStats(service);
- serviceStats.put(service.getClass().getName(), stats);
- }
- return stats;
- }
- }
-
- private static class ServiceStats {
- private final Service service;
- private final List initTimes;
- private final List startTimes;
- private final List terminateTimes;
- private final List children;
-
- public ServiceStats(Service service) {
- this.service = service;
- initTimes = new ArrayList();
- startTimes = new ArrayList();
- terminateTimes = new ArrayList();
- children = new ArrayList();
- }
-
- public void addChild(ServiceStats service) {
- children.add(service);
- }
-
- public void removeChild(ServiceStats service) {
- children.remove(service);
- }
-
- public void addInitTime(double time, boolean success) {
- initTimes.add(new ServiceControlTime(time, success));
- }
-
- public void addStartTime(double time, boolean success) {
- startTimes.add(new ServiceControlTime(time, success));
- }
-
- public void addTerminateTime(double time, boolean success) {
- terminateTimes.add(new ServiceControlTime(time, success));
- }
-
- public int getSerializeSize() {
- return 2 + service.getClass().getName().length() + 9*3;
- }
-
- public byte [] serialize() {
- String name = service.getClass().getName();
- ByteBuffer data = ByteBuffer.allocate(getSerializeSize());
- data.putShort((short) name.length());
- data.put(name.getBytes(Charset.forName("UTF-8")));
- serializeLastControlTime(data, initTimes);
- serializeLastControlTime(data, startTimes);
- serializeLastControlTime(data, terminateTimes);
- return data.array();
- }
-
- private void serializeLastControlTime(ByteBuffer data, List times) {
- if (initTimes.size() > times.size() || times.size() == 0)
- serializeControlTime(data, null);
- else
- serializeControlTime(data, times.get(initTimes.size()-1));
- }
-
- private void serializeControlTime(ByteBuffer data, ServiceControlTime time) {
- if (time == null) {
- data.put((byte) 0);
- data.putDouble(Double.NaN);
- } else {
- data.put((byte) (time.isSuccess() ? 1 : 0));
- data.putDouble(time.getTime());
- }
- }
-
- public String toString() {
- return "ServiceStats[" + service.getClass().getName() + "]";
- }
- }
-
- private static class ServiceControlTime {
- private final double time;
- private final boolean success;
-
- public ServiceControlTime(double time, boolean success) {
- this.time = time;
- this.success = success;
- }
-
- public double getTime() {
- return time;
- }
-
- public boolean isSuccess() {
- return success;
- }
-
- public String toString() {
- return "[" + (isSuccess()?"Success":"Failure") + " in " + getTime() + "ms]";
- }
- }
-
-}
diff --git a/src/resources/control/ServerPublicInterface.java b/src/resources/control/ServerPublicInterface.java
deleted file mode 100644
index d27de845..00000000
--- a/src/resources/control/ServerPublicInterface.java
+++ /dev/null
@@ -1,191 +0,0 @@
-/***********************************************************************************
-* Copyright (c) 2015 /// Project SWG /// www.projectswg.com *
-* *
-* ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on *
-* July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. *
-* Our goal is to create an emulator which will provide a server for players to *
-* continue playing a game similar to the one they used to play. We are basing *
-* it on the final publish of the game prior to end-game events. *
-* *
-* This file is part of Holocore. *
-* *
-* -------------------------------------------------------------------------------- *
-* *
-* Holocore is free software: you can redistribute it and/or modify *
-* it under the terms of the GNU Affero General Public License as *
-* published by the Free Software Foundation, either version 3 of the *
-* License, or (at your option) any later version. *
-* *
-* Holocore is distributed in the hope that it will be useful, *
-* but WITHOUT ANY WARRANTY; without even the implied warranty of *
-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
-* GNU Affero General Public License for more details. *
-* *
-* You should have received a copy of the GNU Affero General Public License *
-* along with Holocore. If not, see . *
-* *
-***********************************************************************************/
-package resources.control;
-
-import java.io.ByteArrayInputStream;
-import java.io.DataInputStream;
-import java.io.IOException;
-import java.net.InetAddress;
-import java.net.SocketException;
-import java.net.UnknownHostException;
-import java.nio.ByteBuffer;
-
-import resources.config.ConfigFile;
-import resources.network.UDPServer;
-import resources.network.UDPServer.UDPCallback;
-import resources.network.UDPServer.UDPPacket;
-import resources.server_info.Config;
-import resources.server_info.DataManager;
-
-final class ServerPublicInterface {
-
- private static final int PORT = 44452;
- private static final int PACKET_SIZE = 1024;
-
- private static ServerPublicInterface publicInterface = null;
-
- private ServerManager serverManager;
- private UDPServer server;
- private boolean initialized = false;
- private boolean terminated = false;
-
- private ServerPublicInterface() {
- serverManager = null;
- }
-
- public static final void initialize(ServerManager instance) {
- if (publicInterface == null)
- publicInterface = new ServerPublicInterface();
- publicInterface.initializeInterface(instance);
- }
-
- public static final void terminate() {
- publicInterface.terminateInterface();
- }
-
- private void initializeInterface(ServerManager instance) {
- if (!initialized) {
- serverManager = instance;
- server = createUdpServer();
- initialized = true;
- terminated = false;
- }
- }
-
- private void terminateInterface() {
- if (!terminated) {
- serverManager = null;
- if (server != null)
- server.close();
- initialized = false;
- terminated = true;
- }
- }
-
- private void onReceivedPacket(UDPPacket packet) {
- if (packet.getData().length == 0)
- return;
- InetAddress sender = packet.getAddress();
- int port = packet.getPort();
- DataInputStream dis = new DataInputStream(new ByteArrayInputStream(packet.getData()));
- try {
- while (dis.available() > 0) {
- processPacket(sender, port, dis);
- }
- } catch (IOException e) {
-
- }
- }
-
- private void processPacket(InetAddress sender, int port, DataInputStream is) throws IOException {
- RequestType type = getType(is.readByte());
- switch (type) {
- case PING_PONG: processPing(sender, port, is); break;
- case STATUS: processServerStatus(sender, port, is); break;
- case CONTROL_TIMES: processControlTimes(sender, port, is); break;
- default:
- break;
- }
- }
-
- private void processPing(InetAddress sender, int port, DataInputStream is) {
- server.send(port, sender, new byte[]{RequestType.PING_PONG.getType()});
- }
-
- private void processServerStatus(InetAddress sender, int port, DataInputStream is) {
- ServerStatus status = serverManager.getServerStatus();
- ByteBuffer data = ByteBuffer.allocate(3 + status.name().length());
- data.put(RequestType.STATUS.getType());
- data.putShort((short) status.name().length());
- data.put(status.name().getBytes());
- server.send(port, sender, data.array());
- }
-
- private void processControlTimes(InetAddress sender, int port, DataInputStream is) {
- byte [] controlTimes = serverManager.serializeControlTimes();
- byte [] timePacket = new byte[controlTimes.length+1];
- timePacket[0] = RequestType.CONTROL_TIMES.getType();
- System.arraycopy(controlTimes, 0, timePacket, 1, controlTimes.length);
- server.send(port, sender, timePacket);
- }
-
- private UDPServer createUdpServer() {
- UDPServer server = null;
- InetAddress bindAddr = getBindAddr();
- try {
- if (bindAddr == null)
- server = new UDPServer(PORT, PACKET_SIZE);
- else
- server = new UDPServer(bindAddr, PORT, PACKET_SIZE);
- server.setCallback(new UDPCallback() {
- public void onReceivedPacket(UDPPacket packet) {
- ServerPublicInterface.this.onReceivedPacket(packet);
- }
- });
- } catch (SocketException e) {
- // Keep it quiet - nobody needs to know this class exists
- }
- return server;
- }
-
- private InetAddress getBindAddr() {
- Config c = DataManager.getInstance().getConfig(ConfigFile.NETWORK);
- try {
- if (c.containsKey("BIND-ADDR"))
- return InetAddress.getByName(c.getString("BIND-ADDR", "127.0.0.1"));
- if (c.containsKey("INTERFACE-BIND-ADDR"))
- return InetAddress.getByName(c.getString("INTERFACE-BIND-ADDR", "127.0.0.1"));
- } catch (UnknownHostException e) {
-
- }
- return null;
- }
-
- private RequestType getType(byte b) {
- for (RequestType type : RequestType.values())
- if (type.getType() == b)
- return type;
- return RequestType.UNKNOWN;
- }
-
- public enum RequestType {
- PING_PONG (0x00),
- STATUS (0x01),
- CONTROL_TIMES (0x02),
- UNKNOWN (0xFF);
-
- private byte type;
-
- RequestType(int type) {
- this.type = (byte) type;
- }
-
- public byte getType() { return type; }
- }
-
-}
diff --git a/src/resources/control/Service.java b/src/resources/control/Service.java
index 49fba0b0..7ea7c0de 100644
--- a/src/resources/control/Service.java
+++ b/src/resources/control/Service.java
@@ -50,7 +50,7 @@ public abstract class Service implements IntentReceiver {
*/
public boolean initialize() {
IntentManager.getInstance().initialize();
- return DataManager.getInstance().isInitialized() && ServerManager.getInstance().initialize();
+ return DataManager.getInstance().isInitialized();
}
/**
@@ -78,7 +78,7 @@ public abstract class Service implements IntentReceiver {
* @return TRUE if termination was successful, FALSE otherwise
*/
public boolean terminate() {
- return ServerManager.getInstance().terminate();
+ return true;
}
/**
diff --git a/src/services/CoreManager.java b/src/services/CoreManager.java
index a5e567a3..d9679729 100644
--- a/src/services/CoreManager.java
+++ b/src/services/CoreManager.java
@@ -55,6 +55,7 @@ import resources.control.Intent;
import resources.control.Manager;
import resources.control.ServerStatus;
import resources.server_info.Config;
+import services.admin.OnlineInterfaceService;
import services.galaxy.GalacticManager;
import utilities.ThreadUtilities;
@@ -63,6 +64,8 @@ public class CoreManager extends Manager {
private static final int galaxyId = 1;
private final ScheduledExecutorService shutdownService;
+
+ private OnlineInterfaceService onlineInterfaceService;
private EngineManager engineManager;
private GalacticManager galacticManager;
private PrintStream packetOutput;
@@ -76,9 +79,11 @@ public class CoreManager extends Manager {
shutdownRequested = false;
galaxy = getGalaxy();
if (galaxy != null) {
+ onlineInterfaceService = new OnlineInterfaceService();
engineManager = new EngineManager(galaxy);
galacticManager = new GalacticManager(galaxy);
-
+
+ addChildService(onlineInterfaceService);
addChildService(engineManager);
addChildService(galacticManager);
}
diff --git a/src/services/admin/OnlineInterfaceService.java b/src/services/admin/OnlineInterfaceService.java
new file mode 100644
index 00000000..29eab83b
--- /dev/null
+++ b/src/services/admin/OnlineInterfaceService.java
@@ -0,0 +1,153 @@
+package services.admin;
+
+import intents.PlayerEventIntent;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import resources.config.ConfigFile;
+import resources.control.Intent;
+import resources.control.Service;
+import resources.server_info.Config;
+import resources.server_info.Log;
+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.HttpsServer;
+import utilities.ThreadUtilities;
+
+public class OnlineInterfaceService extends Service implements HttpServerCallback {
+
+ private static final String TAG = "OnlineInterfaceService";
+
+ private final WebserverData data;
+ private final WebserverHandler handler;
+ private final Runnable dataCollectionRunnable;
+ private ScheduledExecutorService executor;
+ private HttpsServer httpsServer;
+ private HttpServer httpServer;
+ private boolean authorized;
+
+ public OnlineInterfaceService() {
+ data = new WebserverData();
+ handler = new WebserverHandler(data);
+ dataCollectionRunnable = () -> collectData();
+ authorized = false;
+ }
+
+ @Override
+ public boolean initialize() {
+ Config network = getConfig(ConfigFile.NETWORK);
+ authorized = !network.getString("HTTPS-KEYSTORE-PASSWORD", "").isEmpty();
+ if (!authorized)
+ return super.initialize();
+ httpServer = new HttpServer(getBindAddr(network, "HTTP-BIND-ADDR", "BIND-ADDR"), network.getInt("HTTP-PORT", 8080));
+ httpsServer = new HttpsServer(getBindAddr(network, "HTTPS-BIND-ADDR", "BIND-ADDR"), network.getInt("HTTPS-PORT", 8081));
+ httpServer.setMaxConnections(network.getInt("HTTP-MAX-CONNECTIONS", 2));
+ httpsServer.setMaxConnections(network.getInt("HTTPS-MAX-CONNECTIONS", 5));
+ if (!httpsServer.initialize(network)) {
+ System.err.println("Failed to initialize HTTPS server! Incorrect password?");
+ httpServer.stop();
+ httpsServer.stop();
+ super.initialize();
+ return false;
+ }
+ executor = Executors.newSingleThreadScheduledExecutor(ThreadUtilities.newThreadFactory("ServerInterface-DataCollection"));
+ registerForIntent(PlayerEventIntent.TYPE);
+ return super.initialize();
+ }
+
+ @Override
+ public boolean start() {
+ if (authorized) {
+ httpServer.setServerCallback(this);
+ httpsServer.setServerCallback(this);
+ httpServer.start();
+ httpsServer.start();
+ executor.scheduleAtFixedRate(dataCollectionRunnable, 0, 1, TimeUnit.SECONDS);
+ }
+ return super.start();
+ }
+
+ @Override
+ public boolean stop() {
+ if (authorized) {
+ httpServer.stop();
+ httpsServer.stop();
+ executor.shutdownNow();
+ try {
+ executor.awaitTermination(1, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ return super.stop();
+ }
+
+ @Override
+ public void onSocketCreated(HttpSocket socket) {
+ Log.i(TAG, "Received connection from: %s:%d [%s]", socket.getInetAddress(), socket.getPort(), socket.isSecure() ? "secure" : "insecure");
+ }
+
+ @Override
+ public void onRequestReceived(HttpSocket socket, HttpRequest request) {
+ try {
+ if (!request.getType().equals("GET")) {
+ socket.send(HttpStatusCode.METHOD_NOT_ALLOWED);
+ return;
+ }
+ if (!socket.isSecure()) {
+ socket.redirect(new URL("https", httpsServer.getBindAddress().getHostName(), httpsServer.getBindPort(), request.getURI().getPath()).toString());
+ return;
+ }
+ handler.handleRequest(socket, request);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void onIntentReceived(Intent i) {
+ if (i instanceof PlayerEventIntent) {
+ PlayerEventIntent pei = (PlayerEventIntent) i;
+ switch (pei.getEvent()) {
+ case PE_ZONE_IN:
+ data.addOnlinePlayer(pei.getPlayer());
+ break;
+ case PE_LOGGED_OUT:
+ data.removeOnlinePlayer(pei.getPlayer());
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ private void collectData() {
+ Runtime r = Runtime.getRuntime();
+ double memUsage = 1 - ((double) r.totalMemory() / r.maxMemory());
+ data.addMemoryUsageData(memUsage);
+ }
+
+ private InetAddress getBindAddr(Config c, String firstTry, String secondTry) {
+ String t = firstTry;
+ try {
+ if (c.containsKey(firstTry))
+ return InetAddress.getByName(c.getString(firstTry, "127.0.0.1"));
+ t = secondTry;
+ if (c.containsKey(secondTry))
+ return InetAddress.getByName(c.getString(secondTry, "127.0.0.1"));
+ } catch (UnknownHostException e) {
+ System.err.println("NetworkListenerService: Unknown host for IP: " + t);
+ }
+ return null;
+ }
+
+}
diff --git a/src/services/admin/WebserverData.java b/src/services/admin/WebserverData.java
new file mode 100644
index 00000000..5ce3c162
--- /dev/null
+++ b/src/services/admin/WebserverData.java
@@ -0,0 +1,60 @@
+package services.admin;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import resources.player.Player;
+
+class WebserverData {
+
+ private static final Charset ASCII = Charset.forName("ASCII");
+
+ private final double [] memoryUsage;
+ private final Set onlinePlayers;
+
+ public WebserverData() {
+ memoryUsage = new double[60 * 5];
+ onlinePlayers = new HashSet<>();
+ }
+
+ public void addMemoryUsageData(double percent) {
+ for (int i = 1; i < memoryUsage.length; i++)
+ memoryUsage[i-1] = memoryUsage[i];
+ memoryUsage[memoryUsage.length-1] = percent;
+ }
+
+ public void addOnlinePlayer(Player player) {
+ onlinePlayers.add(player);
+ }
+
+ public void removeOnlinePlayer(Player player) {
+ onlinePlayers.remove(player);
+ }
+
+ public double [] getMemoryUsage() {
+ return Arrays.copyOf(memoryUsage, memoryUsage.length);
+ }
+
+ public Set getOnlinePlayers() {
+ return Collections.unmodifiableSet(onlinePlayers);
+ }
+
+ public String getLog() throws IOException {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(new File("log.txt")), ASCII));
+ StringBuilder builder = new StringBuilder();
+ while (reader.ready()) {
+ builder.append(reader.readLine() + System.lineSeparator());
+ }
+ reader.close();
+ return builder.toString();
+ }
+
+}
diff --git a/src/services/admin/WebserverHandler.java b/src/services/admin/WebserverHandler.java
new file mode 100644
index 00000000..6cb36926
--- /dev/null
+++ b/src/services/admin/WebserverHandler.java
@@ -0,0 +1,205 @@
+package services.admin;
+
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.Graphics2D;
+import java.awt.RenderingHints;
+import java.awt.image.BufferedImage;
+import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.Charset;
+import java.util.Locale;
+import java.util.Set;
+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.HttpSocket;
+import services.admin.http.HttpSocket.HttpRequest;
+import services.admin.http.HttpStatusCode;
+
+class WebserverHandler {
+
+ private static final String TAG = "WebserverHandler";
+ private static final Charset ASCII = Charset.forName("ASCII");
+
+ private final WebserverData data;
+ private final Pattern variablePattern;
+
+ public WebserverHandler(WebserverData data) {
+ this.data = data;
+ variablePattern = Pattern.compile("\\$\\{.+\\}"); // Looks for variables: ${VAR_NAME}
+ }
+
+ public void handleRequest(HttpSocket socket, HttpRequest request) throws IOException {
+ Log.i(TAG, "Requested: " + request.getURI());
+ String file = request.getURI().toASCIIString();
+ if (file.contains("?"))
+ file = file.substring(0, file.indexOf('?'));
+ switch (file) {
+ case "/memory_usage.png":
+ socket.send(createMemoryUsage(), HttpImageType.PNG);
+ break;
+ default: {
+ byte [] response = parseFile(file);
+ if (response == null)
+ socket.send(HttpStatusCode.NOT_FOUND, request.getURI() + " is not found!");
+ else
+ socket.send(getFileType(file), response);
+ break;
+ }
+ }
+ }
+
+ private BufferedImage createMemoryUsage() {
+ double [] memoryUsage = data.getMemoryUsage();
+ final int graphHeight = 300;
+ BufferedImage image = new BufferedImage(900, graphHeight + 25, BufferedImage.TYPE_3BYTE_BGR);
+ Graphics2D g = image.createGraphics();
+ int prevX = -1;
+ int prevY = -1;
+ Font font = g.getFont();
+ g.setFont(font.deriveFont(Math.min(image.getHeight()/10.0f, 25.0f)));
+ RenderingHints rh = new RenderingHints(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+ g.setRenderingHints(rh);
+ g.setColor(Color.WHITE);
+ g.drawRect(0, 0, image.getWidth()-1, graphHeight-1);
+ g.drawLine(0, image.getHeight()-1, image.getWidth(), image.getHeight()-1);
+ for (int i = 1; i < 10; i++) {
+ g.drawLine(0, (int) ((i/10.0)*graphHeight), 10, (int) ((i/10.0)*graphHeight));
+ }
+ String inUse = getMemoryInUse();
+ int width = g.getFontMetrics().stringWidth(inUse);
+ g.drawString("Current: " + (int) (memoryUsage[memoryUsage.length-1] * 100) + "%", 5, image.getHeight() - 5);
+ g.drawString(inUse, image.getWidth()-width - 5, image.getHeight() - 5);
+ g.setColor(Color.RED);
+ int start = 0;
+ for (double d : memoryUsage) {
+ if (d == 0)
+ start++;
+ else
+ break;
+ }
+ for (int i = start; i < memoryUsage.length; i++) {
+ int x = (int) ((double) i / memoryUsage.length * image.getWidth());
+ int y = (int) ((1-memoryUsage[i]) * graphHeight);
+ if (prevX != -1 && prevY != -1)
+ g.drawLine(prevX, prevY, x, y);
+ prevX = x;
+ prevY = y;
+ }
+ return image;
+ }
+
+ private String getMemoryInUse() {
+ double memory = Runtime.getRuntime().totalMemory();
+ String [] types = new String[]{"B", "KB", "MB", "GB"};
+ String type = types[0];
+ for (int i = 0; i < types.length && memory >= 1024; i++) {
+ memory /= 1024;
+ type = types[i];
+ }
+ return String.format("In-Use: %.2f%s", memory, type);
+ }
+
+ private String getFileType(String filepath) {
+ if (!filepath.contains("."))
+ return "text/html";
+ String type = filepath.substring(filepath.lastIndexOf('.')+1).toLowerCase(Locale.US);
+ switch (type) {
+ case "png":
+ case "gif":
+ case "jpg":
+ case "jpeg":
+ case "ico":
+ return "image/" + type;
+ default:
+ return "text/" + type;
+ }
+ }
+
+ private byte [] parseFile(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))
+ return null;
+ if (type.equalsIgnoreCase("text/html"))
+ return parseHtmlFile(file).getBytes(ASCII);
+ try (InputStream is = new FileInputStream(file)) {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream(is.available());
+ byte [] buffer = new byte[Math.min(1024, is.available())];
+ while (is.available() > 0) {
+ if (is.available() > buffer.length && buffer.length < 1024)
+ buffer = new byte[Math.min(1024, is.available())];
+ int len = is.read(buffer);
+ baos.write(buffer, 0, len);
+ }
+ return baos.toByteArray();
+ }
+ }
+
+ private boolean verifyPath(File file) {
+ File parent = new File("res/webserver");
+ if (!file.getAbsolutePath().startsWith(parent.getAbsolutePath()))
+ return false;
+ if (!file.isFile())
+ return false;
+ return true;
+ }
+
+ private String parseHtmlFile(File file) throws IOException {
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), ASCII))) {
+ String line = null;
+ StringBuilder builder = new StringBuilder();
+ while (reader.ready()) {
+ line = reader.readLine();
+ if (line == null)
+ break;
+ Matcher matcher = variablePattern.matcher(line);
+ while (matcher.find()) {
+ String var = matcher.group();
+ var = var.substring(2, var.length()-1);
+ line = line.replaceAll("\\$\\{"+var+"\\}", getVariable(var));
+ matcher.reset(line);
+ }
+ builder.append(line + System.lineSeparator());
+ }
+ return builder.toString();
+ }
+ }
+
+ private String getVariable(String var) throws IOException {
+ var = var.toLowerCase(Locale.US);
+ switch (var) {
+ case "log":
+ return data.getLog().replace("\n", "\n
");
+ case "online_players": {
+ Set players = data.getOnlinePlayers();
+ StringBuilder ret = new StringBuilder("Online Players: ["+players.size()+"]
");
+ ret.append("| Username | User ID | Character | Character ID |
");
+ for (Player p : players) {
+ ret.append("");
+ ret.append(String.format("| %s | ", p.getUsername()));
+ ret.append(String.format("%d | ", p.getUserId()));
+ ret.append(String.format("%s | ", p.getCharacterName()));
+ ret.append(String.format("%d | ", p.getCreatureObject().getObjectId()));
+ ret.append("
");
+ }
+ ret.append("
");
+ return ret.toString();
+ }
+ default:
+ return "";
+ }
+ }
+
+}
diff --git a/src/services/admin/http/HttpImageType.java b/src/services/admin/http/HttpImageType.java
new file mode 100644
index 00000000..025018de
--- /dev/null
+++ b/src/services/admin/http/HttpImageType.java
@@ -0,0 +1,7 @@
+package services.admin.http;
+
+public enum HttpImageType {
+ GIF,
+ PNG,
+ JPG
+}
diff --git a/src/services/admin/http/HttpServer.java b/src/services/admin/http/HttpServer.java
new file mode 100644
index 00000000..b5f824e1
--- /dev/null
+++ b/src/services/admin/http/HttpServer.java
@@ -0,0 +1,196 @@
+package services.admin.http;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import resources.server_info.Log;
+import services.admin.http.HttpSocket.HttpRequest;
+import utilities.ThreadUtilities;
+
+public class HttpServer {
+
+ private static final String TAG = "HttpServer";
+
+ private final InetAddress addr;
+ private final int port;
+
+ private ExecutorService executor;
+ private ServerSocket serverSocket;
+ private AtomicInteger currentConnections;
+ private HttpServerCallback callback;
+ private int maxConnections;
+ private boolean secure;
+
+ protected HttpServer(InetAddress addr, int port, boolean secure) {
+ this.addr = addr;
+ this.port = port;
+ this.currentConnections = new AtomicInteger(0);
+ this.callback = null;
+ this.maxConnections = 2;
+ this.secure = secure;
+ }
+
+ public HttpServer(InetAddress addr, int port) {
+ this(addr, port, false);
+ }
+
+ public void start() {
+ executor = Executors.newCachedThreadPool(ThreadUtilities.newThreadFactory(getThreadFactoryName()));
+ try {
+ serverSocket = createSocket();
+ startAcceptThread(serverSocket, secure);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void stop() {
+ if (serverSocket != null) {
+ try {
+ serverSocket.close();
+ } catch (IOException e1) {
+ e1.printStackTrace();
+ }
+ }
+ if (executor != null) {
+ executor.shutdownNow();
+ try {
+ executor.awaitTermination(1, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ public void setServerCallback(HttpServerCallback callback) {
+ this.callback = callback;
+ }
+
+ public void setMaxConnections(int maxConnections) {
+ this.maxConnections = maxConnections;
+ }
+
+ public final InetAddress getBindAddress() {
+ return addr;
+ }
+
+ public final int getBindPort() {
+ return port;
+ }
+
+ protected String getThreadFactoryName() {
+ return "HttpServer-%d";
+ }
+
+ protected ServerSocket createSocket() throws IOException {
+ return new ServerSocket(port, 0, addr);
+ }
+
+ private void startAcceptThread(ServerSocket serverSocket, boolean secure) {
+ executor.submit(() -> {
+ try {
+ acceptThread(serverSocket, secure);
+ } catch (Throwable t) {
+ t.printStackTrace();
+ }
+ });
+ }
+
+ private void startSocketThread(HttpSocket socket) {
+ executor.submit(() -> {
+ try {
+ socketThread(socket);
+ } catch (Throwable t) {
+ t.printStackTrace();
+ }
+ try {
+ socket.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ synchronized (currentConnections) {
+ currentConnections.decrementAndGet();
+ }
+ });
+ }
+
+ private void acceptThread(ServerSocket serverSocket, boolean secure) {
+ Log.i(TAG, "Now listening over HTTP%s. Address: %s:%d", secure?"S":"", serverSocket.getInetAddress(), serverSocket.getLocalPort());
+ while (serverSocket.isBound() && !serverSocket.isClosed()) {
+ try {
+ Socket socket = serverSocket.accept();
+ HttpSocket httpSocket = new HttpSocket(socket, secure);
+ if (!httpSocket.isOpened()) {
+ httpSocket.close();
+ continue;
+ }
+ if (currentConnections.get() >= maxConnections) {
+ httpSocket.send(HttpStatusCode.SERVICE_UNAVAILABLE, "The server has reached it's maximum connection limit: " + maxConnections);
+ socket.close();
+ } else {
+ if (currentConnections.incrementAndGet() > maxConnections) {
+ socket.close();
+ currentConnections.decrementAndGet();
+ } else {
+ startSocketThread(httpSocket);
+ }
+ }
+ } catch (SocketException e) {
+ if (serverSocket.isClosed())
+ break;
+ e.printStackTrace();
+ } catch (Throwable t) {
+ t.printStackTrace();
+ }
+ }
+ Log.i(TAG, "No longer listening over HTTP%s!", secure?"S":"");
+ }
+
+ private void socketThread(HttpSocket socket) throws IOException {
+ onSocketCreated(socket);
+ HttpRequest request;
+ while (socket.isOpened() && !socket.isClosed()) {
+ request = socket.waitForRequest();
+ if (request == null)
+ break;
+ if (request.getType() == null || request.getURI() == null || request.getHttpVersion() == null) {
+ socket.send(HttpStatusCode.BAD_REQUEST);
+ continue;
+ }
+ onRequestReceived(socket, request);
+ }
+ }
+
+ private void onSocketCreated(HttpSocket socket) {
+ if (callback == null)
+ return;
+ try {
+ callback.onSocketCreated(socket);
+ } catch (Throwable t) {
+ t.printStackTrace();
+ }
+ }
+
+ private void onRequestReceived(HttpSocket socket, HttpRequest request) {
+ if (callback == null)
+ return;
+ try {
+ callback.onRequestReceived(socket, request);
+ } catch (Throwable t) {
+ t.printStackTrace();
+ }
+ }
+
+ public interface HttpServerCallback {
+ void onSocketCreated(HttpSocket socket);
+ void onRequestReceived(HttpSocket socket, HttpRequest request);
+ }
+
+}
diff --git a/src/services/admin/http/HttpSocket.java b/src/services/admin/http/HttpSocket.java
new file mode 100644
index 00000000..68ec22c9
--- /dev/null
+++ b/src/services/admin/http/HttpSocket.java
@@ -0,0 +1,263 @@
+package services.admin.http;
+
+import java.awt.image.BufferedImage;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.URI;
+import java.nio.charset.Charset;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import javax.imageio.ImageIO;
+
+import resources.server_info.Log;
+
+public class HttpSocket implements Closeable {
+
+ private static final Charset ASCII = Charset.forName("ASCII");
+
+ private final Socket socket;
+ private final BufferedReader reader;
+ private final BufferedWriter writer;
+ private final OutputStream rawOutputStream;
+ private final boolean secure;
+
+ public HttpSocket(Socket socket, boolean secure) {
+ this.socket = socket;
+ this.reader = createBufferedReader();
+ this.writer = createBufferedWriter();
+ this.rawOutputStream = getOutputStream();
+ this.secure = secure;
+ }
+
+ /**
+ * Waits for the next HTTP request to come in, then returns the request
+ * sent by the client - or null if the client closes the connection.
+ * @return the HttpRequest sent by the client, or null if the connection is
+ * closed
+ */
+ public HttpRequest waitForRequest() {
+ while (!socket.isClosed()) {
+ String line = readLine();
+ boolean hasData = line != null && !line.isEmpty();
+ String [] req = null;
+ Map params = new HashMap<>();
+ while (line != null && !line.isEmpty()) {
+ if (req == null)
+ req = line.split(" ", 3);
+ String [] parts = line.split(": ", 2);
+ if (parts.length == 2)
+ params.put(parts[0], parts[1]);
+ hasData = !line.isEmpty();
+ line = readLine();
+ }
+ if (hasData) {
+ String type = null;
+ URI uri = null;
+ String version = null;
+ if (req.length >= 1)
+ type = req[0];
+ if (req.length >= 2)
+ uri = URI.create(req[1]);
+ if (req.length >= 3)
+ version = req[2];
+ return new HttpRequest(type, uri, version, params);
+ }
+ if (line == null)
+ break;
+ }
+ return null;
+ }
+
+ public void redirect(String url) throws IOException {
+ Map params = new HashMap<>();
+ params.put("Location", url);
+ send(HttpStatusCode.MOVED_PERMANENTLY, params, "text/html", "");
+ }
+
+ public void send(HttpStatusCode code) throws IOException {
+ send(code, "");
+ }
+
+ public void send(String response) throws IOException {
+ send(HttpStatusCode.OK, response);
+ }
+
+ public void send(String contentType, byte [] response) throws IOException {
+ send(HttpStatusCode.OK, new HashMap<>(), contentType, response);
+ }
+
+ public void send(BufferedImage image, HttpImageType type) throws IOException {
+ String contentType = "image";
+ switch (type) {
+ case GIF:
+ contentType = "image/gif";
+ break;
+ case JPG:
+ contentType = "image/jpeg";
+ break;
+ case PNG:
+ contentType = "image/png";
+ break;
+ }
+ ByteArrayOutputStream baos = new ByteArrayOutputStream(image.getWidth() * image.getHeight() * 3);
+ if (!ImageIO.write(image, type.name().toLowerCase(Locale.US), baos))
+ throw new IllegalArgumentException("Cannot write image with type: " + type);
+ send(HttpStatusCode.OK, new HashMap<>(), contentType, baos.toByteArray());
+ }
+
+ public void send(HttpStatusCode code, String response) throws IOException {
+ if (code == HttpStatusCode.OK && response.isEmpty())
+ code = HttpStatusCode.NO_CONTENT;
+ send(code, new HashMap<>(), "text/html", response);
+ }
+
+ private void send(HttpStatusCode code, Map params, String contentType, String response) throws IOException {
+ params.put("Content-Length", Integer.toString(response.length()));
+ params.put("Content-Type", contentType);
+ sendHeader(code, params);
+ writer.write(response);
+ writer.flush();
+ }
+
+ private void send(HttpStatusCode code, Map params, String contentType, byte [] data) throws IOException {
+ params.put("Content-Length", Integer.toString(data.length));
+ params.put("Content-Type", contentType);
+ sendHeader(code, params);
+ writer.flush();
+ rawOutputStream.write(data);
+ rawOutputStream.flush();
+ }
+
+ private void sendHeader(HttpStatusCode code, Map params) throws IOException {
+ write("HTTP/1.1 %d %s", code.getCode(), code.getName());
+ for (Entry param : params.entrySet())
+ write(param.getKey() + ": " + param.getValue());
+ writer.newLine();
+ }
+
+ private String readLine() {
+ try {
+ return reader.readLine();
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ socket.close();
+ }
+
+ public boolean isOpened() {
+ return reader != null && writer != null;
+ }
+
+ public boolean isClosed() {
+ return socket.isClosed();
+ }
+
+ public boolean isSecure() {
+ return secure;
+ }
+
+ public InetAddress getInetAddress() {
+ return socket.getInetAddress();
+ }
+
+ public int getPort() {
+ return socket.getPort();
+ }
+
+ private void write(String str, Object ... args) throws IOException {
+ writer.write(String.format(str, args) + System.lineSeparator());
+ Log.d("HttpSocket", str, args);
+ }
+
+ private BufferedReader createBufferedReader() {
+ try {
+ return new BufferedReader(new InputStreamReader(socket.getInputStream(), ASCII));
+ } catch (IOException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ private BufferedWriter createBufferedWriter() {
+ try {
+ return new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), ASCII));
+ } catch (IOException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ private OutputStream getOutputStream() {
+ try {
+ return socket.getOutputStream();
+ } catch (IOException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ public static class HttpRequest {
+ private final String type;
+ private final URI uri;
+ private final String httpVersion;
+ private final Map params;
+
+ private HttpRequest(String requestType, URI uri, String httpVersion, Map params) {
+ this.type = requestType;
+ this.uri = uri;
+ this.httpVersion = httpVersion;
+ this.params = params;
+ }
+
+ /**
+ * Gets the type of HTTP request sent. It could be one of: GET, POST,
+ * HEAD.
+ * @return the type of HTTP request
+ */
+ public String getType() {
+ return type;
+ }
+
+ /**
+ * Gets the requested URI sent
+ * @return the requested URI
+ */
+ public URI getURI() {
+ return uri;
+ }
+
+ /**
+ * Gets the HTTP version of the client, typical format is: HTTP/1.1
+ * @return the HTTP version of the client
+ */
+ public String getHttpVersion() {
+ return httpVersion;
+ }
+
+ /**
+ * Gets all the paramters sent in the HTTP request, such as:
+ * "User-Agent", "Accept", etc.
+ * @return the HTTP request parameters
+ */
+ public Map getParams() {
+ return Collections.unmodifiableMap(params);
+ }
+ }
+
+}
diff --git a/src/services/admin/http/HttpStatusCode.java b/src/services/admin/http/HttpStatusCode.java
new file mode 100644
index 00000000..0f9de5cf
--- /dev/null
+++ b/src/services/admin/http/HttpStatusCode.java
@@ -0,0 +1,48 @@
+package services.admin.http;
+
+public enum HttpStatusCode {
+ /** The request is OK (this is the standard response for successful HTTP requests) */
+ OK (200, "OK"),
+ /** The request has been successfully processed, but is not returning any content */
+ NO_CONTENT (204, "No Content"),
+ /** The requested page has moved to a new URL */
+ MOVED_PERMANENTLY (301, "Moved Permanently"),
+ /** The requested page has moved temporarily to a new URL */
+ FOUND (302, "Found"),
+ /** The requested page can be found under a different URL */
+ SEE_OTHER (303, "See Other"),
+ /** The requested page has moved temporarily to a new URL */
+ TEMPORARY_REDIRECT (307, "Temporary Redirect"),
+ /** The request cannot be fulfilled due to bad syntax */
+ BAD_REQUEST (400, "Bad Request"),
+ /** The request was a legal request, but the server is refusing to respond to it.
+ For use when authentication is possible but has failed or not yet been provided */
+ UNAUTHORIZED (401, "Unauthorized"),
+ /** The request was a legal request, but the server is refusing to respond to it */
+ FORBIDDEN (403, "Forbidden"),
+ /** The requested page could not be found but may be available again in the future */
+ NOT_FOUND (404, "Not Found"),
+ /** A request was made of a page using a request method not supported by that page */
+ METHOD_NOT_ALLOWED (405, "Method Not Allowed"),
+ /** A generic error message, given when no more specific message is suitable */
+ INTERNAL_ERROR (500, "Internal Error"),
+ /** The server is currently unavailable (overloaded or down) */
+ SERVICE_UNAVAILABLE (503, "");
+
+ private int code;
+ private String name;
+
+ HttpStatusCode(int code, String name) {
+ this.code = code;
+ this.name = name;
+ }
+
+ public int getCode() {
+ return code;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+}
diff --git a/src/services/admin/http/HttpsServer.java b/src/services/admin/http/HttpsServer.java
new file mode 100644
index 00000000..449c5702
--- /dev/null
+++ b/src/services/admin/http/HttpsServer.java
@@ -0,0 +1,81 @@
+package services.admin.http;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLServerSocketFactory;
+
+import resources.server_info.Config;
+import resources.server_info.Log;
+
+public class HttpsServer extends HttpServer {
+
+ private static final String SERVER_KEYSTORE = "server.keystore";
+
+ private SSLContext sslContext;
+ private SSLServerSocketFactory sslServerSocketFactory;
+
+ public HttpsServer(InetAddress addr, int port) {
+ super(addr, port, true);
+ }
+
+ public boolean initialize(Config config) {
+ String pass = config.getString("HTTPS-KEYSTORE-PASSWORD", "");
+ System.setProperty("javax.net.ssl.keyStore", SERVER_KEYSTORE);
+ System.setProperty("javax.net.ssl.keyStorePassword", pass);
+ try {
+ sslContext = SSLContext.getInstance("TLSv1.2");
+ KeyManager [] managers = createKeyManagers(pass.toCharArray());
+ if (managers == null)
+ return false;
+ sslContext.init(managers, null, null);
+ sslServerSocketFactory = (SSLServerSocketFactory) SSLServerSocketFactory.getDefault();
+ return true;
+ } catch (Exception e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ protected String getThreadFactoryName() {
+ return "HttpsServer-%d";
+ }
+
+ protected ServerSocket createSocket() throws IOException {
+ try {
+ return sslServerSocketFactory.createServerSocket(getBindPort(), 0, getBindAddress());
+ } catch (IOException e) {
+ System.err.println("Failed to start HTTPS server!");
+ Log.e("HttpsServer", "Failed to start HTTPS server!");
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ private KeyManager [] createKeyManagers(char [] password) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException, UnrecoverableKeyException {
+ KeyStore ks = KeyStore.getInstance("JKS");
+ InputStream ksIs = new FileInputStream(SERVER_KEYSTORE);
+ try {
+ ks.load(ksIs, password);
+ } catch (IOException e) {
+ return null; // Password invalid
+ } finally {
+ ksIs.close();
+ }
+ KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+ kmf.init(ks, password);
+ return kmf.getKeyManagers();
+ }
+
+}
diff --git a/src/services/player/LoginService.java b/src/services/player/LoginService.java
index ad63d82f..903dd82a 100644
--- a/src/services/player/LoginService.java
+++ b/src/services/player/LoginService.java
@@ -228,7 +228,7 @@ public class LoginService extends Service {
String message = "Sorry, you're banned!";
sendPacket(player.getNetworkId(), new ErrorMessage(type, message, false));
System.err.println("[" + id.getUsername() + "] Can't login - Banned! IP: " + id.getAddress() + ":" + id.getPort());
- Log.i("LoginService", "%s cannot login due to a ban, from %s:%d", id.getAddress(), id.getPort());
+ Log.i("LoginService", "%s cannot login due to a ban, from %s:%d", player.getUsername(), id.getAddress(), id.getPort());
player.setPlayerState(PlayerState.DISCONNECTED);
new LoginEventIntent(player.getNetworkId(), LoginEvent.LOGIN_FAIL_BANNED).broadcast();
}