diff --git a/nge.backup b/nge.backup index e1c0fa569..a65cc13a0 100644 Binary files a/nge.backup and b/nge.backup differ diff --git a/res/webserver/authenticated.html b/res/webserver/authenticated.html new file mode 100644 index 000000000..a88c3fc3b --- /dev/null +++ b/res/webserver/authenticated.html @@ -0,0 +1,91 @@ + + + Game Server Diagnostics + + + + + +
+
+
+ Resource Usage:
+ +
+
+ ${ONLINE_PLAYERS} +
+ ${CHARACTER_INFO} +
+
+
+ ${LOG} +
+
+ + diff --git a/res/webserver/css/style.css b/res/webserver/css/style.css new file mode 100644 index 000000000..033b570d3 --- /dev/null +++ b/res/webserver/css/style.css @@ -0,0 +1,139 @@ +@charset "utf-8"; +/* CSS Document */ + +body, html { + width: 100%; + height: 100%; + font-family: "Helvetica Neue", Helvetica, sans-serif; + color: #444; + -webkit-font-smoothing: antialiased; + background: #f0f0f0; +} + +.wrapper{ + min-height:100%; + height:100%; +} +.logo{ + background-image:url(../images/pswg_server_management.png); + width: 200px; + height:210px; + margin:0 auto; +} +.login_container { + /* position: fixed;*/ + width: 340px; + height: 490px; + top: 50%; + left: 50%; + margin:0 auto; + /* margin-top: -140px; + margin-left: -170px;*/ + background: #fff; + border-radius: 3px; + border: 1px solid #ccc; + box-shadow: 0 1px 2px rgba(0, 0, 0, .1); + -webkit-animation-name: bounceIn; + -webkit-animation-fill-mode: both; + -webkit-animation-duration: 1s; + -webkit-animation-iteration-count: 1; + -webkit-animation-timing-function: linear; + -moz-animation-name: bounceIn; + -moz-animation-fill-mode: both; + -moz-animation-duration: 1s; + -moz-animation-iteration-count: 1; + -moz-animation-timing-function: linear; + animation-name: bounceIn; + animation-fill-mode: both; + animation-duration: 1s; + animation-iteration-count: 1; + animation-timing-function: linear; +} + +form { + margin: 0 auto; + margin-top: 20px; +} + +label { + color: #555; + display: inline-block; + margin-left: 18px; + padding-top: 10px; + font-size: 14px; +} + + + +input { + font-family: "Helvetica Neue", Helvetica, sans-serif; + font-size: 12px; + outline: none; +} + +input[type=text], +input[type=password] { + color: #777; + padding-left: 10px; + margin: 10px; + margin-top: 12px; + margin-left: 18px; + width: 290px; + height: 35px; + border: 1px solid #c7d0d2; + border-radius: 2px; + -webkit-transition: all .4s ease; + -moz-transition: all .4s ease; + transition: all .4s ease; +} + +#lower { + background: #ecf2f5; + width: 100%; + height: 63px; + margin-top: 20px; + box-shadow: inset 0 1px 1px #fff; + border-top: 1px solid #ccc; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} + +input[type=submit] { + float: right; + margin-right: 20px; + margin-top: 16px; + width: 80px; + height: 30px; + background-color: #acd6ef; /*IE fallback*/ + background-image: -webkit-gradient(linear, left top, left bottom, from(#acd6ef), to(#6ec2e8)); + background-image: -moz-linear-gradient(top left 90deg, #acd6ef 0%, #6ec2e8 100%); + background-image: linear-gradient(top left 90deg, #acd6ef 0%, #6ec2e8 100%); + border-radius: 30px; + border: 1px solid #66add6; + box-shadow: 0 1px 2px rgba(0, 0, 0, .3), inset 0 1px 0 rgba(255, 255, 255, .5); + cursor: pointer; +} + +input[type=text]:hover, +input[type=password]:hover { + border: 1px solid #b6bfc0; + box-shadow: inset 0 1.5px 3px rgba(190, 190, 190, .7), 0 0 0 5px #f5f7f8; +} + +input[type=text]:focus, +input[type=password]:focus { + border: 1px solid #a8c9e4; + box-shadow: inset 0 1.5px 3px rgba(190, 190, 190, .4), 0 0 0 5px #e6f2f9; +} + +input[type=submit]:hover { + background-image: -webkit-gradient(linear, left top, left bottom, from(#b6e2ff), to(#6ec2e8)); + background-image: -moz-linear-gradient(top left 90deg, #b6e2ff 0%, #6ec2e8 100%); + background-image: linear-gradient(top left 90deg, #b6e2ff 0%, #6ec2e8 100%); +} + +input[type=submit]:active { + background-image: -webkit-gradient(linear, left top, left bottom, from(#6ec2e8), to(#b6e2ff)); + background-image: -moz-linear-gradient(top left 90deg, #6ec2e8 0%, #b6e2ff 100%); + background-image: linear-gradient(top left 90deg, #6ec2e8 0%, #b6e2ff 100%); +} \ No newline at end of file diff --git a/res/webserver/favicon.ico b/res/webserver/favicon.ico new file mode 100644 index 000000000..5d39b59fe Binary files /dev/null and b/res/webserver/favicon.ico differ diff --git a/res/webserver/images/pswg_server_management.png b/res/webserver/images/pswg_server_management.png new file mode 100644 index 000000000..a9d6f9efa Binary files /dev/null and b/res/webserver/images/pswg_server_management.png differ diff --git a/res/webserver/index.html b/res/webserver/index.html new file mode 100644 index 000000000..acfb36e1e --- /dev/null +++ b/res/webserver/index.html @@ -0,0 +1,25 @@ + + + + + ProjectSWG Server Management Interface + + + + +
+ +
+ +
+ +
+
+
+ +
+
+
+
+ + diff --git a/server.keystore b/server.keystore new file mode 100644 index 000000000..45136856e Binary files /dev/null and b/server.keystore differ diff --git a/serverdata/map/starting_locations.sdb b/serverdata/map/starting_locations.sdb new file mode 100644 index 000000000..16254bc43 --- /dev/null +++ b/serverdata/map/starting_locations.sdb @@ -0,0 +1,31 @@ +description planet building cell x y z +TEXT PRIMARY KEY TEXT NOT NULL INTEGER TEXT INTEGER NOT NULL INTEGER NOT NULL INTEGER NOT NULL +coronet corellia -177 28 -4725 +vreni_island corellia -5459 23 -6239 +mining_outpost dantooine -614 3 2527 +warren dantooine -560 1 -3808 +aurilia dathomir 5304 78 -4135 +imperial_prison dathomir -6253 120 967 +dwb endor 5996314 4 0,87 12 30,98 +kachirho kashyyyyk_main 244 41 466 +imp_outpost lok -1759 12 -3085 +nym lok 420 7 5267 +nym_sp lok 424 5 5457 +deeja_peak naboo 5170 346 -1489 +kaadara naboo 5204 -192 6677 +keren naboo 1525 25 2765 +moenia naboo 4803 4 -4700 +theed naboo -4866 6 4154 +theed_palace naboo -5496 6 4369 +restuss rori 5289 80 6131 +dearic talus 336 6 -2930 +imp_outpost talus -2185 20 2287 +bestine tatooine -1284 12 -3591 +fort_tusken tatooine -3982 60 6313 +jabbas_inside tatooine 1177465 22 -10,28 2 43,95 +jabbas_outside tatooine -5846 90 -6182 +mos_eisley tatooine 3525 4 -4807 +mos_entha tatooine 1290 7 3138 +mos_espa tatooine -2907 5 2130 +mos_taike tatooine 3865 28 2353 +dark_enclave yavin4 3435626 17 -0,32 -47,42 -74,92 \ No newline at end of file diff --git a/src/resources/control/Manager.java b/src/resources/control/Manager.java index 6b1b01749..c4219e4e4 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 a1cb20412..000000000 --- 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 d27de8456..000000000 --- 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 49fba0b06..7ea7c0de3 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/resources/spawn/SpawnerType.java b/src/resources/spawn/SpawnerType.java index 093f6d4f6..768b79c10 100644 --- a/src/resources/spawn/SpawnerType.java +++ b/src/resources/spawn/SpawnerType.java @@ -29,7 +29,7 @@ package resources.spawn; public enum SpawnerType { - EGG("object/tangible/spawning/shared_spawn_egg.iff"); + EGG("object/tangible/ground_spawning/shared_patrol_waypoint.iff"); private String objectTemplate; diff --git a/src/services/CoreManager.java b/src/services/CoreManager.java index a5e567a34..d96797291 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 000000000..7280a812f --- /dev/null +++ b/src/services/admin/OnlineInterfaceService.java @@ -0,0 +1,209 @@ +package services.admin; + +import intents.PlayerEventIntent; + +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; +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.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 = ?"; + + 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; + private boolean authorized; + + public OnlineInterfaceService() { + data = new WebserverData(); + handler = new WebserverHandler(data); + dataCollectionRunnable = () -> collectData(); + getUser = getLocalDatabase().prepareStatement(GET_USER_SQL); + 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("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()); + 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() { + data.updateResourceUsage(); + } + + 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; + } + + private void login(HttpSocket socket, String username, String password) { + HttpSession session = socket.getSession(); + if (session == null) + return; + synchronized (getUser) { + try { + getUser.setString(1, username); + boolean prevAuthenticated = session.isAuthenticated(); + session.setAuthenticated(false); + try (ResultSet cursor = getUser.executeQuery()) { + session.setAuthenticated(cursor.next() && isUserValid(cursor, password)); + if (session.isAuthenticated()) { + if (!prevAuthenticated) + 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 new file mode 100644 index 000000000..2f989087d --- /dev/null +++ b/src/services/admin/WebserverData.java @@ -0,0 +1,104 @@ +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.lang.management.ManagementFactory; +import java.lang.management.OperatingSystemMXBean; +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 double [] cpuUsage; + private final double [] systemMemoryUsage; + private final double [] systemCpuUsage; + private final Set onlinePlayers; + + public WebserverData() { + memoryUsage = new double[60 * 5]; + cpuUsage = new double[memoryUsage.length]; + systemMemoryUsage = new double[memoryUsage.length]; + systemCpuUsage = new double[memoryUsage.length]; + onlinePlayers = new HashSet<>(); + } + + @SuppressWarnings("restriction") + public void updateResourceUsage() { + OperatingSystemMXBean os = ManagementFactory.getOperatingSystemMXBean(); + if (os instanceof com.sun.management.OperatingSystemMXBean) { + com.sun.management.OperatingSystemMXBean castedOS = (com.sun.management.OperatingSystemMXBean) os; + updateMemory(castedOS); + updateCPU(castedOS); + } + } + + 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 double [] getCpuUsage() { + return Arrays.copyOf(cpuUsage, cpuUsage.length); + } + + public double [] getSystemMemoryUsage() { + return Arrays.copyOf(systemMemoryUsage, systemMemoryUsage.length); + } + + public double [] getSystemCpuUsage() { + return Arrays.copyOf(systemCpuUsage, systemCpuUsage.length); + } + + public Set getOnlinePlayers() { + return Collections.unmodifiableSet(onlinePlayers); + } + + public String getLog() throws IOException { + try (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(); + } + } + + @SuppressWarnings("restriction") + private void updateMemory(com.sun.management.OperatingSystemMXBean os) { + Runtime r = Runtime.getRuntime(); + appendData(memoryUsage, (double) r.totalMemory() / os.getTotalPhysicalMemorySize()); + appendData(systemMemoryUsage, 1 - ((double) os.getFreePhysicalMemorySize() / os.getTotalPhysicalMemorySize())); + } + + @SuppressWarnings("restriction") + private void updateCPU(com.sun.management.OperatingSystemMXBean os) { + appendData(cpuUsage, os.getProcessCpuLoad()); + appendData(systemCpuUsage, os.getSystemCpuLoad()); + } + + private void appendData(double [] data, double value) { + for (int i = 1; i < data.length; i++) + data[i-1] = data[i]; + data[data.length-1] = value; + } + +} diff --git a/src/services/admin/WebserverHandler.java b/src/services/admin/WebserverHandler.java new file mode 100644 index 000000000..de8434fc5 --- /dev/null +++ b/src/services/admin/WebserverHandler.java @@ -0,0 +1,318 @@ +package services.admin; + +import java.awt.Color; +import java.awt.Font; +import java.awt.FontMetrics; +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.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import resources.Location; +import resources.objects.SWGObject; +import resources.objects.cell.CellObject; +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; + +class 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 { + String file = request.getURI().toASCIIString(); + Map getVariables = new HashMap<>(); + if (file.contains("?")) { + String vars = file.substring(file.indexOf('?')+1); + String [] variables = vars.split("&"); + for (String var : variables) { + String [] parts = var.split("=", 2); + if (parts.length == 2) + getVariables.put(parts[0], parts[1]); + } + file = file.substring(0, file.indexOf('?')); + } + if (file.contains("#")) + file = file.substring(0, file.indexOf('#')); + switch (file) { + case "/memory_usage.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(socket.getSession(), file, getVariables); + 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(); + Font font = g.getFont(); + g.setFont(font.deriveFont(Font.TRUETYPE_FONT, Math.min(image.getHeight()/10.0f, 25.0f))); + Map hints = new HashMap<>(); + hints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + hints.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + g.setRenderingHints(new RenderingHints(hints)); + g.setColor(Color.WHITE); + renderGraph(g, image.getWidth(), image.getHeight(), graphHeight); + FontMetrics fm = g.getFontMetrics(); + String inUse = getMemoryInUse(); + int width = fm.stringWidth(inUse); + int x = 5; + x += drawStr(g, image.getHeight(), x, "Proc:"); + g.drawString(inUse, image.getWidth()-width - 5, image.getHeight() - 5); + x += drawDataSet(g, Color.RED, " M:", image.getWidth(), image.getHeight(), graphHeight, x, memoryUsage); + x += drawDataSet(g, Color.CYAN, " C:", image.getWidth(), image.getHeight(), graphHeight, x, data.getCpuUsage()); + 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()); + drawDataSet(g, Color.GREEN, " C:", image.getWidth(), image.getHeight(), graphHeight, x, data.getSystemCpuUsage()); + return image; + } + + private void renderGraph(Graphics2D g, int width, int height, int graphHeight) { + g.drawRect(0, 0, width-1, graphHeight-1); + g.drawLine(0, height-1, width, height-1); + for (int i = 1; i < 10; i++) { + g.drawLine(0, (int) ((i/10.0)*graphHeight), 10, (int) ((i/10.0)*graphHeight)); + } + } + + private int drawDataSet(Graphics2D g, Color c, String pre, int width, int height, int graphHeight, int x, double [] data) { + g.setColor(c); + drawLines(g, data, width, graphHeight-1); + return drawStr(g, height, x, String.format("%s%.2f%%", pre, (int) (data[data.length-1] * 10000) / 100.0)); + } + + private int drawStr(Graphics2D g, int height, int x, String str) { + g.drawString(str, x, height - 5); + return g.getFontMetrics().stringWidth(str); + } + + private void drawLines(Graphics2D g, double [] data, int width, int height) { + int start = 0; + for (double d : data) { + if (d == 0) + start++; + else + break; + } + int prevX = -1; + int prevY = -1; + for (int i = start; i < data.length; i++) { + int x = (int) ((double) i / data.length * width); + int y = (int) ((1-data[i]) * height); + if (prevX != -1 && prevY != -1) + g.drawLine(prevX, prevY, x, y); + prevX = x; + prevY = y; + } + } + + 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(HttpSession session, String filepath, Map getVariables) 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)) { + 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() && !type.equals("text/css") && !type.equals("text/js") && !type.startsWith("image")) + return null; + if (type.equalsIgnoreCase("text/html")) + return parseHtmlFile(file, getVariables).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, Map getVariables) 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, getVariables)); + matcher.reset(line); + } + builder.append(line + System.lineSeparator()); + } + return builder.toString(); + } + } + + private String getVariable(String var, Map getVariables) throws IOException { + var = var.toLowerCase(Locale.US); + switch (var) { + case "log": + return data.getLog().replace("\n", "\n
"); + case "online_players": { + return getOnlinePlayerData(); + } + case "character_info": { + if (!getVariables.containsKey("character_id")) + return ""; + long i = Long.parseLong(getVariables.get("character_id")); + for (Player p : data.getOnlinePlayers()) { + if (p.getCreatureObject().getObjectId() == i) { + return getCreatureData(p.getCreatureObject()); + } + } + return "No Creature Found!"; + } + default: + return ""; + } + } + + private String getOnlinePlayerData() { + Set players = data.getOnlinePlayers(); + StringBuilder ret = new StringBuilder("Online Players: ["+players.size()+"]
"); + ret.append(""); + for (Player p : players) { + ret.append(""); + long id = p.getCreatureObject().getObjectId(); + ret.append(String.format("", id, p.getUsername())); + ret.append(String.format("", id, p.getUserId())); + ret.append(String.format("", id, p.getCharacterName())); + ret.append(String.format("", id, p.getCreatureObject().getObjectId())); + ret.append(""); + } + ret.append("
UsernameUser IDCharacterCharacter ID
%s%d%s%d
"); + return ret.toString(); + } + + private String getCreatureData(SWGObject creature) { + StringBuilder str = new StringBuilder(""); + Location world = creature.getWorldLocation(); + str.append(String.format(" Object ID: %s
", creature.getObjectId())); + str.append(String.format(" Name: %s
", creature.getName())); + str.append(String.format("World:
")); + str.append(addLocationData(world)); + if (creature.getParent() != null) { + Location cell = creature.getLocation(); + str.append(String.format("Local:
")); + str.append(addLocationData(cell)); + SWGObject parent = creature.getParent(); + if (parent != null) { + str.append(String.format(" Parent:
")); + str.append(addParentData(parent, " ")); + parent = parent.getParent(); + if (parent != null) { + str.append(String.format(" Grandparent:
")); + str.append(addParentData(parent, " ")); + } + } + } + return str.toString().replace(" ", " ").replace("", "
"); + } + + private String addParentData(SWGObject obj, String indent) { + StringBuilder str = new StringBuilder(""); + str.append(indent + String.format(" Object ID: %d
", obj.getObjectId())); + str.append(indent + String.format(" Template: %s
", obj.getTemplate())); + if (obj instanceof CellObject) { + str.append(indent + String.format(" Cell Index: %d
", ((CellObject) obj).getNumber())); + str.append(indent + String.format(" Cell Name: %s
", ((CellObject) obj).getCellName())); + } + return str.toString(); + } + + private String addLocationData(Location l) { + StringBuilder str = new StringBuilder(""); + str.append(String.format(" Location: %.2f, %.2f, %.2f [%s]
", l.getX(), l.getY(), l.getZ(), l.getTerrain())); + str.append(String.format("Orientation: %.2f, %.2f, %.2f, %.2f
", l.getOrientationX(), l.getOrientationY(), l.getOrientationZ(), l.getOrientationW())); + return str.toString(); + } + +} diff --git a/src/services/admin/http/HttpCookie.java b/src/services/admin/http/HttpCookie.java new file mode 100644 index 000000000..58c9fcba6 --- /dev/null +++ b/src/services/admin/http/HttpCookie.java @@ -0,0 +1,212 @@ +package services.admin.http; + +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; + +public class HttpCookie implements Comparable { + + private static final String EXPIRES_FORMAT = "EEE, dd-MM-yyyy HH:mm:ss zzz"; // Wed, 18-05-2015 16:00:00 GMT + + private String key; + private String value; + private String domain; + private String path; + private long expires; + private Set flags; + + public HttpCookie(String value, CookieFlag ... flags) { + if (value == null) + throw new IllegalArgumentException("Value for cookie cannot be null!"); + this.key = null; + this.value = value; + if (flags.length == 0) + this.flags = EnumSet.noneOf(CookieFlag.class); + else + this.flags = EnumSet.copyOf(Arrays.asList(flags)); + this.domain = null; + this.path = null; + this.expires = 0; + } + + public HttpCookie(String key, String value, CookieFlag ... flags) { + this(value, flags); + this.key = key; + } + + public HttpCookie(String key, String value, long expires, CookieFlag ... flags) { + this(key, value, flags); + this.expires = expires; + } + + public String getKey() { + return key; + } + + public String getValue() { + return value; + } + + public String getDomain() { + return domain; + } + + public String getPath() { + return path; + } + + public long getExpires() { + return expires; + } + + public Set getFlags() { + return Collections.unmodifiableSet(flags); + } + + public boolean hasFlag(CookieFlag flag) { + return flags.contains(flag); + } + + public boolean hasExpired() { + return expires != 0 && System.currentTimeMillis() >= expires; + } + + public void setKey(String key) { + this.key = key; + } + + public void setValue(String value) { + this.value = value; + } + + public void setDomain(String domain) { + this.domain = domain; + } + + public void setPath(String path) { + this.path = path; + } + + public void setExpires(long expires) { + this.expires = expires; + } + + public void addFlag(CookieFlag flag) { + flags.add(flag); + } + + public void removeFlag(CookieFlag flag) { + flags.remove(flag); + } + + @Override + public int compareTo(HttpCookie c) { + if (key != null && c.key == null) + return 1; + if (key == null && c.key != null) + return -1; + if (!value.equals(c.value)) + return value.compareTo(c.value); + if (expires < c.expires) + return -1; + if (expires > c.expires) + return 1; + return 0; + } + + @Override + public int hashCode() { + int code = 0; + if (key != null) + code += key.hashCode(); + code ^= value.hashCode(); + if (expires != 0) + code ^= Long.valueOf(expires).hashCode(); + return code; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) + return false; + if (!(obj instanceof HttpCookie)) + return false; + HttpCookie c = (HttpCookie) obj; + if (key != null && c.key == null) + return false; + if (c.key != null && key == null) + return false; + if (!c.value.equals(value)) + return false; + return c.expires == expires; + } + + public String encode() { + StringBuilder str = new StringBuilder(""); + if (key == null) + str.append(value); + else + str.append(encodeCookieString(key) + "=" + encodeCookieString(value)); + if (expires != 0) + str.append("; expires=" + encodeCookieString(new SimpleDateFormat(EXPIRES_FORMAT).format(expires))); + if (domain != null) + str.append("; domain=" + encodeCookieString(domain)); + if (path != null) + str.append("; path=" + encodeCookieString(path)); + for (CookieFlag flag : flags) + str.append("; " + flag.encode()); + return str.toString(); + } + + private String encodeCookieString(String str) { + return str.replace(" ", "%20").replace("\t", "%09").replace("\n", "%0A").replace("&", "%26").replace(";", "%3B"); + } + + public static HttpCookie [] decodeCookies(String str) { + String [] parts = str.split("; "); + HttpCookie [] cookies = new HttpCookie[parts.length]; + for (int i = 0; i < parts.length; i++) { + cookies[i] = decodeCookie(decodeCookieString(parts[i])); + } + return cookies; + } + + private static HttpCookie decodeCookie(String str) { + String [] parts = str.split("=", 2); + if (parts.length == 0) + return new HttpCookie(""); + if (parts.length == 1) + return new HttpCookie(parts[0]); + return new HttpCookie(parts[0], parts[1]); + } + + private static String decodeCookieString(String str) { + return str.replace("%20", " ").replace("%09", "\t").replace("%0A", "\n").replace("%26", "&").replace("%3B", ";"); + } + + public static enum CookieFlag { + /** + * Security feature where this cookie is only sent from the client + * over a secure HTTPS connection. + */ + SECURE ("secure"), + /** + * Security feature to avoid Cross Site Scripting (XSS), where this + * cookie is unable to be accessed through javascript - only via HTTP. + */ + HTTP_ONLY ("HttpOnly"); + + private String encoded; + + CookieFlag(String encoded) { + this.encoded = encoded; + } + + protected String encode() { + return encoded; + } + } + +} diff --git a/src/services/admin/http/HttpImageType.java b/src/services/admin/http/HttpImageType.java new file mode 100644 index 000000000..025018dec --- /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 000000000..244cbc84f --- /dev/null +++ b/src/services/admin/http/HttpServer.java @@ -0,0 +1,236 @@ +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.HashMap; +import java.util.Map; +import java.util.Random; +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 final Map sessions; + + 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.sessions = new HashMap<>(); + 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 HttpSession getSessionForRequest(HttpRequest request) { + String token = null; + for (HttpCookie cookie : request.getCookies()) { + if (cookie.getKey() != null && cookie.getKey().equals("sessionToken")) { + if (sessions.containsKey(cookie.getValue())) + token = cookie.getValue(); + } + } + if (token == null) + token = generateSessionToken(); + HttpSession session = sessions.get(token); + if (session == null) { + session = new HttpSession(token); + sessions.put(token, session); + } + return session; + } + + private String generateSessionToken() { + Random r = new Random(); + StringBuilder builder = new StringBuilder(24); + int rand; + for (int i = 0; i < 24; i++) { + rand = r.nextInt(26*2+10); + if (rand < 26) + builder.append((char) ('A' + rand)); + else if (rand < 52) + builder.append((char) ('a' + (rand-26))); + else + builder.append((char) ('0' + (rand-52))); + } + return builder.toString(); + } + + 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) { + socket.setSession(getSessionForRequest(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/HttpSession.java b/src/services/admin/http/HttpSession.java new file mode 100644 index 000000000..9d40bcfb4 --- /dev/null +++ b/src/services/admin/http/HttpSession.java @@ -0,0 +1,25 @@ +package services.admin.http; + +public class HttpSession { + + private final String token; + private boolean authenticated; + + public HttpSession(String token) { + this.token = token; + this.authenticated = false; + } + + public String getToken() { + return token; + } + + public boolean isAuthenticated() { + return authenticated; + } + + public void setAuthenticated(boolean authenticated) { + this.authenticated = authenticated; + } + +} diff --git a/src/services/admin/http/HttpSocket.java b/src/services/admin/http/HttpSocket.java new file mode 100644 index 000000000..9c5b19f9e --- /dev/null +++ b/src/services/admin/http/HttpSocket.java @@ -0,0 +1,335 @@ +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.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import javax.imageio.ImageIO; + +import resources.server_info.Log; +import services.admin.http.HttpCookie.CookieFlag; + +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; + private HttpSession session; + + public HttpSocket(Socket socket, boolean secure) { + this.socket = socket; + this.reader = createBufferedReader(); + this.writer = createBufferedWriter(); + this.rawOutputStream = getOutputStream(); + this.secure = secure; + this.session = null; + } + + /** + * 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<>(); + Set cookies = new HashSet<>(); + StringBuilder body = new StringBuilder(""); + while (line != null && !line.isEmpty()) { + if (req == null) + req = line.split(" ", 3); + String [] parts = line.split(": ", 2); + if (parts.length == 2) { + if (parts[0].equals("Cookie")) { + cookies.addAll(Arrays.asList(HttpCookie.decodeCookies(parts[1]))); + } else { + params.put(parts[0], parts[1]); + } + } + 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; + 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, cookies, body.toString()); + } + if (line == null) + break; + } + return null; + } + + public void setSession(HttpSession session) { + this.session = session; + } + + public HttpSession getSession() { + return session; + } + + public void redirect(String url) throws IOException { + Map params = new HashMap<>(); + params.put("Location", url); + send(HttpStatusCode.MOVED_PERMANENTLY, params, new HashSet<>(), "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<>(), new HashSet<>(), 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<>(), new HashSet<>(), 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<>(), new HashSet<>(), "text/html", response); + } + + private void send(HttpStatusCode code, Map params, Set cookies, String contentType, String response) throws IOException { + params.put("Content-Length", Integer.toString(response.length())); + params.put("Content-Type", contentType); + sendHeader(code, params, cookies); + writer.write(response); + writer.flush(); + } + + private void send(HttpStatusCode code, Map params, Set cookies, String contentType, byte [] data) throws IOException { + params.put("Content-Length", Integer.toString(data.length)); + params.put("Content-Type", contentType); + sendHeader(code, params, cookies); + writer.flush(); + rawOutputStream.write(data); + rawOutputStream.flush(); + } + + private void sendHeader(HttpStatusCode code, Map params, Set cookies) throws IOException { + if (session != null) + cookies.add(new HttpCookie("sessionToken", session.getToken(), CookieFlag.SECURE, CookieFlag.HTTP_ONLY)); + write("HTTP/1.1 %d %s", code.getCode(), code.getName()); + for (Entry param : params.entrySet()) + write(param.getKey() + ": " + param.getValue()); + for (HttpCookie cookie : cookies) + write("Set-Cookie: " + cookie.encode()); + writer.newLine(); + } + + private String readLine() { + try { + return reader.readLine(); + } catch (Exception e) { + return null; + } + } + + 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(); + } + + 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()); + } + + 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 final Set cookies; + private final String body; + + 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; + } + + /** + * 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); + } + + /** + * Gets all the cookies sent in the HTTP request + * @return the cookies + */ + public Set getCookies() { + return Collections.unmodifiableSet(cookies); + } + + /** + * Gets the request body + * @return the request body + */ + public String getBody() { + return body; + } + } + +} diff --git a/src/services/admin/http/HttpStatusCode.java b/src/services/admin/http/HttpStatusCode.java new file mode 100644 index 000000000..0f9de5cfc --- /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 000000000..449c57022 --- /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/objects/ObjectManager.java b/src/services/objects/ObjectManager.java index e5b269426..31176e40d 100644 --- a/src/services/objects/ObjectManager.java +++ b/src/services/objects/ObjectManager.java @@ -149,8 +149,8 @@ public class ObjectManager extends Manager { if (Terrain.doesTerrainExistForName(terrainStr)) terrain = Terrain.getTerrainFromName(terrainStr); else if (!terrainStr.isEmpty()) { - System.err.println("ObjectManager: Unknown terrain '" + terrain + "'"); - Log.e("ObjectManager", "Unknown terrain: %s", terrain); + System.err.println("ObjectManager: Unknown terrain '" + terrainStr + "'"); + Log.e("ObjectManager", "Unknown terrain: %s", terrainStr); } loadBuildouts(terrain); loadSnapshots(terrain); @@ -261,12 +261,7 @@ public class ObjectManager extends Manager { @Override public boolean terminate() { - database.traverse(new Traverser() { - @Override - public void process(SWGObject obj) { - obj.setOwner(null); - } - }); + database.traverse((obj) -> obj.setOwner(null)); database.close(); return super.terminate(); } @@ -279,6 +274,8 @@ public class ObjectManager extends Manager { Player p = ((PlayerEventIntent)i).getPlayer(); switch (((PlayerEventIntent)i).getEvent()) { case PE_DISAPPEAR: + if (p.getCreatureObject() == null) + break; p.getCreatureObject().clearAware(); objectAwareness.remove(p.getCreatureObject()); for (SWGObject obj : p.getCreatureObject().getObservers()) diff --git a/src/services/player/LoginService.java b/src/services/player/LoginService.java index ad63d82f2..903dd82a5 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(); } diff --git a/src/services/spawn/SpawnerService.java b/src/services/spawn/SpawnerService.java index b557ef481..a1b438bc3 100644 --- a/src/services/spawn/SpawnerService.java +++ b/src/services/spawn/SpawnerService.java @@ -66,7 +66,7 @@ public final class SpawnerService extends Service { public boolean initialize() { registerForIntent(ConfigChangedIntent.TYPE); - if (getConfig(ConfigFile.FEATURES).getBoolean("SPAWNERS-ENABLED", true)) + if (getConfig(ConfigFile.FEATURES).getBoolean("SPAWNERS-ENABLED", false)) loadSpawners(); return super.initialize();