mirror of
https://bitbucket.org/projectswg/client-holocore.git
synced 2026-01-16 23:04:29 -05:00
Converted client-holocore from java to kotlin
This commit is contained in:
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<ServerConnectionStatus> status;
|
||||
private final UDPServer udpServer;
|
||||
private final BlockingQueue<DatagramPacket> udpInboundQueue;
|
||||
private final BlockingQueue<RawPacket> 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; }
|
||||
}
|
||||
}
|
||||
289
src/main/java/com/projectswg/holocore/client/HolocoreSocket.kt
Normal file
289
src/main/java/com/projectswg/holocore/client/HolocoreSocket.kt
Normal file
@@ -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<ServerConnectionStatus> = 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<TrustManager>(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<X509Certificate>, authType: String) {}
|
||||
|
||||
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {}
|
||||
|
||||
override fun getAcceptedIssuers(): Array<X509Certificate>? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
***********************************************************************************/
|
||||
* along with this program. If not, see <https:></https:>//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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
44
src/main/java/com/projectswg/holocore/client/SWGProtocol.kt
Normal file
44
src/main/java/com/projectswg/holocore/client/SWGProtocol.kt
Normal file
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,
|
||||
@@ -1,8 +0,0 @@
|
||||
package com.projectswg.holocore.client;
|
||||
|
||||
public enum ServerConnectionStatus {
|
||||
CONNECTING,
|
||||
CONNECTED,
|
||||
DISCONNECTING,
|
||||
DISCONNECTED
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.projectswg.holocore.client
|
||||
|
||||
enum class ServerConnectionStatus {
|
||||
CONNECTING,
|
||||
CONNECTED,
|
||||
DISCONNECTING,
|
||||
DISCONNECTED
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user