diff --git a/.classpath b/.classpath index f8560c1..87e8f73 100644 --- a/.classpath +++ b/.classpath @@ -6,7 +6,6 @@ - @@ -29,5 +28,6 @@ + diff --git a/lib/nanohttpd-2.3.1.jar b/lib/nanohttpd-2.3.1.jar deleted file mode 100644 index 5f17a69..0000000 Binary files a/lib/nanohttpd-2.3.1.jar and /dev/null differ diff --git a/lib/nanohttpd-2.3.2-SNAPSHOT.jar b/lib/nanohttpd-2.3.2-SNAPSHOT.jar new file mode 100644 index 0000000..6b18cbf Binary files /dev/null and b/lib/nanohttpd-2.3.2-SNAPSHOT.jar differ diff --git a/src/com/projectswg/lightspeed/CommunicationService.java b/src/com/projectswg/lightspeed/CommunicationService.java index 83f5b77..627d57f 100644 --- a/src/com/projectswg/lightspeed/CommunicationService.java +++ b/src/com/projectswg/lightspeed/CommunicationService.java @@ -27,14 +27,30 @@ ***********************************************************************************/ package com.projectswg.lightspeed; +import java.io.File; +import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.security.KeyStore; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLServerSocketFactory; +import javax.net.ssl.TrustManagerFactory; + +import org.nanohttpd.protocols.http.ClientHandler; +import org.nanohttpd.protocols.http.IHTTPSession; +import org.nanohttpd.protocols.http.NanoHTTPD; +import org.nanohttpd.protocols.http.response.Response; +import org.nanohttpd.protocols.http.response.Status; +import org.nanohttpd.protocols.http.threading.IAsyncRunner; + import me.joshlarson.json.JSONObject; import com.projectswg.common.concurrency.PswgThreadPool; @@ -47,11 +63,6 @@ import com.projectswg.common.intents.RegisterHttpListenerIntent; import com.projectswg.common.network.packets.PacketType; import com.projectswg.lightspeed.communication.HttpResponder; -import fi.iki.elonen.NanoHTTPD; -import fi.iki.elonen.NanoHTTPD.IHTTPSession; -import fi.iki.elonen.NanoHTTPD.Response; -import fi.iki.elonen.NanoHTTPD.Response.Status; - public class CommunicationService extends LightspeedService { private final NanoHTTPD httpServer; @@ -75,6 +86,13 @@ public class CommunicationService extends LightspeedService { public boolean initialize() { Config config = getConfig(ConfigFile.LIGHTSPEED); try { + String pubkey = config.getString("PUBKEY", ""); + if (!pubkey.isEmpty()) { + String pass = config.getString("PUBKEY-PASS", ""); + File cert = new File("data", pubkey); + System.setProperty("javax.net.ssl.trustStore", cert.getAbsolutePath()); + httpServer.makeSecure(createSSLSocketFactory(cert, pass), null); + } runner.start(); httpServer.start(config.getInt("HTTP-TIMEOUT", 2000), false); } catch (IOException e) { @@ -91,6 +109,25 @@ public class CommunicationService extends LightspeedService { return super.terminate(); } + private SSLServerSocketFactory createSSLSocketFactory(File keystoreFile, String password) throws IOException { + try { + char [] passphrase = password.toCharArray(); + KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); + InputStream keystoreStream = new FileInputStream(keystoreFile); + + keystore.load(keystoreStream, passphrase); + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(keystore, passphrase); + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(keystore); + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null); + return ctx.getServerSocketFactory(); + } catch (Exception e) { + throw new IOException(e.getMessage()); + } + } + private Response serve(IHTTPSession session) { try { JSONObject params = getJSONObject(session); @@ -126,7 +163,7 @@ public class CommunicationService extends LightspeedService { JSONObject response = responder.request(request, params); if (response == null) return getNotFound("Invalid Request Parameters"); - return NanoHTTPD.newFixedLengthResponse(Status.OK, NanoHTTPD.MIME_PLAINTEXT, response.toString()); + return Response.newFixedLengthResponse(Status.OK, NanoHTTPD.MIME_PLAINTEXT, response.toString()); } private JSONObject getJSONObject(IHTTPSession session) { @@ -156,15 +193,15 @@ public class CommunicationService extends LightspeedService { } private Response getMethodNotAllowed() { - return NanoHTTPD.newFixedLengthResponse(Status.METHOD_NOT_ALLOWED, NanoHTTPD.MIME_PLAINTEXT, "Method Not Allowed"); + return Response.newFixedLengthResponse(Status.METHOD_NOT_ALLOWED, NanoHTTPD.MIME_PLAINTEXT, "Method Not Allowed"); } private Response getNotFound(String str) { - return NanoHTTPD.newFixedLengthResponse(Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, str); + return Response.newFixedLengthResponse(Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, str); } private Response getServerError(String str) { - return NanoHTTPD.newFixedLengthResponse(Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, str); + return Response.newFixedLengthResponse(Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, str); } private void handleRegisterListener(RegisterHttpListenerIntent rhli) { @@ -176,10 +213,10 @@ public class CommunicationService extends LightspeedService { return httpServer.getListeningPort(); } - private static class BoundRunner implements NanoHTTPD.AsyncRunner { + private static class BoundRunner implements IAsyncRunner { private final PswgThreadPool threadPool; - private final List running; + private final List running; public BoundRunner(int threads) { if (threads <= 0) @@ -203,21 +240,21 @@ public class CommunicationService extends LightspeedService { public void closeAll() { // copy of the list for concurrency synchronized (running) { - for (NanoHTTPD.ClientHandler clientHandler : running) { + for (ClientHandler clientHandler : running) { clientHandler.close(); } } } @Override - public void closed(NanoHTTPD.ClientHandler clientHandler) { + public void closed(ClientHandler clientHandler) { synchronized (running) { running.remove(clientHandler); } } @Override - public void exec(NanoHTTPD.ClientHandler clientHandler) { + public void exec(ClientHandler clientHandler) { threadPool.execute(clientHandler); synchronized (running) { running.add(clientHandler); diff --git a/src/com/projectswg/lightspeed_frontend/Frontend.java b/src/com/projectswg/lightspeed_frontend/Frontend.java index c22f84d..87ce91a 100644 --- a/src/com/projectswg/lightspeed_frontend/Frontend.java +++ b/src/com/projectswg/lightspeed_frontend/Frontend.java @@ -27,6 +27,8 @@ ***********************************************************************************/ package com.projectswg.lightspeed_frontend; +import java.io.File; +import java.io.IOException; import java.net.InetSocketAddress; import java.util.List; @@ -35,6 +37,7 @@ import com.projectswg.common.concurrency.PswgThreadPool; import com.projectswg.common.data.TestDetails; import com.projectswg.common.debug.Assert; import com.projectswg.common.debug.Log; +import com.projectswg.common.info.Config; import com.projectswg.common.network.packets.Packet; import com.projectswg.common.network.packets.request.RequestBuildDetailedPacket; import com.projectswg.common.network.packets.request.RequestBuildListPacket; @@ -62,13 +65,19 @@ public class Frontend { private final PswgScheduledThreadPool scheduledUpdater; private final FrontendCommunication communication; private final FrontendData data; + private final Config config; public Frontend() { this.threadPool = new PswgThreadPool(10, "frontend-thread-pool"); this.scheduledUpdater = new PswgScheduledThreadPool(1, "frontend-updater"); - this.communication = new FrontendCommunication(); - this.communication.setCallback(packet -> process(packet)); this.data = new FrontendData(); + File data = new File("cfg"); + data.mkdirs(); + File cfgFile = new File(data, "frontend.cfg"); + try { cfgFile.createNewFile(); } catch (IOException e) { Log.e(e); } + this.config = new Config(cfgFile); + this.communication = new FrontendCommunication(config); + this.communication.setCallback(packet -> process(packet)); } public void setAddress(InetSocketAddress address) { @@ -97,6 +106,10 @@ public class Frontend { return data; } + public Config getConfig() { + return config; + } + public void send(Packet p, ResponseCallback callback) { threadPool.execute(() -> { Packet response = communication.send(p); diff --git a/src/com/projectswg/lightspeed_frontend/LightspeedFrontendText.java b/src/com/projectswg/lightspeed_frontend/LightspeedFrontendText.java index fda5d52..501e79d 100644 --- a/src/com/projectswg/lightspeed_frontend/LightspeedFrontendText.java +++ b/src/com/projectswg/lightspeed_frontend/LightspeedFrontendText.java @@ -49,16 +49,18 @@ import com.projectswg.lightspeed_frontend.data.SharedServerData; public class LightspeedFrontendText { + private final Frontend frontend; private final FrontendCommunication communication; private final Queue inbound; private boolean running; public LightspeedFrontendText() { - communication = new FrontendCommunication(); - inbound = new ArrayDeque<>(); - running = false; - communication.setAddress(new InetSocketAddress(InetAddress.getLoopbackAddress(), 44444)); - communication.setCallback(packet -> { + this.frontend = new Frontend(); + this.communication = frontend.getCommunication(); + this.inbound = new ArrayDeque<>(); + this.running = false; + this.communication.setAddress(new InetSocketAddress(InetAddress.getLoopbackAddress(), 44444)); + this.communication.setCallback(packet -> { synchronized (inbound) { inbound.add(packet); inbound.notify(); diff --git a/src/com/projectswg/lightspeed_frontend/communication/FrontendCommunication.java b/src/com/projectswg/lightspeed_frontend/communication/FrontendCommunication.java index 80d2483..82c3303 100644 --- a/src/com/projectswg/lightspeed_frontend/communication/FrontendCommunication.java +++ b/src/com/projectswg/lightspeed_frontend/communication/FrontendCommunication.java @@ -27,11 +27,13 @@ ***********************************************************************************/ package com.projectswg.lightspeed_frontend.communication; +import java.io.File; import java.net.InetSocketAddress; import java.util.concurrent.atomic.AtomicBoolean; import com.projectswg.common.debug.Assert; import com.projectswg.common.debug.Log; +import com.projectswg.common.info.Config; import com.projectswg.common.network.packets.Packet; public class FrontendCommunication { @@ -40,10 +42,13 @@ public class FrontendCommunication { private ResponseCallback callback; private InetSocketAddress address; - public FrontendCommunication() { - running = new AtomicBoolean(false); - callback = null; - address = null; + public FrontendCommunication(Config config) { + this.running = new AtomicBoolean(false); + this.callback = null; + this.address = null; + String keystore = config.getString("KEYSTORE", ""); + if (!keystore.isEmpty()) + HttpClient.setupSSL(new File(keystore), config.getString("KEYSTORE-PASS", "")); } public InetSocketAddress getAddress() { diff --git a/src/com/projectswg/lightspeed_frontend/communication/HttpClient.java b/src/com/projectswg/lightspeed_frontend/communication/HttpClient.java index 1acaef7..7d4c140 100644 --- a/src/com/projectswg/lightspeed_frontend/communication/HttpClient.java +++ b/src/com/projectswg/lightspeed_frontend/communication/HttpClient.java @@ -28,15 +28,24 @@ package com.projectswg.lightspeed_frontend.communication; import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.net.ConnectException; -import java.net.HttpURLConnection; import java.net.InetSocketAddress; import java.net.URL; import java.net.URLEncoder; +import java.security.KeyStore; import java.util.Map.Entry; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; import me.joshlarson.json.JSON; import me.joshlarson.json.JSONObject; @@ -49,7 +58,35 @@ import com.projectswg.common.network.packets.request.RequestPacket; public class HttpClient { + private static final AtomicBoolean SSL = new AtomicBoolean(false); + + public static void setupSSL(File keystoreFile, String password) { + try { + InputStream keystoreStream = new FileInputStream(keystoreFile); + char[] trustPassword = password.toCharArray(); + KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); + + keystore.load(keystoreStream, trustPassword); + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(keystore, trustPassword); + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(keystore); + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null); + SSLContext.setDefault(ctx); + HttpsURLConnection.setDefaultHostnameVerifier((hostname, session) -> true); + SSL.set(true); + } catch (Exception e) { + Log.e(e); + SSL.set(false); + } + } + public static Packet requestPacket(InetSocketAddress address, Packet packet) { + if (!SSL.get()) { + Log.e("Cannot send packet - SSL not initialized yet!"); + return null; + } try { String str = requestString(address, packet); JSONObject obj = JSON.readObject(str, true); @@ -70,7 +107,7 @@ public class HttpClient { public static String requestString(InetSocketAddress address, Packet packet) throws IOException { URL url = new URL(generateURL(address, packet)); - HttpURLConnection con = (HttpURLConnection) url.openConnection(); + HttpsURLConnection con = (HttpsURLConnection) url.openConnection(); con.setRequestMethod((packet instanceof PostPacket) ? "POST" : "GET"); con.setRequestProperty("User-Agent", "LightspeedFrontend"); @@ -90,7 +127,7 @@ public class HttpClient { } private static String generateURL(InetSocketAddress address, Packet packet) throws UnsupportedEncodingException { - StringBuilder url = new StringBuilder("http://"+address.getAddress().getHostAddress()+":"+address.getPort()+"/?"); + StringBuilder url = new StringBuilder("https://"+address.getAddress().getHostAddress()+":"+address.getPort()+"/?"); JSONObject obj = packet.getJSON(); if (packet instanceof PostPacket) url.append("type=" + ((PostPacket) packet).getType().name().replace("POST_", "") + '&'); diff --git a/test/com/projectswg/lightspeed/frontend/LinearTestFrontendCommunication.java b/test/com/projectswg/lightspeed/frontend/LinearTestFrontendCommunication.java deleted file mode 100644 index 819110a..0000000 --- a/test/com/projectswg/lightspeed/frontend/LinearTestFrontendCommunication.java +++ /dev/null @@ -1,65 +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 com.projectswg.lightspeed.frontend; - -import java.util.concurrent.atomic.AtomicBoolean; - -import com.projectswg.lightspeed_frontend.communication.FrontendCommunication; - -public class LinearTestFrontendCommunication { - - private final FrontendCommunication communication; - private final AtomicBoolean connected; - - public LinearTestFrontendCommunication() { - communication = new FrontendCommunication(); - connected = new AtomicBoolean(false); - communication.start(); - } - - public void shutdown() { - communication.stop(); - } - - public boolean isConnected() { - return connected.get(); - } - - public boolean waitForConnectionState(boolean state, long timeout) { - while (connected.get() != state && timeout >= 0) { - try { - Thread.sleep(1); - } catch (InterruptedException e) { - return false; - } - timeout--; - } - return connected.get() == state; - } - -}