From 1fb38ecf37d9b79b782dda87dec65e277c532a61 Mon Sep 17 00:00:00 2001 From: Obique PSWG Date: Tue, 8 Oct 2019 19:26:59 -0400 Subject: [PATCH] Converted client-holocore from java to kotlin --- build.gradle | 6 +- .../holocore/client/HolocoreInputStream.kt | 37 ++ .../holocore/client/HolocoreOutputStream.kt | 23 ++ .../holocore/client/HolocoreProtocol.java | 69 ---- .../holocore/client/HolocoreProtocol.kt | 82 ++++ .../holocore/client/HolocoreSocket.java | 371 ------------------ .../holocore/client/HolocoreSocket.kt | 289 ++++++++++++++ .../client/{RawPacket.java => RawPacket.kt} | 44 ++- .../holocore/client/SWGProtocol.java | 40 -- .../projectswg/holocore/client/SWGProtocol.kt | 44 +++ ....java => ServerConnectionChangedReason.kt} | 4 +- .../client/ServerConnectionStatus.java | 8 - .../holocore/client/ServerConnectionStatus.kt | 8 + src/main/java/module-info.java | 6 +- 14 files changed, 518 insertions(+), 513 deletions(-) create mode 100644 src/main/java/com/projectswg/holocore/client/HolocoreInputStream.kt create mode 100644 src/main/java/com/projectswg/holocore/client/HolocoreOutputStream.kt delete mode 100644 src/main/java/com/projectswg/holocore/client/HolocoreProtocol.java create mode 100644 src/main/java/com/projectswg/holocore/client/HolocoreProtocol.kt delete mode 100644 src/main/java/com/projectswg/holocore/client/HolocoreSocket.java create mode 100644 src/main/java/com/projectswg/holocore/client/HolocoreSocket.kt rename src/main/java/com/projectswg/holocore/client/{RawPacket.java => RawPacket.kt} (54%) delete mode 100644 src/main/java/com/projectswg/holocore/client/SWGProtocol.java create mode 100644 src/main/java/com/projectswg/holocore/client/SWGProtocol.kt rename src/main/java/com/projectswg/holocore/client/{ServerConnectionChangedReason.java => ServerConnectionChangedReason.kt} (70%) delete mode 100644 src/main/java/com/projectswg/holocore/client/ServerConnectionStatus.java create mode 100644 src/main/java/com/projectswg/holocore/client/ServerConnectionStatus.kt diff --git a/build.gradle b/build.gradle index d0252d7..47c85bf 100644 --- a/build.gradle +++ b/build.gradle @@ -1,17 +1,21 @@ plugins { - id 'java' + id "java" id "org.javamodularity.moduleplugin" + id "org.jetbrains.kotlin.jvm" } sourceCompatibility = 12 repositories { + mavenLocal() jcenter() } dependencies { compile project(':pswgcommon') compile group: 'me.joshlarson', name: 'jlcommon-network', version: '1.0.0' + compile group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib', version: '1.3.50' + compile group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-core', version: '1.3.0' testCompile 'junit:junit:4.12' } diff --git a/src/main/java/com/projectswg/holocore/client/HolocoreInputStream.kt b/src/main/java/com/projectswg/holocore/client/HolocoreInputStream.kt new file mode 100644 index 0000000..097ee2d --- /dev/null +++ b/src/main/java/com/projectswg/holocore/client/HolocoreInputStream.kt @@ -0,0 +1,37 @@ +package com.projectswg.holocore.client + +import java.io.Closeable +import java.io.EOFException +import java.io.InputStream +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock + +class HolocoreInputStream(private val stream: InputStream): Closeable { + + private val lock = ReentrantLock(false) + private val protocol = SWGProtocol() + private val buffer = ByteArray(32*1024) + + fun read(): RawPacket { + lock.withLock { + if (protocol.hasPacket()) { + val packet = protocol.disassemble() + if (packet != null) + return packet + } + do { + val n = stream.read(buffer) + if (n <= 0) + throw EOFException("end of stream") + if (protocol.addToBuffer(buffer, 0, n)) + return protocol.disassemble() ?: continue + } while (true) + } + throw AssertionError("unreachable code") + } + + override fun close() { + stream.close() + } + +} diff --git a/src/main/java/com/projectswg/holocore/client/HolocoreOutputStream.kt b/src/main/java/com/projectswg/holocore/client/HolocoreOutputStream.kt new file mode 100644 index 0000000..162a7e5 --- /dev/null +++ b/src/main/java/com/projectswg/holocore/client/HolocoreOutputStream.kt @@ -0,0 +1,23 @@ +package com.projectswg.holocore.client + +import java.io.Closeable +import java.io.OutputStream +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock + +class HolocoreOutputStream(private val stream: OutputStream): Closeable { + + private val lock = ReentrantLock(false) + private val protocol = SWGProtocol() + + fun write(buffer: ByteArray) { + lock.withLock { + stream.write(protocol.assemble(buffer).array()) + } + } + + override fun close() { + stream.close() + } + +} diff --git a/src/main/java/com/projectswg/holocore/client/HolocoreProtocol.java b/src/main/java/com/projectswg/holocore/client/HolocoreProtocol.java deleted file mode 100644 index afad5be..0000000 --- a/src/main/java/com/projectswg/holocore/client/HolocoreProtocol.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.projectswg.holocore.client; - -import com.projectswg.common.network.NetBuffer; -import com.projectswg.common.network.NetBufferStream; - -import java.nio.ByteBuffer; - -class HolocoreProtocol { - - public static final String VERSION = "2018-02-04"; - - private static final byte [] EMPTY_PACKET = new byte[0]; - - private final NetBufferStream inboundStream; - - public HolocoreProtocol() { - this.inboundStream = new NetBufferStream(); - } - - public void reset() { - inboundStream.reset(); - } - - public NetBuffer assemble(byte [] raw) { - NetBuffer data = NetBuffer.allocate(raw.length + 4); // large array - data.addArrayLarge(raw); - data.flip(); - return data; - } - - public boolean addToBuffer(ByteBuffer data) { - synchronized (inboundStream) { - inboundStream.write(data); - return hasPacket(); - } - } - - public byte [] disassemble() { - synchronized (inboundStream) { - if (inboundStream.remaining() < 4) { - return EMPTY_PACKET; - } - inboundStream.mark(); - int messageLength = inboundStream.getInt(); - if (inboundStream.remaining() < messageLength) { - inboundStream.rewind(); - return EMPTY_PACKET; - } - byte [] data = inboundStream.getArray(messageLength); - inboundStream.compact(); - return data; - } - } - - public boolean hasPacket() { - synchronized (inboundStream) { - if (inboundStream.remaining() < 4) - return false; - inboundStream.mark(); - try { - int messageLength = inboundStream.getInt(); - return inboundStream.remaining() >= messageLength; - } finally { - inboundStream.rewind(); - } - } - } - -} diff --git a/src/main/java/com/projectswg/holocore/client/HolocoreProtocol.kt b/src/main/java/com/projectswg/holocore/client/HolocoreProtocol.kt new file mode 100644 index 0000000..ddeafca --- /dev/null +++ b/src/main/java/com/projectswg/holocore/client/HolocoreProtocol.kt @@ -0,0 +1,82 @@ +package com.projectswg.holocore.client + +import com.projectswg.common.network.NetBuffer +import com.projectswg.common.network.NetBufferStream + +import java.nio.ByteBuffer + +internal class HolocoreProtocol { + + private val inboundStream: NetBufferStream = NetBufferStream() + + fun reset() { + inboundStream.reset() + } + + fun assemble(raw: ByteArray): NetBuffer { + val data = NetBuffer.allocate(raw.size + 4) // large array + data.addArrayLarge(raw) + data.flip() + return data + } + + fun addToBuffer(data: ByteBuffer): Boolean { + synchronized(inboundStream) { + inboundStream.write(data) + return hasPacket() + } + } + + fun addToBuffer(data: ByteArray): Boolean { + synchronized(inboundStream) { + inboundStream.write(data) + return hasPacket() + } + } + + fun addToBuffer(data: ByteArray, offset: Int, length: Int): Boolean { + synchronized(inboundStream) { + inboundStream.write(data, offset, length) + return hasPacket() + } + } + + fun disassemble(): ByteArray { + synchronized(inboundStream) { + if (inboundStream.remaining() < 4) { + return EMPTY_PACKET + } + inboundStream.mark() + val messageLength = inboundStream.int + if (inboundStream.remaining() < messageLength) { + inboundStream.rewind() + return EMPTY_PACKET + } + val data = inboundStream.getArray(messageLength) + inboundStream.compact() + return data + } + } + + fun hasPacket(): Boolean { + synchronized(inboundStream) { + if (inboundStream.remaining() < 4) + return false + inboundStream.mark() + try { + val messageLength = inboundStream.int + return inboundStream.remaining() >= messageLength + } finally { + inboundStream.rewind() + } + } + } + + companion object { + + const val VERSION = "2018-02-04" + + private val EMPTY_PACKET = ByteArray(0) + } + +} diff --git a/src/main/java/com/projectswg/holocore/client/HolocoreSocket.java b/src/main/java/com/projectswg/holocore/client/HolocoreSocket.java deleted file mode 100644 index 3f6f68f..0000000 --- a/src/main/java/com/projectswg/holocore/client/HolocoreSocket.java +++ /dev/null @@ -1,371 +0,0 @@ -package com.projectswg.holocore.client; - -import com.projectswg.common.network.NetBuffer; -import com.projectswg.common.network.packets.swg.holo.HoloConnectionStarted; -import com.projectswg.common.network.packets.swg.holo.HoloConnectionStopped; -import com.projectswg.common.network.packets.swg.holo.HoloConnectionStopped.ConnectionStoppedReason; -import com.projectswg.common.network.packets.swg.holo.HoloSetProtocolVersion; -import me.joshlarson.jlcommon.concurrency.Delay; -import me.joshlarson.jlcommon.log.Log; -import me.joshlarson.jlcommon.network.SSLEngineWrapper.SSLClosedException; -import me.joshlarson.jlcommon.network.SecureTCPSocket; -import me.joshlarson.jlcommon.network.TCPSocket; -import me.joshlarson.jlcommon.network.TCPSocket.TCPSocketCallback; -import me.joshlarson.jlcommon.network.UDPServer; - -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocket; -import javax.net.ssl.TrustManager; -import javax.net.ssl.X509TrustManager; -import java.io.IOException; -import java.net.*; -import java.nio.ByteBuffer; -import java.nio.channels.ClosedChannelException; -import java.security.KeyManagementException; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.security.cert.X509Certificate; -import java.util.Locale; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; - -public class HolocoreSocket implements AutoCloseable { - - private final SWGProtocol swgProtocol; - private final AtomicReference status; - private final UDPServer udpServer; - private final BlockingQueue udpInboundQueue; - private final BlockingQueue inboundQueue; - private final boolean verifyServer; - - private SecureTCPSocket socket; - private StatusChangedCallback callback; - private InetSocketAddress address; - - public HolocoreSocket(InetAddress addr, int port) { - this(addr, port, true); - } - - public HolocoreSocket(InetAddress addr, int port, boolean verifyServer) { - this.swgProtocol = new SWGProtocol(); - this.status = new AtomicReference<>(ServerConnectionStatus.DISCONNECTED); - this.udpInboundQueue = new LinkedBlockingQueue<>(); - this.inboundQueue = new LinkedBlockingQueue<>(); - this.verifyServer = verifyServer; - this.udpServer = createUDPServer(); - this.socket = null; - this.callback = null; - this.address = new InetSocketAddress(addr, port); - } - - /** - * Shuts down any miscellaneous resources--such as the query UDP server - */ - public void close() { - if (udpServer != null) - udpServer.close(); - udpInboundQueue.clear(); - - disconnect(ConnectionStoppedReason.APPLICATION); - - TCPSocket socket = this.socket; - if (socket != null) - socket.disconnect(); - } - - /** - * Shuts down any miscellaneous resources--such as the query UDP server - */ - @Deprecated - public void terminate() { - close(); - } - - /** - * Sets a callback for when the status of the server socket changes - * @param callback the callback - */ - public void setStatusChangedCallback(StatusChangedCallback callback) { - this.callback = callback; - } - - /** - * Sets the remote address this socket will attempt to connect to - * @param addr the destination address - * @param port the destination port - */ - public void setRemoteAddress(InetAddress addr, int port) { - this.address = new InetSocketAddress(addr, port); - } - - /** - * Returns the remote address this socket is pointing to - * @return the remote address as an InetSocketAddress - */ - public InetSocketAddress getRemoteAddress() { - return address; - } - - /** - * Gets the current connection state of the socket - * @return the connection state - */ - public ServerConnectionStatus getConnectionState() { - return status.get(); - } - - /** - * Returns whether or not this socket is disconnected - * @return TRUE if disconnected, FALSE otherwise - */ - public boolean isDisconnected() { - return status.get() == ServerConnectionStatus.DISCONNECTED; - } - - /** - * Returns whether or not this socket is connecting - * @return TRUE if connecting, FALSE otherwise - */ - public boolean isConnecting() { - return status.get() == ServerConnectionStatus.CONNECTING; - } - - /** - * Returns whether or not this socket is connected - * @return TRUE if connected, FALSE otherwise - */ - public boolean isConnected() { - return status.get() == ServerConnectionStatus.CONNECTED; - } - - /** - * Retrieves the server status via a UDP query, with the default timeout of 2000ms - * @return the server status as a string - */ - public String getServerStatus() { - return getServerStatus(2000); - } - - /** - * Retrives the server status via a UDP query, with the specified timeout - * @param timeout the timeout in milliseconds - * @return the server status as a string - */ - public String getServerStatus(long timeout) { - Log.t("Requesting server status from %s", address); - if (!udpServer.isRunning()) { - try { - udpServer.bind(); - } catch (SocketException e) { - return "UNKNOWN"; - } - } - udpServer.send(address, new byte[]{1}); - try { - DatagramPacket packet = udpInboundQueue.poll(timeout, TimeUnit.MILLISECONDS); - if (packet == null) - return "OFFLINE"; - NetBuffer data = NetBuffer.wrap(packet.getData()); - data.getByte(); - return data.getAscii(); - } catch (InterruptedException e) { - Log.w("Interrupted while waiting for server status response"); - return "UNKNOWN"; - } - } - - /** - * Attempts to connect to the remote server. This call is a blocking function that will not - * return until it has either successfully connected or has failed. It starts by initializing a - * TCP connection, then initializes the Holocore connection, then returns. - * @param timeout the timeout for the connect call - * @return TRUE if successful and connected, FALSE on error - */ - public boolean connect(int timeout) { - try { - SSLContext sslContext = SSLContext.getInstance("TLSv1.3"); - TrustManager [] tm = verifyServer ? null : new TrustManager[]{new TrustingTrustManager()}; - sslContext.init(null, tm, new SecureRandom()); - socket = new SecureTCPSocket(address, sslContext, Runnable::run); - } catch (NoSuchAlgorithmException | KeyManagementException e) { - throw new RuntimeException(e); - } - inboundQueue.clear(); - return finishConnection(socket, timeout); - } - - private boolean finishConnection(SecureTCPSocket socket, int timeout) { - updateStatus(ServerConnectionStatus.CONNECTING, ServerConnectionChangedReason.NONE); - try { - socket.createConnection(); - - socket.setCallback(new TCPSocketCallback() { - @Override - public void onIncomingData(TCPSocket socket, ByteBuffer data) { - swgProtocol.addToBuffer(data); - while (true) { - RawPacket packet = swgProtocol.disassemble(); - if (packet != null) { - handlePacket(packet.getCrc(), packet.getData()); - inboundQueue.offer(packet); - } else - break; - } - } - @Override - public void onError(TCPSocket socket, Throwable t) { - if (t instanceof ClosedChannelException || t instanceof SSLClosedException) - return; - Log.e(t); - } - @Override - public void onDisconnected(TCPSocket socket) { updateStatus(ServerConnectionStatus.DISCONNECTED, ServerConnectionChangedReason.OTHER_SIDE_TERMINATED); } - @Override - public void onConnected(TCPSocket socket) { updateStatus(ServerConnectionStatus.CONNECTED, ServerConnectionChangedReason.NONE); } - }); - this.socket = socket; - waitForConnect(timeout); - return true; - } catch (IOException e) { - updateStatus(ServerConnectionStatus.DISCONNECTED, getReason(e.getMessage())); - socket.disconnect(); - } - return false; - } - - /** - * Attempts to disconnect from the server with the specified reason. Before this socket is - * closed, it will send a HoloConnectionStopped packet to notify the remote server. - * @param reason the reason for disconnecting - * @return TRUE if successfully disconnected, FALSE on error - */ - public boolean disconnect(ConnectionStoppedReason reason) { - ServerConnectionStatus status = this.status.get(); - TCPSocket socket = this.socket; - if (socket == null) - return true; - switch (status) { - case CONNECTING: - case DISCONNECTING: - case DISCONNECTED: - default: - return socket.disconnect(); - case CONNECTED: - updateStatus(ServerConnectionStatus.DISCONNECTING, ServerConnectionChangedReason.CLIENT_DISCONNECT); - send(new HoloConnectionStopped(reason).encode().array()); - return true; - } - } - - /** - * Attempts to send a byte array to the remote server. This method blocks until it has - * completely sent or has failed. - * @param raw the byte array to send - * @return TRUE on success, FALSE on failure - */ - public boolean send(byte [] raw) { - TCPSocket socket = this.socket; - if (socket != null) - return socket.send(swgProtocol.assemble(raw).getBuffer()) > 0; - return false; - } - - /** - * Attempts to receive a packet from the remote server. This method blocks until a packet is - * recieved or has failed. - * @return the RawPacket containing the CRC of the SWG message and the raw data array, or NULL - * on error - */ - public RawPacket receive() { - try { - return inboundQueue.take(); - } catch (InterruptedException e) { - return null; - } - } - - /** - * Returns whether or not there is a packet ready to be received without blocking - * @return TRUE if there is a packet, FALSE otherwise - */ - public boolean hasPacket() { - return !inboundQueue.isEmpty(); - } - - private void waitForConnect(int timeout) throws IOException { - Socket rawSocket = socket.getSocket(); - rawSocket.setSoTimeout(timeout); - try { - socket.startConnection(); - send(new HoloSetProtocolVersion(HolocoreProtocol.VERSION).encode().array()); - while (isConnecting() && !Delay.isInterrupted()) { - Delay.sleepMilli(50); - } - } finally { - rawSocket.setSoTimeout(0); // Reset back to how it was before the function - } - } - - private void handlePacket(int crc, byte [] raw) { - if (crc == HoloConnectionStarted.CRC) { - updateStatus(ServerConnectionStatus.CONNECTED, ServerConnectionChangedReason.NONE); - } else if (crc == HoloConnectionStopped.CRC) { - HoloConnectionStopped packet = new HoloConnectionStopped(); - packet.decode(NetBuffer.wrap(raw)); - updateStatus(ServerConnectionStatus.DISCONNECTING, ServerConnectionChangedReason.OTHER_SIDE_TERMINATED); - disconnect(packet.getReason()); - } - } - - private void updateStatus(ServerConnectionStatus status, ServerConnectionChangedReason reason) { - ServerConnectionStatus old = this.status.getAndSet(status); - if (old != status && callback != null) - callback.onConnectionStatusChanged(old, status, reason); - } - - private ServerConnectionChangedReason getReason(String message) { - message = message.toLowerCase(Locale.US); - if (message.contains("broken pipe")) - return ServerConnectionChangedReason.BROKEN_PIPE; - if (message.contains("connection reset")) - return ServerConnectionChangedReason.CONNECTION_RESET; - if (message.contains("connection refused")) - return ServerConnectionChangedReason.CONNECTION_REFUSED; - if (message.contains("address in use")) - return ServerConnectionChangedReason.ADDR_IN_USE; - if (message.contains("socket closed")) - return ServerConnectionChangedReason.SOCKET_CLOSED; - if (message.contains("no route to host")) - return ServerConnectionChangedReason.NO_ROUTE_TO_HOST; - return ServerConnectionChangedReason.UNKNOWN; - } - - public interface StatusChangedCallback { - void onConnectionStatusChanged(ServerConnectionStatus oldStatus, ServerConnectionStatus newStatus, ServerConnectionChangedReason reason); - } - - private UDPServer createUDPServer() { - try { - UDPServer server = new UDPServer(new InetSocketAddress(0), 1500, udpInboundQueue::add); - server.bind(); - return server; - } catch (SocketException e) { - Log.e(e); - } - return null; - } - - private static class TrustingTrustManager implements X509TrustManager { - - @Override - public void checkClientTrusted(X509Certificate[] chain, String authType) { } - - @Override - public void checkServerTrusted(X509Certificate[] chain, String authType) { } - - @Override - public X509Certificate[] getAcceptedIssuers() { return null; } - } -} diff --git a/src/main/java/com/projectswg/holocore/client/HolocoreSocket.kt b/src/main/java/com/projectswg/holocore/client/HolocoreSocket.kt new file mode 100644 index 0000000..5402d02 --- /dev/null +++ b/src/main/java/com/projectswg/holocore/client/HolocoreSocket.kt @@ -0,0 +1,289 @@ +package com.projectswg.holocore.client + +import com.projectswg.common.network.NetBuffer +import com.projectswg.common.network.packets.swg.holo.HoloConnectionStarted +import com.projectswg.common.network.packets.swg.holo.HoloConnectionStopped +import com.projectswg.common.network.packets.swg.holo.HoloConnectionStopped.ConnectionStoppedReason +import com.projectswg.common.network.packets.swg.holo.HoloSetProtocolVersion +import me.joshlarson.jlcommon.concurrency.Delay +import me.joshlarson.jlcommon.log.Log +import java.io.Closeable +import java.io.EOFException +import java.io.IOException +import java.net.* +import java.security.SecureRandom +import java.security.cert.X509Certificate +import java.util.* +import java.util.concurrent.atomic.AtomicReference +import javax.net.ssl.SSLContext +import javax.net.ssl.TrustManager +import javax.net.ssl.X509TrustManager + +class HolocoreSocket @JvmOverloads constructor(addr: InetAddress, port: Int, private val verifyServer: Boolean = true, private val encryptionEnabled: Boolean = true) : Closeable { + + private val status: AtomicReference = AtomicReference(ServerConnectionStatus.DISCONNECTED) + private val udpServer = DatagramSocket() + private val socket = createSocket() + + private var callback: StatusChangedCallback? = null + private var socketInputStream: HolocoreInputStream? = null + private var socketOutputStream: HolocoreOutputStream? = null + + /** + * Returns the remote address this socket is pointing to + * @return the remote address as an InetSocketAddress + */ + var remoteAddress: InetSocketAddress? = null + private set + + /** + * Gets the current connection state of the socket + * @return the connection state + */ + val connectionState: ServerConnectionStatus + get() = status.get() + + /** + * Returns whether or not this socket is disconnected + * @return TRUE if disconnected, FALSE otherwise + */ + val isDisconnected: Boolean + get() = status.get() == ServerConnectionStatus.DISCONNECTED + + /** + * Returns whether or not this socket is connecting + * @return TRUE if connecting, FALSE otherwise + */ + val isConnecting: Boolean + get() = status.get() == ServerConnectionStatus.CONNECTING + + /** + * Returns whether or not this socket is connected + * @return TRUE if connected, FALSE otherwise + */ + val isConnected: Boolean + get() = status.get() == ServerConnectionStatus.CONNECTED + + /** + * Retrieves the server status via a UDP query, with the default timeout of 2000ms + * @return the server status as a string + */ + val serverStatus: String + get() = getServerStatus(2000) + + init { + this.callback = null + this.remoteAddress = InetSocketAddress(addr, port) + } + + /** + * Shuts down any miscellaneous resources--such as the query UDP server + */ + override fun close() { + udpServer.close() + + disconnect(ConnectionStoppedReason.APPLICATION) + + Log.t("Disconnecting from Holocore") + val socket = this.socket + socketOutputStream?.close() + socketInputStream?.close() + socket.close() + + socketOutputStream = null + socketInputStream = null + } + + /** + * Shuts down any miscellaneous resources--such as the query UDP server + */ + @Deprecated("should be replaced with close()", replaceWith=ReplaceWith("close()")) + fun terminate() { + close() + } + + /** + * Sets a callback for when the status of the server socket changes + * @param callback the callback + */ + fun setStatusChangedCallback(callback: StatusChangedCallback) { + this.callback = callback + } + + /** + * Sets the remote address this socket will attempt to connect to + * @param addr the destination address + * @param port the destination port + */ + fun setRemoteAddress(addr: InetAddress, port: Int) { + this.remoteAddress = InetSocketAddress(addr, port) + } + + /** + * Retrives the server status via a UDP query, with the specified timeout + * @param timeout the timeout in milliseconds + * @return the server status as a string + */ + fun getServerStatus(timeout: Long): String { + Log.t("Requesting server status from %s", remoteAddress!!) + udpServer.send(DatagramPacket(byteArrayOf(1), 1, remoteAddress!!)) + val packet = DatagramPacket(ByteArray(512), 512) + + return try { + udpServer.soTimeout = timeout.toInt() + udpServer.receive(packet) + udpServer.soTimeout = 0 + val data = NetBuffer.wrap(packet.data) + data.byte + data.ascii // status string + } catch (e: Throwable) { + "OFFLINE" // status string = OFFLINE + } + } + + /** + * Attempts to connect to the remote server. This call is a blocking function that will not + * return until it has either successfully connected or has failed with an exception. It + * starts by initializing a TCP connection, then initializes the Holocore connection, then + * returns. + * @param timeout the timeout for the connect call + */ + fun connect(timeout: Int) { + try { + Log.t("Connecting to Holocore: encryption=%b", encryptionEnabled) + socket.connect(remoteAddress ?: throw IllegalStateException("no remote endpoint defined")) + socketInputStream = HolocoreInputStream(socket.getInputStream()) + socketOutputStream = HolocoreOutputStream(socket.getOutputStream()) + + updateStatus(ServerConnectionStatus.CONNECTING, ServerConnectionChangedReason.NONE) + waitForConnect(timeout) + } catch (e: Throwable) { + updateStatus(ServerConnectionStatus.DISCONNECTED, getReason(e.message)) + socket.close() + throw e + } + } + + /** + * Attempts to disconnect from the server with the specified reason. Before this socket is + * closed, it will send a HoloConnectionStopped packet to notify the remote server. + * @param reason the reason for disconnecting + * @return TRUE if successfully disconnected, FALSE on error + */ + fun disconnect(reason: ConnectionStoppedReason): Boolean { + when (status.get()) { + ServerConnectionStatus.CONNECTING, ServerConnectionStatus.DISCONNECTING, ServerConnectionStatus.DISCONNECTED -> socket.close() + ServerConnectionStatus.CONNECTED -> { + updateStatus(ServerConnectionStatus.DISCONNECTING, ServerConnectionChangedReason.CLIENT_DISCONNECT) + send(HoloConnectionStopped(reason).encode().array()) + } + else -> socket.close() + } + return true + } + + /** + * Attempts to send a byte array to the remote server. This method blocks until it has + * completely sent or has failed. + * @param raw the byte array to send + * @return TRUE on success, FALSE on failure + */ + fun send(raw: ByteArray): Boolean { + try { + socketOutputStream?.write(raw) + return true + } catch (e: IOException) { + return false + } + } + + /** + * Attempts to receive a packet from the remote server. This method blocks until a packet is + * recieved or has failed. + * @return the RawPacket containing the CRC of the SWG message and the raw data array, or NULL + * on error + */ + fun receive(): RawPacket? { + try { + val packet = socketInputStream?.read() ?: return null + handlePacket(packet.crc, packet.data) + return packet + } catch (e: InterruptedException) { + return null + } catch (e: EOFException) { + return null + } + } + + private fun waitForConnect(timeout: Int) { + socket.soTimeout = timeout + try { + send(HoloSetProtocolVersion(HolocoreProtocol.VERSION).encode().array()) + while (isConnecting && !Delay.isInterrupted()) { + receive() ?: throw IOException("socket closed") + } + } finally { + socket.soTimeout = 0 // Reset back to how it was before the function + } + } + + private fun handlePacket(crc: Int, raw: ByteArray) { + when (crc) { + HoloConnectionStarted.CRC -> updateStatus(ServerConnectionStatus.CONNECTED, ServerConnectionChangedReason.NONE) + HoloConnectionStopped.CRC -> { + val packet = HoloConnectionStopped() + packet.decode(NetBuffer.wrap(raw)) + updateStatus(ServerConnectionStatus.DISCONNECTING, ServerConnectionChangedReason.OTHER_SIDE_TERMINATED) + disconnect(packet.reason) + } + } + } + + private fun createSocket(): Socket { + if (encryptionEnabled) { + val sslContext = SSLContext.getInstance("TLSv1.3") + val tm = if (verifyServer) null else arrayOf(TrustingTrustManager()) + sslContext.init(null, tm, SecureRandom()) + return sslContext.socketFactory.createSocket() + } else { + return Socket() + } + } + + private fun updateStatus(status: ServerConnectionStatus, reason: ServerConnectionChangedReason) { + val old = this.status.getAndSet(status) + if (old != status && callback != null) + callback!!.onConnectionStatusChanged(old, status, reason) + } + + private fun getReason(message: String?): ServerConnectionChangedReason { + var messageLower = message ?: return ServerConnectionChangedReason.UNKNOWN + messageLower = messageLower.toLowerCase(Locale.US) + if (messageLower.contains("broken pipe")) + return ServerConnectionChangedReason.BROKEN_PIPE + if (messageLower.contains("connection reset")) + return ServerConnectionChangedReason.CONNECTION_RESET + if (messageLower.contains("connection refused")) + return ServerConnectionChangedReason.CONNECTION_REFUSED + if (messageLower.contains("address in use")) + return ServerConnectionChangedReason.ADDR_IN_USE + if (messageLower.contains("socket closed")) + return ServerConnectionChangedReason.SOCKET_CLOSED + return if (messageLower.contains("no route to host")) ServerConnectionChangedReason.NO_ROUTE_TO_HOST else ServerConnectionChangedReason.UNKNOWN + } + + interface StatusChangedCallback { + fun onConnectionStatusChanged(oldStatus: ServerConnectionStatus, newStatus: ServerConnectionStatus, reason: ServerConnectionChangedReason) + } + + private class TrustingTrustManager : X509TrustManager { + + override fun checkClientTrusted(chain: Array, authType: String) {} + + override fun checkServerTrusted(chain: Array, authType: String) {} + + override fun getAcceptedIssuers(): Array? { + return null + } + } +} diff --git a/src/main/java/com/projectswg/holocore/client/RawPacket.java b/src/main/java/com/projectswg/holocore/client/RawPacket.kt similarity index 54% rename from src/main/java/com/projectswg/holocore/client/RawPacket.java rename to src/main/java/com/projectswg/holocore/client/RawPacket.kt index 9c864aa..427ba8a 100644 --- a/src/main/java/com/projectswg/holocore/client/RawPacket.java +++ b/src/main/java/com/projectswg/holocore/client/RawPacket.kt @@ -1,41 +1,43 @@ /*********************************************************************************** * Copyright (C) 2018 /// Project SWG /// www.projectswg.com * - * * + * * * This file is part of the ProjectSWG Launcher. * - * * + * * * This program 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. * - * * + * * * This program 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 this program. If not, see . * - * * - ***********************************************************************************/ + * along with this program. If not, see //www.gnu.org/licenses/>. * + * * + */ -package com.projectswg.holocore.client; +package com.projectswg.holocore.client -public class RawPacket { +data class RawPacket(val crc: Int, val data: ByteArray) { - private final int crc; - private final byte[] data; - - public RawPacket(int crc, byte[] data) { - this.crc = crc; - this.data = data; + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as RawPacket + + if (crc != other.crc) return false + if (!data.contentEquals(other.data)) return false + + return true } - public int getCrc() { - return crc; - } - - public byte[] getData() { - return data; + override fun hashCode(): Int { + var result = crc + result = 31 * result + data.contentHashCode() + return result } } diff --git a/src/main/java/com/projectswg/holocore/client/SWGProtocol.java b/src/main/java/com/projectswg/holocore/client/SWGProtocol.java deleted file mode 100644 index fc5d4eb..0000000 --- a/src/main/java/com/projectswg/holocore/client/SWGProtocol.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.projectswg.holocore.client; - -import com.projectswg.common.network.NetBuffer; - -import java.nio.ByteBuffer; - -class SWGProtocol { - - private final HolocoreProtocol holocore; - - public SWGProtocol() { - holocore = new HolocoreProtocol(); - } - - public void reset() { - holocore.reset(); - } - - public NetBuffer assemble(byte [] packet) { - return holocore.assemble(packet); - } - - public boolean addToBuffer(ByteBuffer data) { - return holocore.addToBuffer(data); - } - - public RawPacket disassemble() { - byte [] packet = holocore.disassemble(); - if (packet.length < 6) - return null; - NetBuffer data = NetBuffer.wrap(packet); - data.getShort(); - return new RawPacket(data.getInt(), packet); - } - - public boolean hasPacket() { - return holocore.hasPacket(); - } - -} diff --git a/src/main/java/com/projectswg/holocore/client/SWGProtocol.kt b/src/main/java/com/projectswg/holocore/client/SWGProtocol.kt new file mode 100644 index 0000000..67f057f --- /dev/null +++ b/src/main/java/com/projectswg/holocore/client/SWGProtocol.kt @@ -0,0 +1,44 @@ +package com.projectswg.holocore.client + +import com.projectswg.common.network.NetBuffer + +import java.nio.ByteBuffer + +internal class SWGProtocol { + + private val holocore: HolocoreProtocol = HolocoreProtocol() + + fun reset() { + holocore.reset() + } + + fun assemble(packet: ByteArray): NetBuffer { + return holocore.assemble(packet) + } + + fun addToBuffer(data: ByteArray): Boolean { + return holocore.addToBuffer(data) + } + + fun addToBuffer(data: ByteArray, offset: Int, length: Int): Boolean { + return holocore.addToBuffer(data, offset, length) + } + + fun addToBuffer(data: ByteBuffer): Boolean { + return holocore.addToBuffer(data) + } + + fun disassemble(): RawPacket? { + val packet = holocore.disassemble() + if (packet.size < 6) + return null + val data = NetBuffer.wrap(packet) + data.position(2) + return RawPacket(data.int, packet) + } + + fun hasPacket(): Boolean { + return holocore.hasPacket() + } + +} diff --git a/src/main/java/com/projectswg/holocore/client/ServerConnectionChangedReason.java b/src/main/java/com/projectswg/holocore/client/ServerConnectionChangedReason.kt similarity index 70% rename from src/main/java/com/projectswg/holocore/client/ServerConnectionChangedReason.java rename to src/main/java/com/projectswg/holocore/client/ServerConnectionChangedReason.kt index ebdea85..ad18da0 100644 --- a/src/main/java/com/projectswg/holocore/client/ServerConnectionChangedReason.java +++ b/src/main/java/com/projectswg/holocore/client/ServerConnectionChangedReason.kt @@ -1,6 +1,6 @@ -package com.projectswg.holocore.client; +package com.projectswg.holocore.client -public enum ServerConnectionChangedReason { +enum class ServerConnectionChangedReason { NONE, CLIENT_DISCONNECT, SOCKET_CLOSED, diff --git a/src/main/java/com/projectswg/holocore/client/ServerConnectionStatus.java b/src/main/java/com/projectswg/holocore/client/ServerConnectionStatus.java deleted file mode 100644 index 72ac13c..0000000 --- a/src/main/java/com/projectswg/holocore/client/ServerConnectionStatus.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.projectswg.holocore.client; - -public enum ServerConnectionStatus { - CONNECTING, - CONNECTED, - DISCONNECTING, - DISCONNECTED -} diff --git a/src/main/java/com/projectswg/holocore/client/ServerConnectionStatus.kt b/src/main/java/com/projectswg/holocore/client/ServerConnectionStatus.kt new file mode 100644 index 0000000..f18c405 --- /dev/null +++ b/src/main/java/com/projectswg/holocore/client/ServerConnectionStatus.kt @@ -0,0 +1,8 @@ +package com.projectswg.holocore.client + +enum class ServerConnectionStatus { + CONNECTING, + CONNECTED, + DISCONNECTING, + DISCONNECTED +} diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index e0c626e..fa920c2 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,6 +1,10 @@ module com.projectswg.holocore.client { - requires com.projectswg.common; + requires org.jetbrains.annotations; + requires me.joshlarson.jlcommon; requires me.joshlarson.jlcommon.network; + requires com.projectswg.common; + requires kotlin.stdlib; + requires kotlinx.coroutines.core; exports com.projectswg.holocore.client; }