Added support for websocket connections (disabled by default)
This commit is contained in:
@@ -29,6 +29,7 @@
|
|||||||
* [Prerequisites](#prerequisites)
|
* [Prerequisites](#prerequisites)
|
||||||
* [Project Setup](#project-setup)
|
* [Project Setup](#project-setup)
|
||||||
* [Running the project](#running-the-project)
|
* [Running the project](#running-the-project)
|
||||||
|
* [Browser WebSocket Transport](#browser-websocket-transport)
|
||||||
* [License](#license)
|
* [License](#license)
|
||||||
* [Contact](#contact)
|
* [Contact](#contact)
|
||||||
|
|
||||||
@@ -125,6 +126,40 @@ Start the game server with the included run script. Use `./run -h` for more info
|
|||||||
|
|
||||||
Start the game server with `run-server.bat`
|
Start the game server with `run-server.bat`
|
||||||
|
|
||||||
|
#### Browser WebSocket Transport
|
||||||
|
|
||||||
|
Enable the listener in `Server/worldprops/default.conf`:
|
||||||
|
|
||||||
|
```properties
|
||||||
|
websocket_enabled = true
|
||||||
|
websocket_port = 0
|
||||||
|
```
|
||||||
|
|
||||||
|
When `websocket_port = 0`, the listener uses `53594 + world_id`. For world `1`, that is `53595`.
|
||||||
|
|
||||||
|
Both plain WebSocket (`ws://`) and secure WebSocket (`wss://`) are supported. Use `ws://` for local HTTP testing. PWA requires HTTPS/WSS.
|
||||||
|
|
||||||
|
Enable WSS with a Java keystore:
|
||||||
|
|
||||||
|
```properties
|
||||||
|
websocket_tls_enabled = true
|
||||||
|
websocket_tls_keystore_path = "certs/dev-wss.p12"
|
||||||
|
websocket_tls_keystore_password = "<keystore-password>"
|
||||||
|
```
|
||||||
|
|
||||||
|
The keystore password is optional. Leave `websocket_tls_keystore_password` blank when using a PKCS12 file exported with an empty password:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openssl pkcs12 -export \
|
||||||
|
-in fullchain.pem \
|
||||||
|
-inkey privkey.pem \
|
||||||
|
-out certs/dev-wss.p12 \
|
||||||
|
-name websocket \
|
||||||
|
-passout pass:
|
||||||
|
```
|
||||||
|
|
||||||
|
For local WSS development, create a certificate for your hostname or LAN IP that the browser will use, then point the server at a PKCS12 keystore. For production, use a normal certificate from certbot. Plain WS does not require a certificate.
|
||||||
|
|
||||||
#### Docker
|
#### Docker
|
||||||
|
|
||||||
Make sure [Docker Engine](https://docs.docker.com/engine/install/) & [Docker Compose](https://docs.docker.com/compose/install/) plugin are installed first:
|
Make sure [Docker Engine](https://docs.docker.com/engine/install/) & [Docker Compose](https://docs.docker.com/compose/install/) plugin are installed first:
|
||||||
|
|||||||
@@ -91,6 +91,11 @@
|
|||||||
<version>2.9.0</version>
|
<version>2.9.0</version>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.java-websocket</groupId>
|
||||||
|
<artifactId>Java-WebSocket</artifactId>
|
||||||
|
<version>1.5.7</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import core.game.system.SystemState
|
|||||||
import core.game.system.config.ServerConfigParser
|
import core.game.system.config.ServerConfigParser
|
||||||
import core.game.world.GameWorld
|
import core.game.world.GameWorld
|
||||||
import core.net.NioReactor
|
import core.net.NioReactor
|
||||||
|
import core.net.websocket.GameWebSocketServer
|
||||||
|
import core.net.websocket.WebSocketTls
|
||||||
import core.tools.Log
|
import core.tools.Log
|
||||||
import core.tools.NetworkReachability
|
import core.tools.NetworkReachability
|
||||||
import core.tools.TimeStamp
|
import core.tools.TimeStamp
|
||||||
@@ -44,6 +46,9 @@ object Server {
|
|||||||
@JvmStatic
|
@JvmStatic
|
||||||
var reactor: NioReactor? = null
|
var reactor: NioReactor? = null
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
var webSocketServer: GameWebSocketServer? = null
|
||||||
|
|
||||||
var networkReachability = NetworkReachability.Reachable
|
var networkReachability = NetworkReachability.Reachable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -70,6 +75,16 @@ object Server {
|
|||||||
try {
|
try {
|
||||||
reactor = NioReactor.configure(43594 + GameWorld.settings?.worldId!!)
|
reactor = NioReactor.configure(43594 + GameWorld.settings?.worldId!!)
|
||||||
reactor!!.start()
|
reactor!!.start()
|
||||||
|
if (ServerConstants.WEBSOCKET_ENABLED) {
|
||||||
|
val websocketPort = if (ServerConstants.WEBSOCKET_PORT > 0) {
|
||||||
|
ServerConstants.WEBSOCKET_PORT
|
||||||
|
} else {
|
||||||
|
53594 + GameWorld.settings?.worldId!!
|
||||||
|
}
|
||||||
|
webSocketServer = GameWebSocketServer(websocketPort, 1)
|
||||||
|
WebSocketTls.configure(webSocketServer!!)
|
||||||
|
webSocketServer!!.start()
|
||||||
|
}
|
||||||
} catch (e: BindException) {
|
} catch (e: BindException) {
|
||||||
log(this::class.java, Log.ERR, "Port " + (43594 + GameWorld.settings?.worldId!!) + " is already in use!")
|
log(this::class.java, Log.ERR, "Port " + (43594 + GameWorld.settings?.worldId!!) + " is already in use!")
|
||||||
throw e
|
throw e
|
||||||
|
|||||||
@@ -374,5 +374,20 @@ class ServerConstants {
|
|||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
var CONNECTIVITY_TIMEOUT = 500
|
var CONNECTIVITY_TIMEOUT = 500
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
var WEBSOCKET_ENABLED = false
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
var WEBSOCKET_PORT = 0
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
var WEBSOCKET_TLS_ENABLED = false
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
var WEBSOCKET_TLS_KEYSTORE_PATH = ""
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
var WEBSOCKET_TLS_KEYSTORE_PASSWORD = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ public final class SystemTermination {
|
|||||||
log(this.getClass(), Log.INFO, "Stopping all bots...");
|
log(this.getClass(), Log.INFO, "Stopping all bots...");
|
||||||
AIRepository.clearAllBots();
|
AIRepository.clearAllBots();
|
||||||
Server.getReactor().terminate();
|
Server.getReactor().terminate();
|
||||||
|
if (Server.getWebSocketServer() != null) {
|
||||||
|
Server.getWebSocketServer().stop(1000, "Server shutting down");
|
||||||
|
Server.setWebSocketServer(null);
|
||||||
|
}
|
||||||
log(this.getClass(), Log.INFO, "Stopping all pulses...");
|
log(this.getClass(), Log.INFO, "Stopping all pulses...");
|
||||||
GameWorld.getMajorUpdateWorker().stop();
|
GameWorld.getMajorUpdateWorker().stop();
|
||||||
for (Iterator<Player> it = Repository.getPlayers().iterator(); it.hasNext();) {
|
for (Iterator<Player> it = Repository.getPlayers().iterator(); it.hasNext();) {
|
||||||
|
|||||||
@@ -176,6 +176,11 @@ object ServerConfigParser {
|
|||||||
ServerConstants.BOOSTED_TRAWLER_REWARDS = data.getBoolean("world.boosted_trawler_rewards", false)
|
ServerConstants.BOOSTED_TRAWLER_REWARDS = data.getBoolean("world.boosted_trawler_rewards", false)
|
||||||
ServerConstants.CONNECTIVITY_CHECK_URL = data.getString("server.connectivity_check_url", "https://google.com,https://2009scape.org")
|
ServerConstants.CONNECTIVITY_CHECK_URL = data.getString("server.connectivity_check_url", "https://google.com,https://2009scape.org")
|
||||||
ServerConstants.CONNECTIVITY_TIMEOUT = data.getLong("server.connectivity_timeout", 500L).toInt()
|
ServerConstants.CONNECTIVITY_TIMEOUT = data.getLong("server.connectivity_timeout", 500L).toInt()
|
||||||
|
ServerConstants.WEBSOCKET_ENABLED = data.getBoolean("server.websocket_enabled", false)
|
||||||
|
ServerConstants.WEBSOCKET_PORT = data.getLong("server.websocket_port", 0L).toInt()
|
||||||
|
ServerConstants.WEBSOCKET_TLS_ENABLED = data.getBoolean("server.websocket_tls_enabled", false)
|
||||||
|
ServerConstants.WEBSOCKET_TLS_KEYSTORE_PATH = data.getString("server.websocket_tls_keystore_path", "")
|
||||||
|
ServerConstants.WEBSOCKET_TLS_KEYSTORE_PASSWORD = data.getString("server.websocket_tls_keystore_password", "")
|
||||||
|
|
||||||
val logLevel = data.getString("server.log_level", "VERBOSE").uppercase()
|
val logLevel = data.getString("server.log_level", "VERBOSE").uppercase()
|
||||||
ServerConstants.LOG_LEVEL = parseEnumEntry<LogLevel>(logLevel) ?: LogLevel.VERBOSE
|
ServerConstants.LOG_LEVEL = parseEnumEntry<LogLevel>(logLevel) ?: LogLevel.VERBOSE
|
||||||
|
|||||||
@@ -122,12 +122,45 @@ public class IoSession {
|
|||||||
* @param service The executor service.
|
* @param service The executor service.
|
||||||
*/
|
*/
|
||||||
public IoSession(SelectionKey key, ExecutorService service) {
|
public IoSession(SelectionKey key, ExecutorService service) {
|
||||||
|
this(key, service, resolveRemoteAddress(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code IoSession} with an explicit remote address.
|
||||||
|
* @param key The selection key.
|
||||||
|
* @param service The executor service.
|
||||||
|
* @param remoteAddress The remote address.
|
||||||
|
*/
|
||||||
|
public IoSession(SelectionKey key, ExecutorService service, String remoteAddress) {
|
||||||
this.key = key;
|
this.key = key;
|
||||||
this.service = service;
|
this.service = service;
|
||||||
this.address = getRemoteAddress().replaceAll("/", "").split(":")[0];
|
this.address = normalizeRemoteAddress(remoteAddress);
|
||||||
this.js5Queue = new JS5Queue(this);
|
this.js5Queue = new JS5Queue(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String resolveRemoteAddress(SelectionKey key) {
|
||||||
|
if (key == null || !(key.channel() instanceof SocketChannel)) {
|
||||||
|
return "127.0.0.1";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return ((SocketChannel) key.channel()).getRemoteAddress().toString();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalizeRemoteAddress(String remoteAddress) {
|
||||||
|
if (remoteAddress == null || remoteAddress.trim().isEmpty()) {
|
||||||
|
return "127.0.0.1";
|
||||||
|
}
|
||||||
|
String normalized = remoteAddress.replace("/", "");
|
||||||
|
int separator = normalized.lastIndexOf(':');
|
||||||
|
if (separator > 0) {
|
||||||
|
return normalized.substring(0, separator);
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fires a write event created using the current event producer.
|
* Fires a write event created using the current event producer.
|
||||||
* @param context The event context.
|
* @param context The event context.
|
||||||
@@ -213,9 +246,11 @@ public class IoSession {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
active = false;
|
active = false;
|
||||||
key.cancel();
|
if (key != null) {
|
||||||
SocketChannel channel = (SocketChannel) key.channel();
|
key.cancel();
|
||||||
channel.socket().close();
|
SocketChannel channel = (SocketChannel) key.channel();
|
||||||
|
channel.socket().close();
|
||||||
|
}
|
||||||
if (getPlayer() != null) {
|
if (getPlayer() != null) {
|
||||||
try {
|
try {
|
||||||
getPlayer().clear();
|
getPlayer().clear();
|
||||||
@@ -435,4 +470,4 @@ public class IoSession {
|
|||||||
this.clientInfo = clientInfo;
|
this.clientInfo = clientInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,6 +143,21 @@ public final class NioReactor implements Runnable {
|
|||||||
*/
|
*/
|
||||||
public void terminate() {
|
public void terminate() {
|
||||||
running = false;
|
running = false;
|
||||||
|
try {
|
||||||
|
if (channel != null) {
|
||||||
|
if (channel.getChannel() != null) {
|
||||||
|
channel.getChannel().close();
|
||||||
|
}
|
||||||
|
if (channel.getSocket() != null) {
|
||||||
|
channel.getSocket().close();
|
||||||
|
}
|
||||||
|
channel.getSelector().wakeup();
|
||||||
|
channel.getSelector().close();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
service.shutdownNow();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,6 @@ import core.ServerConstants
|
|||||||
import core.api.getAttribute
|
import core.api.getAttribute
|
||||||
import core.api.log
|
import core.api.log
|
||||||
import core.api.sendMessage
|
import core.api.sendMessage
|
||||||
import core.api.tryPop
|
|
||||||
import core.api.utils.Vector
|
import core.api.utils.Vector
|
||||||
import core.cache.def.impl.ItemDefinition
|
import core.cache.def.impl.ItemDefinition
|
||||||
import core.cache.def.impl.NPCDefinition
|
import core.cache.def.impl.NPCDefinition
|
||||||
@@ -62,21 +61,35 @@ import java.lang.Math.min
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
object PacketProcessor {
|
object PacketProcessor {
|
||||||
val queue = LinkedList<Packet>()
|
private var queue = LinkedList<Packet>()
|
||||||
|
private var processingQueue = LinkedList<Packet>()
|
||||||
|
private val queueLock = Any()
|
||||||
|
|
||||||
@JvmStatic fun enqueue(pkt: Packet) {
|
@JvmStatic fun enqueue(pkt: Packet) {
|
||||||
queue.addLast(pkt)
|
synchronized(queueLock) {
|
||||||
|
queue.addLast(pkt)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic fun processQueue() {
|
@JvmStatic fun processQueue() {
|
||||||
var countThisCycle = queue.size
|
synchronized(queueLock) {
|
||||||
|
if (queue.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val queued = queue
|
||||||
|
queue = processingQueue
|
||||||
|
processingQueue = queued
|
||||||
|
}
|
||||||
val sw = StringWriter()
|
val sw = StringWriter()
|
||||||
val pw = PrintWriter(sw)
|
val pw = PrintWriter(sw)
|
||||||
var pkt: Packet
|
var pkt: Packet
|
||||||
while (countThisCycle-- > 0) {
|
while (processingQueue.isNotEmpty()) {
|
||||||
pkt = queue.tryPop(Packet.NoProcess()) ?: return
|
pkt = processingQueue.pollFirst() ?: return
|
||||||
if (pkt is Packet.NoProcess) {
|
if (pkt is Packet.NoProcess) {
|
||||||
queue.clear()
|
synchronized(queueLock) {
|
||||||
|
queue.clear()
|
||||||
|
}
|
||||||
|
processingQueue.clear()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
package core.net.packet
|
package core.net.packet
|
||||||
|
|
||||||
import core.api.log
|
import core.api.log
|
||||||
import core.api.tryPop
|
|
||||||
import core.net.packet.out.*
|
import core.net.packet.out.*
|
||||||
import core.tools.Log
|
import core.tools.Log
|
||||||
import core.tools.SystemLogger
|
import core.tools.SystemLogger
|
||||||
import java.io.PrintWriter
|
import java.io.PrintWriter
|
||||||
import java.io.StringWriter
|
import java.io.StringWriter
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.locks.ReentrantLock
|
|
||||||
|
|
||||||
class PacketWriteQueue {
|
class PacketWriteQueue {
|
||||||
companion object {
|
companion object {
|
||||||
private val packetsToWrite = LinkedList<QueuedPacket<*>>()
|
private var packetsToWrite = LinkedList<QueuedPacket<*>>()
|
||||||
|
private var packetsToFlush = LinkedList<QueuedPacket<*>>()
|
||||||
|
private val queueLock = Any()
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun <T> handle(packet: OutgoingPacket<T>, context: T) {
|
fun <T> handle(packet: OutgoingPacket<T>, context: T) {
|
||||||
@@ -34,16 +34,25 @@ class PacketWriteQueue {
|
|||||||
log(this::class.java, Log.ERR, "${packet::class.java.simpleName} tried to queue with a null context!")
|
log(this::class.java, Log.ERR, "${packet::class.java.simpleName} tried to queue with a null context!")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
packetsToWrite.add(QueuedPacket(packet, context))
|
synchronized(queueLock) {
|
||||||
|
packetsToWrite.add(QueuedPacket(packet, context))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun flush() {
|
fun flush() {
|
||||||
var countThisCycle = packetsToWrite.size
|
synchronized(queueLock) {
|
||||||
|
if (packetsToWrite.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val queued = packetsToWrite
|
||||||
|
packetsToWrite = packetsToFlush
|
||||||
|
packetsToFlush = queued
|
||||||
|
}
|
||||||
val sw = StringWriter()
|
val sw = StringWriter()
|
||||||
val pw = PrintWriter(sw)
|
val pw = PrintWriter(sw)
|
||||||
while (countThisCycle-- > 0) {
|
while (packetsToFlush.isNotEmpty()) {
|
||||||
val pkt = packetsToWrite.tryPop(null) ?: continue
|
val pkt = packetsToFlush.pollFirst() ?: continue
|
||||||
try {
|
try {
|
||||||
write(pkt.out, pkt.context)
|
write(pkt.out, pkt.context)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package core.net.websocket
|
||||||
|
|
||||||
|
import core.api.log
|
||||||
|
import core.net.IoSession
|
||||||
|
import core.tools.Log
|
||||||
|
import org.java_websocket.WebSocket
|
||||||
|
import org.java_websocket.handshake.ClientHandshake
|
||||||
|
import org.java_websocket.server.WebSocketServer
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import java.util.concurrent.ExecutorService
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
|
class GameWebSocketServer(port: Int, poolSize: Int) : WebSocketServer(InetSocketAddress(port)) {
|
||||||
|
private val service: ExecutorService = Executors.newFixedThreadPool(poolSize) { runnable ->
|
||||||
|
Thread(runnable, "WebSocketIo").apply {
|
||||||
|
isDaemon = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private val sessions = ConcurrentHashMap<WebSocket, WebSocketIoSession>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
// This pool processes websocket frames; it is not a cap on connected webclients.
|
||||||
|
setTcpNoDelay(true)
|
||||||
|
setReuseAddr(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOpen(conn: WebSocket, handshake: ClientHandshake) {
|
||||||
|
sessions[conn] = WebSocketIoSession(conn, service)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClose(conn: WebSocket, code: Int, reason: String, remote: Boolean) {
|
||||||
|
val session = sessions.remove(conn)
|
||||||
|
if (session != null && session.isActive) {
|
||||||
|
session.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMessage(conn: WebSocket, message: String) {
|
||||||
|
log(javaClass, Log.WARN, "Closing websocket client ${conn.remoteSocketAddress}: text frames are unsupported.")
|
||||||
|
conn.close(1003, "Binary frames only")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMessage(conn: WebSocket, message: ByteBuffer) {
|
||||||
|
val session: IoSession = sessions[conn] ?: run {
|
||||||
|
conn.close(1011, "Session not initialized")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val copy = ByteBuffer.allocate(message.remaining())
|
||||||
|
copy.put(message)
|
||||||
|
copy.flip()
|
||||||
|
service.execute(session.producer.produceReader(session, copy))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(conn: WebSocket?, ex: Exception?) {
|
||||||
|
if (ex != null) {
|
||||||
|
log(javaClass, Log.ERR, "WebSocket server error: ${ex.message}")
|
||||||
|
}
|
||||||
|
if (conn != null) {
|
||||||
|
sessions.remove(conn)?.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
log(javaClass, Log.INFO, "WebSocket listener started on port $port.")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stop(timeout: Int, closeMessage: String) {
|
||||||
|
super.stop(timeout, closeMessage)
|
||||||
|
service.shutdownNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package core.net.websocket
|
||||||
|
|
||||||
|
import core.net.IoSession
|
||||||
|
import org.java_websocket.WebSocket
|
||||||
|
import org.java_websocket.exceptions.WebsocketNotConnectedException
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.util.concurrent.ExecutorService
|
||||||
|
|
||||||
|
class WebSocketIoSession(
|
||||||
|
private val socket: WebSocket,
|
||||||
|
service: ExecutorService
|
||||||
|
) : IoSession(null, service, socket.remoteSocketAddress?.toString() ?: "127.0.0.1") {
|
||||||
|
|
||||||
|
override fun queue(buffer: ByteBuffer?) {
|
||||||
|
if (buffer == null || !socket.isOpen) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val copy = buffer.slice()
|
||||||
|
val data = ByteArray(copy.remaining())
|
||||||
|
copy.get(data)
|
||||||
|
try {
|
||||||
|
socket.send(data)
|
||||||
|
} catch (e: WebsocketNotConnectedException) {
|
||||||
|
disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun write() {
|
||||||
|
// WebSocket sessions bypass the SelectionKey-backed TCP write queue.
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun disconnect() {
|
||||||
|
if (!isActive) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
super.disconnect()
|
||||||
|
if (socket.isOpen) {
|
||||||
|
socket.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package core.net.websocket
|
||||||
|
|
||||||
|
import core.ServerConstants
|
||||||
|
import core.api.log
|
||||||
|
import core.game.system.config.ServerConfigParser
|
||||||
|
import core.tools.Log
|
||||||
|
import org.java_websocket.server.DefaultSSLWebSocketServerFactory
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.security.KeyStore
|
||||||
|
import javax.net.ssl.KeyManagerFactory
|
||||||
|
import javax.net.ssl.SSLContext
|
||||||
|
|
||||||
|
object WebSocketTls {
|
||||||
|
|
||||||
|
private const val KEYSTORE_TYPE = "PKCS12"
|
||||||
|
|
||||||
|
fun configure(server: GameWebSocketServer) {
|
||||||
|
if (!ServerConstants.WEBSOCKET_TLS_ENABLED) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
server.setWebSocketFactory(DefaultSSLWebSocketServerFactory(createContext()))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createContext(): SSLContext {
|
||||||
|
val keystorePath = ServerConstants.WEBSOCKET_TLS_KEYSTORE_PATH.trim()
|
||||||
|
if (keystorePath.isEmpty()) {
|
||||||
|
throw IllegalStateException("server.websocket_tls_enabled is true but websocket_tls_keystore_path is blank")
|
||||||
|
}
|
||||||
|
val resolvedKeystorePath = ServerConfigParser.parsePath(keystorePath)
|
||||||
|
val keystorePassword = ServerConstants.WEBSOCKET_TLS_KEYSTORE_PASSWORD.toCharArray()
|
||||||
|
|
||||||
|
val keyStore = KeyStore.getInstance(KEYSTORE_TYPE)
|
||||||
|
FileInputStream(resolvedKeystorePath).use { input ->
|
||||||
|
keyStore.load(input, keystorePassword)
|
||||||
|
}
|
||||||
|
|
||||||
|
val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
|
||||||
|
keyManagerFactory.init(keyStore, keystorePassword)
|
||||||
|
|
||||||
|
val sslContext = SSLContext.getInstance("TLS")
|
||||||
|
sslContext.init(keyManagerFactory.keyManagers, null, null)
|
||||||
|
log(WebSocketTls::class.java, Log.INFO, "Configured secure websocket listener using keystore $resolvedKeystorePath.")
|
||||||
|
return sslContext
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,17 @@ daily_accounts_per_ip = 3
|
|||||||
watchdog_enabled = true
|
watchdog_enabled = true
|
||||||
connectivity_check_url = "https://google.com,https://2009scape.org"
|
connectivity_check_url = "https://google.com,https://2009scape.org"
|
||||||
connectivity_timeout = 500
|
connectivity_timeout = 500
|
||||||
|
# Enables a websocket listener for browser clients. The websocket stream carries the
|
||||||
|
# same raw binary protocol as TCP; each websocket binary frame is treated as a byte chunk.
|
||||||
|
websocket_enabled = false
|
||||||
|
# Leave at 0 to use the default websocket port of 53594 + world_id.
|
||||||
|
websocket_port = 0
|
||||||
|
# Enables TLS on the websocket listener so browser clients can connect over wss://.
|
||||||
|
websocket_tls_enabled = false
|
||||||
|
# PKCS12 keystore path, e.g. certs/dev-wss.p12.
|
||||||
|
websocket_tls_keystore_path = ""
|
||||||
|
# Optional. Leave blank for a PKCS12 file exported with an empty password.
|
||||||
|
websocket_tls_keystore_password = ""
|
||||||
|
|
||||||
[database]
|
[database]
|
||||||
database_name = "global"
|
database_name = "global"
|
||||||
|
|||||||
Reference in New Issue
Block a user