Basic HTTPS server

This commit is contained in:
Obique PSWG
2015-08-24 17:27:45 -05:00
parent d6e6240ea2
commit 93cac8e971
20 changed files with 1124 additions and 445 deletions

BIN
res/webserver/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

81
res/webserver/index.html Normal file
View File

@@ -0,0 +1,81 @@
<html>
<head>
<title>Game Server Diagnostics</title>
<meta http-equiv="refresh" content="-1" >
<style type="text/css">
body {
background-color: #FFFFFF;
}
.main_container {
width: 75%;
color: #000000;
margin: 0 auto;
}
.memory_container {
width: 50%;
max-width: 50%;
max-height: 500px;
float: left;
color: #000000;
display: inline-block;
}
.log_container {
width: 100%;
height: 500px;
max-height: 500px;
color: #000000;
font-family: 'Courier New', Courier, 'Lucida Sans Typewriter', 'Lucida Typewriter', monospace;
font-size: 13px;
overflow: visible;
}
.server_info {
width: 50%;
max-width: 50%;
max-height: 500px;
float: right;
display: inline-block;
overflow: scroll;
}
</style>
<script src="https://code.jquery.com/jquery-1.10.2.js"></script>
<script>
getUrlParameter = function getUrlParameter(sParam) {
var sPageURL = decodeURIComponent(window.location.search.substring(1)), sURLVariables = sPageURL.split('&'), sParameterName, i;
for (i = 0; i < sURLVariables.length; i++) {
sParameterName = sURLVariables[i].split('=');
if (sParameterName[0] === sParam) {
return sParameterName[1] === undefined ? true : sParameterName[1];
}
}
return undefined;
};
function updateServerInfo() {
d = new Date();
$("#memory_usage").attr("src", "memory_usage.png?"+d.getTime())
$("#server_info").attr("src", "server_info.html");
}
$(document).ready(function() {
var time = getUrlParameter("refresh");
if (time === undefined)
time = 3000;
setInterval(updateServerInfo, time);
});
</script>
</head>
<body>
<div class="main_container">
<div width="100%">
<div class="memory_container">
Memory Usage:<br />
<image id="memory_usage" src="memory_usage.png" width="100%"/>
</div>
<iframe id="server_info" class="server_info" src="server_info.html" frameBorder="0" seamless="yes">
Browser does not support iframes!
</iframe>
</div>
<iframe id="log" class="log_container" src="log.html" frameBorder="0" seamless="yes">
Browser does not support iframes!
</iframe>
</div>
</body>
</html>

1
res/webserver/log.html Normal file
View File

@@ -0,0 +1 @@
${LOG}

View File

View File

@@ -0,0 +1,12 @@
<style type="text/css">
.online_players_table {
width: 100%;
}
.online_player_cell {
border: 1px solid black;
padding: 3px;
}
</style>
<div class="server_info">
${ONLINE_PLAYERS}
</div>

BIN
server.keystore Normal file

Binary file not shown.

View File

@@ -37,7 +37,6 @@ import java.util.List;
*/
public abstract class Manager extends Service {
private static final ServerManager serverManager = ServerManager.getInstance();
private List <Service> 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);
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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 <String, ServiceStats> serviceStats;
private ServerStatus status;
private ServerManager() {
serviceStats = new HashMap<String, ServiceStats>();
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 <ServiceControlTime> initTimes;
private final List <ServiceControlTime> startTimes;
private final List <ServiceControlTime> terminateTimes;
private final List <ServiceStats> children;
public ServiceStats(Service service) {
this.service = service;
initTimes = new ArrayList<ServiceControlTime>();
startTimes = new ArrayList<ServiceControlTime>();
terminateTimes = new ArrayList<ServiceControlTime>();
children = new ArrayList<ServiceStats>();
}
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 <ServiceControlTime> 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]";
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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; }
}
}

View File

@@ -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;
}
/**

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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<Player> 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<Player> 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();
}
}

View File

@@ -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<br />");
case "online_players": {
Set<Player> players = data.getOnlinePlayers();
StringBuilder ret = new StringBuilder("Online Players: ["+players.size()+"]<br />");
ret.append("<table class=\"online_players_table\"><tr><th>Username</th><th>User ID</th><th>Character</th><th>Character ID</th></tr>");
for (Player p : players) {
ret.append("<tr>");
ret.append(String.format("<td class=\"online_player_cell\">%s</td>", p.getUsername()));
ret.append(String.format("<td class=\"online_player_cell\">%d</td>", p.getUserId()));
ret.append(String.format("<td class=\"online_player_cell\">%s</td>", p.getCharacterName()));
ret.append(String.format("<td class=\"online_player_cell\">%d</td>", p.getCreatureObject().getObjectId()));
ret.append("</tr>");
}
ret.append("</table>");
return ret.toString();
}
default:
return "";
}
}
}

View File

@@ -0,0 +1,7 @@
package services.admin.http;
public enum HttpImageType {
GIF,
PNG,
JPG
}

View File

@@ -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);
}
}

View File

@@ -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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> params) throws IOException {
write("HTTP/1.1 %d %s", code.getCode(), code.getName());
for (Entry<String, String> 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<String, String> params;
private HttpRequest(String requestType, URI uri, String httpVersion, Map<String, String> 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<String, String> getParams() {
return Collections.unmodifiableMap(params);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}