Revamped Forwarder

This commit is contained in:
Josh Larson
2018-05-17 10:23:22 -05:00
parent 70695be0fe
commit d064989ef7
78 changed files with 1899 additions and 2980 deletions

View File

@@ -1,30 +1,19 @@
apply plugin: 'application'
apply plugin: 'com.github.johnrengelman.shadow'
apply plugin: 'java'
mainClassName = 'com.projectswg.Forwarder'
manifest {
attributes 'Main-Class': 'com.projectswg.Forwarder'
plugins {
id 'java'
id 'idea'
}
sourceSets {
main {
java {
srcDirs = ['src']
includes ['**/*.java']
}
}
sourceCompatibility = 1.8
targetCompatibility = 1.8
jar {
baseName = "Forwarder"
classifier = null
version = null
}
shadowJar {
archiveName = "Forwarder.jar"
}
dependencies {
compile project(':PSWGCommon')
compile project(':ClientHolocore')
compileOnly fileTree(dir: 'lib', include: ['*.jar'])
task wrapper(type: Wrapper) {
gradleVersion = "4.6"
}
repositories {
@@ -32,14 +21,7 @@ repositories {
}
dependencies {
}
buildscript {
repositories {
maven { url 'https://plugins.gradle.org/m2/' }
}
dependencies {
classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.0'
}
compile project(':PSWGCommon')
compile project(':ClientHolocore')
compileOnly fileTree(dir: 'lib', include: ['*.jar'])
}

View File

@@ -1,180 +0,0 @@
package com.projectswg;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.concurrent.atomic.AtomicLong;
import com.projectswg.common.concurrency.Delay;
import com.projectswg.common.control.IntentManager;
import com.projectswg.common.control.Manager;
import com.projectswg.common.debug.Log;
import com.projectswg.common.network.packets.swg.ErrorMessage;
import com.projectswg.connection.ServerConnectionChangedReason;
import com.projectswg.connection.ServerConnectionStatus;
import com.projectswg.intents.ClientConnectionChangedIntent;
import com.projectswg.intents.ClientToServerPacketIntent;
import com.projectswg.intents.ServerConnectionChangedIntent;
import com.projectswg.intents.ServerToClientPacketIntent;
import com.projectswg.networking.NetInterceptor.InterceptorProperties;
import com.projectswg.networking.client.ClientConnectionService;
import com.projectswg.networking.server.ServerConnectionService;
import com.projectswg.networking.soe.Disconnect.DisconnectReason;
import com.projectswg.services.PacketRecordingService;
public class Connections extends Manager {
public static final String VERSION = "0.9.8";
private final ServerConnectionService server;
private final ClientConnectionService client;
private final PacketRecordingService recording;
private final AtomicLong serverToClient;
private final AtomicLong clientToServer;
private ConnectionCallback callback;
private InetAddress addr;
private int port;
public Connections() {
this(InetAddress.getLoopbackAddress(), 44463, 44453, true);
}
public Connections(InetAddress remoteAddr, int remotePort, int loginPort, boolean timeout) {
setIntentManager(new IntentManager(Runtime.getRuntime().availableProcessors()));
this.server = new ServerConnectionService(remoteAddr, remotePort);
this.client = new ClientConnectionService(loginPort, timeout);
this.recording = new PacketRecordingService();
this.serverToClient = new AtomicLong(0);
this.clientToServer = new AtomicLong(0);
this.callback = null;
this.addr = null;
this.port = 0;
setRemote(remoteAddr, remotePort);
addChildService(server);
addChildService(client);
addChildService(recording);
}
@Override
public boolean initialize() {
getIntentManager().initialize();
registerForIntent(ClientConnectionChangedIntent.class, ccci -> processClientStatusChanged(ccci));
registerForIntent(ServerConnectionChangedIntent.class, scci -> processServerStatusChanged(scci));
registerForIntent(ClientToServerPacketIntent.class, ctspi -> onDataClientToServer(ctspi.getData()));
registerForIntent(ServerToClientPacketIntent.class, stcpi -> onDataServerToClient(stcpi.getRawData()));
return super.initialize();
}
@Override
public boolean terminate() {
getIntentManager().terminate();
return super.terminate();
}
public void setCallback(ConnectionCallback callback) {
this.callback = callback;
}
public boolean setRemote(InetAddress addr, int port) {
this.addr = addr;
this.port = port;
recording.setAddress(new InetSocketAddress("::1", 0), server.getRemoteAddress());
server.setRemoteAddress(addr, port);
return true;
}
public InetAddress getRemoteAddress() {
return addr;
}
public int getRemotePort() {
return port;
}
public int getLoginPort() {
return client.getLoginPort();
}
public int getZonePort() {
return client.getZonePort();
}
public long getServerToClientCount() {
return serverToClient.get();
}
public long getClientToServerCount() {
return clientToServer.get();
}
public InterceptorProperties getInterceptorProperties() {
return client.getInterceptorProperties();
}
private void processServerStatusChanged(ServerConnectionChangedIntent scci) {
if (callback != null)
callback.onServerStatusChanged(scci.getOldStatus(), scci.getStatus());
if (scci.getStatus() == ServerConnectionStatus.DISCONNECTED && scci.getReason() != ServerConnectionChangedReason.CLIENT_DISCONNECT) {
Log.i("Shutting down client due to server status: %s and reason %s", scci.getStatus(), scci.getReason());
String title = "";
String text = "";
if (scci.getReason() != ServerConnectionChangedReason.INVALID_PROTOCOL) {
title = "Connection Lost";
text = "\n" + scci.getReason().name().replace('_', ' ');
} else {
title = "Network";
text = "\nInvalid protocol version!";
text += "\nTry updating your launcher to the latest version.";
text += "\nInstalled Version: " + VERSION;
}
client.sendPackaged(new ErrorMessage(title, text, false));
client.waitForClientAcknowledge();
Delay.sleepMilli(100);
client.disconnect(DisconnectReason.OTHER_SIDE_TERMINATED);
}
}
private void processClientStatusChanged(ClientConnectionChangedIntent ccci) {
switch (ccci.getStatus()) {
case LOGIN_CONNECTED:
onClientConnected();
break;
case DISCONNECTED:
onClientDisconnected();
break;
default:
break;
}
}
private void onClientConnected() {
if (callback != null)
callback.onClientConnected();
}
private void onClientDisconnected() {
if (callback != null)
callback.onClientDisconnected();
}
private void onDataServerToClient(byte [] data) {
serverToClient.addAndGet(data.length);
if (callback != null)
callback.onDataServerToClient(data);
}
private void onDataClientToServer(byte [] data) {
clientToServer.addAndGet(data.length);
if (callback != null)
callback.onDataClientToServer(data);
}
public interface ConnectionCallback {
void onServerStatusChanged(ServerConnectionStatus oldStatus, ServerConnectionStatus status);
void onClientConnected();
void onClientDisconnected();
void onDataServerToClient(byte [] data);
void onDataClientToServer(byte [] data);
}
}

View File

@@ -1,196 +0,0 @@
package com.projectswg;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import com.projectswg.Connections.ConnectionCallback;
import com.projectswg.common.debug.Log;
import com.projectswg.common.debug.Log.LogLevel;
import com.projectswg.common.debug.log_wrapper.ConsoleLogWrapper;
import com.projectswg.connection.ServerConnectionStatus;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.paint.Color;
import javafx.scene.text.Text;
import javafx.stage.Stage;
public class Forwarder extends Application implements ConnectionCallback {
private static final String [] DATA_NAMES = new String[]{"B", "KB", "MB", "GB", "TB"};
private final HolocoreConnection connections;
private final ExecutorService executor;
private final TextField usernameField;
private final TextField passwordField;
private final TextField serverIpField;
private final TextField serverPortField;
private final Text serverConnectionText;
private final Text serverStatusText;
private final Text clientConnectionText;
private final Text clientConnectionPort;
private final Text serverToClientText;
private final Text clientToServerText;
public static void main(String [] args) {
Log.addWrapper(new ConsoleLogWrapper(LogLevel.VERBOSE));
launch(args);
}
public Forwarder() {
executor = Executors.newSingleThreadExecutor();
usernameField = new TextField("");
passwordField = new PasswordField();
serverIpField = new TextField();
serverPortField = new TextField();
serverConnectionText = new Text(getConnectionStatus(false));
serverStatusText = new Text(ServerConnectionStatus.DISCONNECTED.name());
clientConnectionText = new Text(getConnectionStatus(false));
clientConnectionPort = new Text();
serverToClientText = new Text("0");
clientToServerText = new Text("0");
connections = new HolocoreConnection();
updateConnection(serverConnectionText, false);
updateConnection(clientConnectionText, false);
usernameField.promptTextProperty().setValue("Username");
passwordField.promptTextProperty().setValue("Password");
serverIpField.setOnKeyPressed((event) -> updateServerIp());
serverPortField.setOnKeyPressed((event) -> updateServerIp());
}
@Override
public void onServerStatusChanged(ServerConnectionStatus oldStatus, ServerConnectionStatus status) {
Platform.runLater(() -> {
serverStatusText.setText(status.name().replace('_', ' '));
updateConnection(serverConnectionText, status == ServerConnectionStatus.CONNECTED);
});
}
@Override
public void onClientConnected() {
Platform.runLater(() -> updateConnection(clientConnectionText, true));
}
@Override
public void onClientDisconnected() {
Platform.runLater(() -> updateConnection(clientConnectionText, false));
}
@Override
public void onDataClientToServer(byte[] data) {
Platform.runLater(() -> clientToServerText.setText(getByteName(connections.getClientToServerCount())));
}
@Override
public void onDataServerToClient(byte[] data) {
Platform.runLater(() -> serverToClientText.setText(getByteName(connections.getServerToClientCount())));
}
private void updateConnection(Text t, boolean status) {
t.setText(getConnectionStatus(status));
t.setFill(status ? Color.GREEN : Color.RED);
}
private void updateServerIp() {
executor.execute(() -> {
try {
InetAddress addr = InetAddress.getByName(serverIpField.getText());
int port = Integer.parseInt(serverPortField.getText());
connections.setRemote(addr, port);
} catch (UnknownHostException e) {
Log.e("Unknown IP: " + serverIpField.getText());
} catch (NumberFormatException e) {
Log.e("Invalid Port: " + serverPortField.getText());
}
});
}
@Override
public void start(Stage primaryStage) throws Exception {
initializeConnections();
clientConnectionPort.setText(Integer.toString(connections.getLoginPort()));
GridPane root = new GridPane();
setupGridPane(root);
Scene scene = new Scene(root, 400, 160);
primaryStage.setTitle("Holocore Forwarder [" + Connections.VERSION + "]");
primaryStage.setScene(scene);
primaryStage.setMinWidth(scene.getWidth());
primaryStage.setMinHeight(scene.getHeight());
primaryStage.setMaxWidth(scene.getWidth());
primaryStage.setMaxHeight(scene.getHeight());
root.setOnMouseClicked((event) -> root.requestFocus());
root.requestFocus();
primaryStage.show();
primaryStage.setOnCloseRequest(we -> {
try {
connections.stop();
} catch (Exception e) {
Log.e(e);
}
primaryStage.close();
});
}
private void initializeConnections() {
connections.start();
serverIpField.setText(connections.getRemoteAddress().getHostAddress());
serverPortField.setText(Integer.toString(connections.getRemotePort()));
clientConnectionPort.setText(Integer.toString(connections.getLoginPort()));
usernameField.setText(connections.getInterceptorProperties().getUsername());
passwordField.setText(connections.getInterceptorProperties().getPassword());
usernameField.textProperty().addListener((event, oldValue, newValue) -> connections.getInterceptorProperties().setUsername(newValue));
passwordField.textProperty().addListener((event, oldValue, newValue) -> connections.getInterceptorProperties().setPassword(newValue));
connections.setCallback(this);
}
private void setupGridPane(GridPane root) {
addColumnConstraint(root, 50);
addColumnConstraint(root, 100);
addColumnConstraint(root, 75);
addColumnConstraint(root, 175);
root.add(usernameField, 0, 0, 2, 1);
root.add(passwordField, 2, 0, 2, 1);
root.add(serverIpField, 0, 1, 2, 1);
root.add(serverPortField, 2, 1, 1, 1);
root.add(new Text("Server Connection:"), 0, 2, 2, 1);
root.add(serverConnectionText, 2, 2, 1, 1);
root.add(serverStatusText, 3, 2, 1, 1);
root.add(new Text("Client Connection:"), 0, 3, 2, 1);
root.add(clientConnectionText, 2, 3, 2, 1);
root.add(clientConnectionPort, 3, 3, 1, 1);
root.add(new Text("Server->Client"), 0, 4, 2, 1);
root.add(serverToClientText, 2, 4, 2, 1);
root.add(new Text("Client->Server"), 0, 5, 2, 1);
root.add(clientToServerText, 2, 5, 2, 1);
}
private void addColumnConstraint(GridPane root, double width) {
ColumnConstraints cc = new ColumnConstraints();
cc.setPrefWidth(width);
root.getColumnConstraints().add(cc);
}
private static String getByteName(long bytes) {
int index = 0;
double reduced = bytes;
while (reduced >= 1024 && index < DATA_NAMES.length) {
reduced /= 1024;
index++;
}
if (index == 0)
return bytes + " B";
return String.format("%.2f %s", reduced, DATA_NAMES[index]);
}
private static String getConnectionStatus(boolean status) {
return status ? "ONLINE" : "OFFLINE";
}
}

View File

@@ -1,140 +0,0 @@
package com.projectswg;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.concurrent.atomic.AtomicBoolean;
import com.projectswg.Connections.ConnectionCallback;
import com.projectswg.common.concurrency.Delay;
import com.projectswg.common.debug.Log;
import com.projectswg.networking.NetInterceptor.InterceptorProperties;
import com.projectswg.resources.HolocorePreferences;
public class HolocoreConnection {
private static final InetAddress DEFAULT_ADDRESS;
static {
InetAddress addr;
try {
addr = InetAddress.getByName("::1");
} catch (UnknownHostException e) {
addr = InetAddress.getLoopbackAddress();
}
DEFAULT_ADDRESS = addr;
}
private final AtomicBoolean running;
private final InetAddress remoteAddr;
private final int remotePort;
private final boolean timeout;
private int loginPort;
private Connections connections;
public HolocoreConnection() {
this(DEFAULT_ADDRESS, 44463, 44453, true);
}
public HolocoreConnection(InetAddress remoteAddr, int remotePort, int loginPort, boolean timeout) {
this.remoteAddr = remoteAddr;
this.remotePort = remotePort;
this.timeout = timeout;
this.loginPort = loginPort;
this.running = new AtomicBoolean(false);
}
public void start() {
if (running.getAndSet(true)) {
Log.e("Not starting, already started!");
return;
}
boolean success = false;
int attempts = 0;
while (!success) {
Log.i("Initializing connections... attempt %d", attempts++);
connections = new Connections(remoteAddr, remotePort, loginPort, timeout);
success = connections.initialize() && connections.start();
if (!success) {
Log.e("Failed to initialize");
connections.stop();
connections.terminate();
if (Delay.sleepMilli(50)) {
Log.e("Interrupted while connecting!");
return;
}
loginPort++;
}
}
Log.i("Connections initialized.");
setProperties();
}
public void stop() {
if (!running.getAndSet(false)) {
Log.e("Not stopping, already stopped!");
return;
}
if (connections == null) {
Log.e("Not stopping, connections is null!");
return;
}
updateProperties();
connections.stop();
connections.terminate();
connections = null;
Log.i("Connections terminated.");
}
public void setCallback(ConnectionCallback callback) {
connections.setCallback(callback);
}
public boolean setRemote(InetAddress addr, int port) {
return connections.setRemote(addr, port);
}
public InetAddress getRemoteAddress() {
return connections.getRemoteAddress();
}
public int getRemotePort() {
return connections.getRemotePort();
}
public int getLoginPort() {
return connections.getLoginPort();
}
public int getZonePort() {
return connections.getZonePort();
}
public long getServerToClientCount() {
return connections.getServerToClientCount();
}
public long getClientToServerCount() {
return connections.getClientToServerCount();
}
public InterceptorProperties getInterceptorProperties() {
return connections.getInterceptorProperties();
}
private void setProperties() {
HolocorePreferences pref = HolocorePreferences.getInstance();
InterceptorProperties inter = getInterceptorProperties();
inter.setUsername(pref.getUsername());
inter.setPassword(pref.getPassword());
}
private void updateProperties() {
HolocorePreferences pref = HolocorePreferences.getInstance();
InterceptorProperties inter = getInterceptorProperties();
if (!inter.getUsername().isEmpty())
pref.setUsername(inter.getUsername());
if (!inter.getPassword().isEmpty())
pref.setPassword(inter.getPassword());
}
}

View File

@@ -1,32 +0,0 @@
package com.projectswg.intents;
import com.projectswg.common.control.Intent;
import com.projectswg.resources.ClientConnectionStatus;
public class ClientConnectionChangedIntent extends Intent {
private ClientConnectionStatus old;
private ClientConnectionStatus status;
public ClientConnectionChangedIntent(ClientConnectionStatus old, ClientConnectionStatus status) {
setOldStatus(old);
setStatus(status);
}
public ClientConnectionStatus getStatus() {
return status;
}
public void setStatus(ClientConnectionStatus status) {
this.status = status;
}
public ClientConnectionStatus getOldStatus() {
return old;
}
public void setOldStatus(ClientConnectionStatus old) {
this.old = old;
}
}

View File

@@ -1,26 +0,0 @@
package com.projectswg.intents;
import com.projectswg.common.control.Intent;
import com.projectswg.networking.Packet;
public class ClientSonyPacketIntent extends Intent {
private Packet packet;
public ClientSonyPacketIntent() {
this(null);
}
public ClientSonyPacketIntent(Packet packet) {
setPacket(packet);
}
public Packet getPacket() {
return packet;
}
public void setPacket(Packet packet) {
this.packet = packet;
}
}

View File

@@ -1,21 +0,0 @@
package com.projectswg.intents;
import com.projectswg.common.control.Intent;
public class ClientToServerPacketIntent extends Intent {
private byte [] data;
public ClientToServerPacketIntent(byte [] data) {
setData(data);
}
public byte [] getData() {
return data;
}
public void setData(byte [] data) {
this.data = data;
}
}

View File

@@ -1,43 +0,0 @@
package com.projectswg.intents;
import com.projectswg.common.control.Intent;
import com.projectswg.connection.ServerConnectionChangedReason;
import com.projectswg.connection.ServerConnectionStatus;
public class ServerConnectionChangedIntent extends Intent {
private ServerConnectionStatus old;
private ServerConnectionStatus status;
private ServerConnectionChangedReason reason;
public ServerConnectionChangedIntent(ServerConnectionStatus old, ServerConnectionStatus status, ServerConnectionChangedReason reason) {
setOldStatus(old);
setStatus(status);
setReason(reason);
}
public ServerConnectionStatus getStatus() {
return status;
}
public ServerConnectionStatus getOldStatus() {
return old;
}
public ServerConnectionChangedReason getReason() {
return reason;
}
public void setStatus(ServerConnectionStatus status) {
this.status = status;
}
public void setOldStatus(ServerConnectionStatus old) {
this.old = old;
}
public void setReason(ServerConnectionChangedReason reason) {
this.reason = reason;
}
}

View File

@@ -1,31 +0,0 @@
package com.projectswg.intents;
import com.projectswg.common.control.Intent;
public class ServerToClientPacketIntent extends Intent {
private int crc;
private byte [] rawData;
public ServerToClientPacketIntent(int crc, byte [] rawData) {
setCrc(crc);
setRawData(rawData);
}
public int getCrc() {
return crc;
}
public byte [] getRawData() {
return rawData;
}
public void setCrc(int crc) {
this.crc = crc;
}
public void setRawData(byte [] rawData) {
this.rawData = rawData;
}
}

View File

@@ -1,113 +0,0 @@
package com.projectswg.networking;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import com.projectswg.common.data.encodables.galaxy.Galaxy;
import com.projectswg.common.network.NetBuffer;
import com.projectswg.common.network.packets.PacketType;
import com.projectswg.common.network.packets.swg.login.LoginClientId;
import com.projectswg.common.network.packets.swg.login.LoginClusterStatus;
import com.projectswg.networking.client.ClientData;
public class NetInterceptor {
private final InterceptorProperties properties;
private final ClientData clientData;
public NetInterceptor(ClientData clientData) {
this.properties = new InterceptorProperties();
this.clientData = clientData;
}
public InterceptorProperties getProperties() {
return properties;
}
public byte [] interceptClient(byte [] data) {
if (data.length < 6)
return data;
ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
PacketType type = PacketType.fromCrc(bb.getInt(2));
switch (type) {
case LOGIN_CLIENT_ID:
return setAutoLogin(NetBuffer.wrap(bb));
case CMD_SCENE_READY:
clientData.setZoning(false);
return data;
default:
return data;
}
}
public byte [] interceptServer(byte [] data) {
if (data.length < 6)
return data;
ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
PacketType type = PacketType.fromCrc(bb.getInt(2));
switch (type) {
case LOGIN_CLUSTER_STATUS:
return getServerList(NetBuffer.wrap(bb));
default:
return data;
}
}
private byte [] setAutoLogin(NetBuffer data) {
LoginClientId id = new LoginClientId(data);
if (!id.getUsername().equals(properties.getUsername()) || !id.getPassword().isEmpty())
return data.array();
id.setPassword(properties.getPassword());
return id.encode().array();
}
private byte [] getServerList(NetBuffer data) {
LoginClusterStatus cluster = new LoginClusterStatus();
cluster.decode(data);
for (Galaxy g : cluster.getGalaxies()) {
g.setAddress("127.0.0.1");
g.setZonePort((short) properties.getPort());
g.setPingPort((short) properties.getPort());
}
return cluster.encode().array();
}
public static class InterceptorProperties {
private int port;
private String username;
private String password;
public InterceptorProperties() {
port = 0;
username = "";
password = "";
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
}

View File

@@ -1,150 +0,0 @@
package com.projectswg.networking.client;
import com.projectswg.common.concurrency.Delay;
import com.projectswg.common.concurrency.PswgBasicScheduledThread;
import com.projectswg.common.control.Manager;
import com.projectswg.common.debug.Log;
import com.projectswg.common.network.packets.SWGPacket;
import com.projectswg.common.network.packets.swg.zone.HeartBeat;
import com.projectswg.intents.ServerToClientPacketIntent;
import com.projectswg.networking.NetInterceptor;
import com.projectswg.networking.NetInterceptor.InterceptorProperties;
import com.projectswg.networking.Packet;
import com.projectswg.networking.client.ClientServerSocket.IncomingPacket;
import com.projectswg.networking.client.receiver.ClientInboundService;
import com.projectswg.networking.client.sender.ClientOutboundService;
import com.projectswg.networking.soe.Disconnect;
import com.projectswg.networking.soe.Disconnect.DisconnectReason;
import com.projectswg.resources.ClientConnectionStatus;
public class ClientConnectionService extends Manager implements ClientPacketSender {
private final NetInterceptor interceptor;
private final ClientData clientData;
/* Send-related variables */
private final ClientOutboundService outboundService;
private final ClientServerSocket server;
/* Receive-related variables */
private final PswgBasicScheduledThread pingerThread;
private final ClientInboundService inboundService;
public ClientConnectionService(int initialLoginPort, boolean timeout) {
this.clientData = new ClientData();
this.interceptor = new NetInterceptor(clientData);
this.server = new ClientServerSocket(clientData, initialLoginPort);
this.outboundService = new ClientOutboundService(clientData, interceptor, this);
this.inboundService = new ClientInboundService(interceptor, clientData, this);
this.pingerThread = new PswgBasicScheduledThread("client-pinger", () -> ping(timeout));
addChildService(outboundService);
addChildService(inboundService);
}
public InterceptorProperties getInterceptorProperties() {
return interceptor.getProperties();
}
@Override
public boolean initialize() {
registerForIntent(ServerToClientPacketIntent.class, stcpi -> handleServerToClientPacketIntent(stcpi));
return super.initialize();
}
@Override
public boolean start() {
if (!server.connect(incoming -> onPacket(incoming)))
return false;
interceptor.getProperties().setPort(server.getZonePort());
pingerThread.startWithFixedRate(0, 1000);
return super.start();
}
@Override
public boolean stop() {
pingerThread.stop();
pingerThread.awaitTermination(3000);
server.disconnect();
return super.stop();
}
public boolean isConnected() {
return server.isConnected();
}
public void waitForClientAcknowledge() {
while (clientData.isWaitingForClientAcknowledge()) {
if (Delay.sleepMicro(5))
break;
}
}
public void disconnect(DisconnectReason reason) {
if (clientData.getConnectionId() != -1)
sendRaw(new Disconnect(clientData.getConnectionId(), reason));
inboundService.disconnect();
}
public int getLoginPort() {
return server.getLoginPort();
}
public int getZonePort() {
return server.getZonePort();
}
@Override
public void sendPackaged(byte[] ... packets) {
if (!clientData.isConnectionInitialized())
return;
for (byte[] data : packets)
outboundService.sendSequential(data);
}
@Override
public void sendPackaged(SWGPacket ... packets) {
if (!clientData.isConnectionInitialized())
return;
for (SWGPacket p : packets)
outboundService.sendSequential(p.encode().array());
}
@Override
public void sendRaw(byte[]... packets) {
if (!clientData.isConnectionInitialized())
return;
for (byte [] data : packets)
server.send(data);
}
@Override
public void sendRaw(Packet... packets) {
if (!clientData.isConnectionInitialized())
return;
for (Packet p : packets)
server.send(p.encode().array());
}
private void handleServerToClientPacketIntent(ServerToClientPacketIntent stcpi) {
sendPackaged(stcpi.getRawData());
}
private void onPacket(IncomingPacket incoming) {
if (incoming.getLength() == 4 && incoming.getData()[0] != 0) { // Reasonable to assume this is a ping packet
server.send(incoming.getPacket());
return;
}
inboundService.addPacket(incoming);
}
private void ping(boolean timeout) {
if (clientData.getStatus() == ClientConnectionStatus.DISCONNECTED || !timeout)
return;
if (clientData.isTimedOut()) {
Log.i("Disconnecting due to timeout! Time Since Packet: %.2fms Zoning: %b", clientData.getTimeSinceLastAcknowledgement(), clientData.isZoning());
disconnect(DisconnectReason.TIMEOUT);
} else {
sendPackaged(new HeartBeat().encode().array());
}
}
}

View File

@@ -1,156 +0,0 @@
package com.projectswg.networking.client;
import com.projectswg.networking.client.ClientServerSocket.ClientServer;
import com.projectswg.resources.ClientConnectionStatus;
public class ClientData {
private int connectionId;
private int communicationPort;
private short rxSequence;
private short txSequence;
private short ackSequence;
private short oooSequence;
private long lastAcknowledgement;
private boolean zoning;
private ClientServer server;
private ClientConnectionStatus status;
public ClientData() {
reset(ClientConnectionStatus.DISCONNECTED);
}
public void resetConnectionInfo() {
setConnectionId(-1);
setCommunicationPort(0);
setRxSequence((short) -1);
setTxSequence((short) 0);
setAckSequence((short) 0);
setOOOSequence((short) 0);
setLastAcknowledgement(0);
setZoning(false);
}
public void reset(ClientConnectionStatus status) {
resetConnectionInfo();
setStatus(status);
if (status == ClientConnectionStatus.LOGIN_CONNECTED)
setClientServer(ClientServer.LOGIN);
else if (status == ClientConnectionStatus.ZONE_CONNECTED)
setClientServer(ClientServer.ZONE);
else
setClientServer(ClientServer.NONE);
}
public int getConnectionId() {
return connectionId;
}
public int getCommunicationPort() {
return communicationPort;
}
public short getRxSequence() {
return rxSequence;
}
public short getTxSequence() {
return txSequence;
}
public short getAckSequence() {
return ackSequence;
}
public short getOOOSequence() {
return oooSequence;
}
public long getLastAcknowledgement() {
return lastAcknowledgement;
}
public boolean isZoning() {
return zoning;
}
public double getTimeSinceLastAcknowledgement() {
long last = lastAcknowledgement;
if (last == 0)
return 0;
return (System.nanoTime() - last) / 1E6;
}
public boolean isTimedOut() {
if (getTimeSinceLastAcknowledgement() > 5000 && !zoning)
return true;
return getTimeSinceLastAcknowledgement() > 30000 && zoning;
}
public ClientServer getClientServer() {
return server;
}
public ClientConnectionStatus getStatus() {
return status;
}
public boolean isWaitingForClientAcknowledge() {
return txSequence-1 > ackSequence && connectionId != -1;
}
public boolean isConnectionInitialized() {
return connectionId != -1 && communicationPort > 0;
}
public void setConnectionId(int connectionId) {
this.connectionId = connectionId;
}
public void setCommunicationPort(int communicationPort) {
this.communicationPort = communicationPort;
}
public void setRxSequence(short rxSequence) {
this.rxSequence = rxSequence;
}
public void setTxSequence(short txSequence) {
this.txSequence = txSequence;
}
public void setAckSequence(short ackSequence) {
this.ackSequence = ackSequence;
}
public void setOOOSequence(short oooSequence) {
this.oooSequence = oooSequence;
}
public void setLastAcknowledgement(long lastAcknowledgement) {
this.lastAcknowledgement = lastAcknowledgement;
}
public void setZoning(boolean zoning) {
this.zoning = zoning;
}
public void setClientServer(ClientServer server) {
this.server = server;
}
public ClientConnectionStatus setStatus(ClientConnectionStatus status) {
ClientConnectionStatus old = this.status;
this.status = status;
return old;
}
public short getAndIncrementRxSequence() {
return rxSequence++;
}
public short getAndIncrementTxSequence() {
return txSequence++;
}
}

View File

@@ -1,17 +0,0 @@
package com.projectswg.networking.client;
import com.projectswg.common.network.packets.SWGPacket;
import com.projectswg.networking.Packet;
public interface ClientPacketSender {
/** Adds the specified packets to a buffer to guarantee sending in-order */
void sendPackaged(byte[] ... packets);
/** Adds the specified packets to a buffer to guarantee sending in-order */
void sendPackaged(SWGPacket ... packets);
/** Sends the specified packets via UDP immediately */
void sendRaw(byte[] ... data);
/** Sends the specified packets via UDP immediately */
void sendRaw(Packet ... p);
}

View File

@@ -1,155 +0,0 @@
package com.projectswg.networking.client;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.util.concurrent.atomic.AtomicBoolean;
import com.projectswg.common.debug.Assert;
import com.projectswg.common.debug.Log;
import com.projectswg.networking.UDPServer;
/**
* This class is in charge of the login and zone UDP servers, and switching between the two
*/
public class ClientServerSocket {
private static final InetAddress ADDR = InetAddress.getLoopbackAddress();
private final AtomicBoolean connected;
private final ClientData data;
private UDPServer loginServer;
private UDPServer zoneServer;
private int loginPort;
public ClientServerSocket(ClientData data, int loginPort) {
Assert.test(loginPort >= 0, "Login port must be positive!");
this.connected = new AtomicBoolean(false);
this.data = data;
this.loginServer = null;
this.zoneServer = null;
setLoginPort(loginPort);
}
public boolean connect(SocketCallback callback) {
Assert.test(!connected.getAndSet(true));
Assert.notNull(callback);
try {
InetAddress loopback = InetAddress.getLoopbackAddress();
loginServer = new UDPServer(new InetSocketAddress(loopback, loginPort), 496);
zoneServer = new UDPServer(new InetSocketAddress(loopback, 0), 496);
loginServer.bind();
zoneServer.bind();
loginPort = loginServer.getPort();
Assert.test(loginPort > 0, "Login port was not set correctly by the UDPServer!");
loginServer.setCallback((packet) -> callback.onPacket(new IncomingPacket(ClientServer.LOGIN, packet)));
zoneServer.setCallback((packet) -> callback.onPacket(new IncomingPacket(ClientServer.ZONE, packet)));
return true;
} catch (SocketException e) {
disconnect();
Log.e(e);
}
return false;
}
public void disconnect() {
Assert.test(connected.getAndSet(false));
Assert.notNull(loginServer);
Assert.notNull(zoneServer);
loginServer.removeCallback();
loginServer.close();
zoneServer.removeCallback();
zoneServer.close();
}
public void send(byte [] packet) {
if (data.getClientServer() == ClientServer.NONE)
return;
Assert.test(packet.length > 0, "Packet length cannot be 0!");
Assert.test(data.getCommunicationPort() > 0, "Communication port has not been set!");
getServer().send(data.getCommunicationPort(), ADDR, packet);
}
public void send(DatagramPacket packet) {
if (data.getClientServer() == ClientServer.NONE)
return;
Assert.test(packet.getLength() > 0, "Packet length cannot be 0!");
Assert.test(packet.getData().length == packet.getLength(), "Data length and packet length do not match!");
getServer().send(packet);
}
private UDPServer getServer() {
switch (data.getClientServer()) {
case LOGIN:
return loginServer;
case ZONE:
return zoneServer;
default:
Assert.fail("Unknown server: " + data.getClientServer());
return null;
}
}
public void setLoginPort(int loginPort) {
Assert.test(loginPort >= 0, "Login port must be >= 0");
this.loginPort = loginPort;
}
public int getLoginPort() {
Assert.notNull(loginServer, "Login server has not been initialized");
return loginServer.getPort();
}
public int getZonePort() {
Assert.notNull(zoneServer, "Zone server has not been initialized");
return zoneServer.getPort();
}
public boolean isConnected() {
return connected.get();
}
public enum ClientServer {
NONE,
LOGIN,
ZONE
}
public interface SocketCallback {
void onPacket(IncomingPacket incoming);
}
public static class IncomingPacket {
private final ClientServer server;
private final DatagramPacket packet;
public IncomingPacket(ClientServer server, DatagramPacket packet) {
Assert.notNull(server, "Server cannot be null");
Assert.notNull(packet, "Packet cannot be null");
this.server = server;
this.packet = packet;
}
public ClientServer getServer() {
return server;
}
public DatagramPacket getPacket() {
return packet;
}
public int getPort() {
return packet.getPort();
}
public byte [] getData() {
return packet.getData();
}
public int getLength() {
return packet.getData().length;
}
}
}

View File

@@ -1,277 +0,0 @@
package com.projectswg.networking.client.receiver;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import com.projectswg.common.concurrency.PswgTaskThreadPool;
import com.projectswg.common.control.IntentChain;
import com.projectswg.common.control.IntentManager;
import com.projectswg.common.debug.Log;
import com.projectswg.intents.ClientConnectionChangedIntent;
import com.projectswg.intents.ClientSonyPacketIntent;
import com.projectswg.intents.ClientToServerPacketIntent;
import com.projectswg.networking.NetInterceptor;
import com.projectswg.networking.Packet;
import com.projectswg.networking.client.ClientData;
import com.projectswg.networking.client.ClientPacketSender;
import com.projectswg.networking.client.ClientServerSocket.ClientServer;
import com.projectswg.networking.client.ClientServerSocket.IncomingPacket;
import com.projectswg.networking.soe.Acknowledge;
import com.projectswg.networking.soe.ClientNetworkStatusUpdate;
import com.projectswg.networking.soe.DataChannel;
import com.projectswg.networking.soe.Disconnect;
import com.projectswg.networking.soe.Disconnect.DisconnectReason;
import com.projectswg.networking.soe.Fragmented;
import com.projectswg.networking.soe.KeepAlive;
import com.projectswg.networking.soe.MultiPacket;
import com.projectswg.networking.soe.OutOfOrder;
import com.projectswg.networking.soe.SequencedPacket;
import com.projectswg.networking.soe.ServerNetworkStatusUpdate;
import com.projectswg.networking.soe.SessionRequest;
import com.projectswg.networking.soe.SessionResponse;
import com.projectswg.resources.ClientConnectionStatus;
import com.projectswg.utilities.ByteUtilities;
public class ClientInboundProcessor {
private static final int MAX_PACKET_SIZE = 496;
private static final int GALACTIC_BASE_TIME = 1323043200;
private final NetInterceptor interceptor;
private final ClientData clientData;
private final ClientFragmentedProcessor fragmentedProcessor;
private final PswgTaskThreadPool<IncomingPacket> processorThreadPool;
private final ClientPacketSender packetSender;
private IntentChain processorIntentChain;
public ClientInboundProcessor(NetInterceptor interceptor, ClientData data, ClientPacketSender packetSender) {
this.interceptor = interceptor;
this.clientData = data;
this.fragmentedProcessor = new ClientFragmentedProcessor();
this.processorThreadPool = new PswgTaskThreadPool<>(1, "packet-processor", packet -> process(packet, packet.getData()));
this.packetSender = packetSender;
this.processorIntentChain = null;
}
public void start(IntentManager intentManager) {
processorIntentChain = new IntentChain(intentManager);
processorThreadPool.start();
}
public void stop() {
processorThreadPool.stop(false);
processorThreadPool.awaitTermination(1000);
}
public void addPacket(IncomingPacket packet) {
processorThreadPool.addTask(packet);
}
public void disconnect() {
setConnectionState(ClientConnectionStatus.DISCONNECTED);
}
public void onConnected() {
fragmentedProcessor.reset();
}
public void onDisconnected() {
setConnectionState(ClientConnectionStatus.DISCONNECTED);
clientData.reset(ClientConnectionStatus.DISCONNECTED);
fragmentedProcessor.reset();
}
private void process(IncomingPacket packet, byte [] raw) {
if (raw.length < 2)
return;
ByteBuffer data = ByteBuffer.wrap(raw).order(ByteOrder.BIG_ENDIAN);
if (data.get(0) != 0) {
onSWGPacket(data.array());
return;
}
Packet p;
try {
p = createPacket(data);
if (p == null)
return;
} catch (BufferUnderflowException e) {
Log.w("Invalid packet structure received! %s", ByteUtilities.getHexString(raw));
return;
}
if (!(p instanceof SessionRequest) && clientData.getStatus() == ClientConnectionStatus.DISCONNECTED) {
Log.i("Packet sent out of connection: %s", p);
return;
}
handlePacket(packet, p);
}
private Packet createPacket(ByteBuffer data) {
short opcode = data.getShort(0);
switch (opcode) {
case 0x01: return new SessionRequest(data);
case 0x03: return new MultiPacket(data);
case 0x05: return new Disconnect(data);
case 0x06: return new KeepAlive(data);
case 0x07: return new ClientNetworkStatusUpdate(data);
case 0x09:
case 0x0A:
case 0x0B:
case 0x0C: return new DataChannel(data);
case 0x0D:
case 0x0E:
case 0x0F:
case 0x10: return new Fragmented(data);
case 0x11:
case 0x12:
case 0x13:
case 0x14: return new OutOfOrder(data);
case 0x15:
case 0x16:
case 0x17:
case 0x18: return new Acknowledge(data);
default:
Log.e("Unknown SOE packet: %d %s", opcode, ByteUtilities.getHexString(data.array()));
return null;
}
}
private void handlePacket(IncomingPacket incoming, Packet p) {
if (p instanceof SessionRequest) {
onSessionRequest(incoming, (SessionRequest) p);
processorIntentChain.broadcastAfter(new ClientSonyPacketIntent(p));
return;
}
if (incoming.getServer() != clientData.getClientServer()) {
Log.w("Dropping packet %s with invalid server %s [expected %s]", p, incoming.getServer(), clientData.getClientServer());
return;
}
if (p instanceof MultiPacket)
onMultiPacket(incoming, (MultiPacket) p);
else if (p instanceof Disconnect)
onDisconnect(incoming, (Disconnect) p);
else if (p instanceof KeepAlive)
onKeepAlive((KeepAlive) p);
else if (p instanceof ClientNetworkStatusUpdate)
onClientNetwork((ClientNetworkStatusUpdate) p);
else if (p instanceof DataChannel)
onDataChannel((DataChannel) p);
else if (p instanceof Fragmented)
onFragmented(incoming, (Fragmented) p);
else if (p instanceof Acknowledge)
onAcknowledge((Acknowledge) p);
else if (p instanceof OutOfOrder)
onOutOfOrder((OutOfOrder) p);
else
Log.e("Unhandled SOE packet: %s", p);
processorIntentChain.broadcastAfter(new ClientSonyPacketIntent(p));
}
private void onSessionRequest(IncomingPacket incoming, SessionRequest request) {
ClientConnectionStatus newStatus;
switch (incoming.getServer()) {
case LOGIN:
Log.i("Login Session Request [port set to %d]", incoming.getPort());
newStatus = ClientConnectionStatus.LOGIN_CONNECTED;
break;
case ZONE:
Log.i("Zone Session Request [switching port from %d to %d]", clientData.getCommunicationPort(), incoming.getPort());
newStatus = ClientConnectionStatus.ZONE_CONNECTED;
break;
default:
Log.i("Unknown server in session request! Server: %s", incoming.getServer());
return;
}
clientData.resetConnectionInfo();
clientData.setClientServer(incoming.getServer());
if (incoming.getServer() == ClientServer.ZONE)
packetSender.sendRaw(new Disconnect(clientData.getConnectionId(), DisconnectReason.NEW_CONNECTION_ATTEMPT));
clientData.setConnectionId(request.getConnectionId());
clientData.setCommunicationPort(incoming.getPort());
packetSender.sendRaw(new SessionResponse(request.getConnectionId(), 0, (byte) 0, (byte) 0, (byte) 0, MAX_PACKET_SIZE));
setConnectionState(newStatus);
}
private void onMultiPacket(IncomingPacket incoming, MultiPacket packet) {
for (byte [] p : packet.getPackets()) {
process(incoming, p);
}
}
private void onDisconnect(IncomingPacket incoming, Disconnect disconnect) {
if (disconnect.getConnectionId() != clientData.getConnectionId()) {
Log.w("Ignoring old disconnect! Current ID: %d Disconnect ID: %d", clientData.getConnectionId(), disconnect.getConnectionId());
return;
}
Log.i("Received client disconnect [port=%d reason=%s]", incoming.getPort(), disconnect.getReason());
setConnectionState(ClientConnectionStatus.DISCONNECTED);
}
private void onKeepAlive(KeepAlive alive) {
packetSender.sendRaw(new KeepAlive());
}
private void onClientNetwork(ClientNetworkStatusUpdate update) {
ServerNetworkStatusUpdate serverNet = new ServerNetworkStatusUpdate();
serverNet.setClientTickCount((short) update.getTick());
serverNet.setServerSyncStampLong((int) (System.currentTimeMillis()-GALACTIC_BASE_TIME));
serverNet.setClientPacketsSent(update.getSent());
serverNet.setClientPacketsRecv(update.getRecv());
serverNet.setServerPacketsSent(clientData.getTxSequence());
serverNet.setServerPacketsRecv(clientData.getRxSequence()+1);
packetSender.sendRaw(serverNet);
}
private boolean validateSequenced(SequencedPacket sequenced) {
short rx = (short) (clientData.getRxSequence()+1);
if (sequenced.getSequence() != rx) {
if (sequenced.getSequence() > rx)
packetSender.sendRaw(new OutOfOrder(sequenced.getSequence()));
Log.e("Invalid Sequence! Expected: " + rx + " Actual: " + sequenced.getSequence());
return false;
}
clientData.setRxSequence(sequenced.getSequence());
packetSender.sendRaw(new Acknowledge(rx));
return true;
}
private void onDataChannel(DataChannel dataChannel) {
if (!validateSequenced(dataChannel))
return;
for (byte [] data : dataChannel.getPackets()) {
onSWGPacket(data);
}
}
private void onFragmented(IncomingPacket incoming, Fragmented frag) {
if (!validateSequenced(frag))
return;
byte [] combined = fragmentedProcessor.addFragmented(frag);
if (combined != null)
process(incoming, combined);
}
private void onAcknowledge(Acknowledge ack) {
clientData.setLastAcknowledgement(System.nanoTime());
clientData.setAckSequence(ack.getSequence());
}
private void onOutOfOrder(OutOfOrder ooo) {
clientData.setOOOSequence(ooo.getSequence());
}
private void onSWGPacket(byte [] data) {
data = interceptor.interceptClient(data);
processorIntentChain.broadcastAfter(new ClientToServerPacketIntent(data));
}
private void setConnectionState(ClientConnectionStatus status) {
ClientConnectionStatus old = clientData.setStatus(status);
if (old != status) {
Log.i("Client Status: %s -> %s", old, status);
processorIntentChain.broadcastAfter(new ClientConnectionChangedIntent(old, status));
}
}
}

View File

@@ -1,57 +0,0 @@
package com.projectswg.networking.client.receiver;
import com.projectswg.common.control.Service;
import com.projectswg.intents.ClientConnectionChangedIntent;
import com.projectswg.networking.NetInterceptor;
import com.projectswg.networking.client.ClientData;
import com.projectswg.networking.client.ClientPacketSender;
import com.projectswg.networking.client.ClientServerSocket.IncomingPacket;
import com.projectswg.resources.ClientConnectionStatus;
public class ClientInboundService extends Service {
private final ClientInboundProcessor processor;
public ClientInboundService(NetInterceptor interceptor, ClientData data, ClientPacketSender packetSender) {
this.processor = new ClientInboundProcessor(interceptor, data, packetSender);
}
@Override
public boolean initialize() {
registerForIntent(ClientConnectionChangedIntent.class, ccci -> onConnectionChanged(ccci.getStatus()));
return super.initialize();
}
@Override
public boolean start() {
processor.start(getIntentManager());
return super.start();
}
@Override
public boolean stop() {
processor.stop();
return super.stop();
}
public void addPacket(IncomingPacket packet) {
processor.addPacket(packet);
}
public void disconnect() {
processor.disconnect();
}
private void onConnectionChanged(ClientConnectionStatus status) {
switch (status) {
case LOGIN_CONNECTED:
case ZONE_CONNECTED:
processor.onConnected();
break;
case DISCONNECTED:
processor.onDisconnected();
break;
}
}
}

View File

@@ -1,32 +0,0 @@
package com.projectswg.networking.client.sender;
import com.projectswg.common.control.Service;
import com.projectswg.networking.NetInterceptor;
import com.projectswg.networking.client.ClientData;
import com.projectswg.networking.client.ClientPacketSender;
public class ClientOutboundService extends Service {
private final ClientPackager packager;
public ClientOutboundService(ClientData data, NetInterceptor interceptor, ClientPacketSender sender) {
this.packager = new ClientPackager(interceptor, data, sender);
}
@Override
public boolean start() {
packager.start(getIntentManager());
return super.start();
}
@Override
public boolean stop() {
packager.stop();
return super.stop();
}
public void sendSequential(byte [] data) {
packager.addToPackage(data);
}
}

View File

@@ -1,219 +0,0 @@
package com.projectswg.networking.client.sender;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import com.projectswg.common.concurrency.Delay;
import com.projectswg.common.concurrency.PswgBasicThread;
import com.projectswg.common.control.IntentManager;
import com.projectswg.common.debug.Assert;
import com.projectswg.intents.ClientConnectionChangedIntent;
import com.projectswg.networking.NetInterceptor;
import com.projectswg.networking.client.ClientData;
import com.projectswg.networking.client.ClientPacketSender;
import com.projectswg.networking.soe.DataChannel;
import com.projectswg.networking.soe.Fragmented;
import com.projectswg.resources.ClientConnectionStatus;
public class ClientPackager {
private final NetInterceptor interceptor;
private final ClientPacketResender packetResender;
private final PackagingWrapper packager;
public ClientPackager(NetInterceptor interceptor, ClientData clientData, ClientPacketSender sender) {
this.interceptor = interceptor;
this.packetResender = new ClientPacketResender(sender);
this.packager = new PackagingWrapper(packetResender, clientData);
}
public void start(IntentManager intentManager) {
packetResender.start(intentManager);
packager.start();
intentManager.registerForIntent(ClientConnectionChangedIntent.class, ccci -> handleClientStatusChanged(ccci.getStatus()));
}
public void stop() {
packager.stop();
packetResender.stop();
}
public void clear() {
packager.reset();
}
public void addToPackage(byte [] data) {
Assert.test(data.length > 0, "Array length must be greater than 0!");
packager.add(interceptor.interceptServer(data));
}
private void handleClientStatusChanged(ClientConnectionStatus status) {
clear();
}
private static class PackagingWrapper {
private final AtomicBoolean running;
private final PswgBasicThread packagerThread;
private final ClientPacketResender packetResender;
private final Packager packager;
private final Queue<byte []> inboundQueue;
private final Lock inboundQueueLock;
private final Condition inboundQueueCondition;
public PackagingWrapper(ClientPacketResender packetResender, ClientData clientData) {
this.running = new AtomicBoolean(false);
this.packagerThread = new PswgBasicThread("packet-packager", this::loop);
this.packetResender = packetResender;
this.packager = new Packager(clientData);
this.inboundQueue = new ArrayDeque<>(128);
this.inboundQueueLock = new ReentrantLock(false);
this.inboundQueueCondition = inboundQueueLock.newCondition();
}
public void start() {
running.set(true);
packagerThread.start();
}
public void stop() {
running.set(false);
packagerThread.stop(true);
packagerThread.awaitTermination(500);
}
public void reset() {
inboundQueue.clear();
}
public void add(byte [] packet) {
inboundQueueLock.lock();
try {
inboundQueue.add(packet);
inboundQueueCondition.signal();
} finally {
inboundQueueLock.unlock();
}
}
private void loop() {
Queue<SequencedOutbound> outboundQueue = new ArrayDeque<>(64);
while (running.get()) {
if (!waitForInbound())
break;
processInbound(outboundQueue);
Delay.sleepMicro(25); // Accumulates some packets
}
}
/**
* Waits for the queue to be non-empty and acquires the lock
* @return TRUE if the lock is acquired and the queue is non-empty, FALSE otherwise
*/
private boolean waitForInbound() {
inboundQueueLock.lock();
while (inboundQueue.isEmpty()) {
try {
inboundQueueCondition.await();
} catch (InterruptedException e) {
running.set(false);
return false;
}
}
return true;
}
private void processInbound(Queue<SequencedOutbound> outboundQueue) {
try {
packager.handle(inboundQueue, outboundQueue);
} finally {
inboundQueueLock.unlock();
}
while (!outboundQueue.isEmpty()) {
packetResender.add(outboundQueue.poll());
}
}
}
private static class Packager {
private final AtomicInteger size;
private final DataChannel channel;
private final ClientData clientData;
public Packager(ClientData clientData) {
this.size = new AtomicInteger(8);
this.channel = new DataChannel();
this.clientData = clientData;
}
/**
* Processes the inbound queue, and then sends it to the outbound queue
* @param inboundQueue inbound queue from the server
* @param outboundQueue outbound queue to the client
*/
public void handle(Queue<byte []> inboundQueue, Queue<SequencedOutbound> outboundQueue) {
byte [] packet;
int packetSize;
while (!inboundQueue.isEmpty()) {
packet = inboundQueue.poll();
packetSize = getPacketLength(packet);
if (size.get() + packetSize >= 496) // overflowed previous packet
sendDataChannel(outboundQueue);
if (packetSize < 496) {
addToDataChannel(packet, packetSize);
} else {
sendFragmented(outboundQueue, packet);
}
}
sendDataChannel(outboundQueue);
}
private void addToDataChannel(byte [] packet, int packetSize) {
channel.addPacket(packet);
size.getAndAdd(packetSize);
}
private void sendDataChannel(Queue<SequencedOutbound> outboundQueue) {
if (channel.getPacketCount() == 0)
return;
channel.setSequence(clientData.getAndIncrementTxSequence());
outboundQueue.add(new SequencedOutbound(channel.getSequence(), channel.encode().array()));
reset();
}
private void sendFragmented(Queue<SequencedOutbound> outboundQueue, byte [] packet) {
Fragmented [] frags = Fragmented.encode(ByteBuffer.wrap(packet), clientData.getTxSequence());
clientData.setTxSequence((short) (clientData.getTxSequence() + frags.length));
for (Fragmented frag : frags) {
outboundQueue.add(new SequencedOutbound(frag.getSequence(), frag.encode().array()));
}
}
private void reset() {
channel.clearPackets();
size.set(8);
}
private static int getPacketLength(byte [] data) {
int len = data.length;
if (len >= 255)
return len + 3;
return len + 1;
}
}
}

View File

@@ -1,344 +0,0 @@
package com.projectswg.networking.client.sender;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import com.projectswg.common.concurrency.Delay;
import com.projectswg.common.concurrency.PswgBasicThread;
import com.projectswg.common.control.IntentManager;
import com.projectswg.common.debug.Log;
import com.projectswg.intents.ClientConnectionChangedIntent;
import com.projectswg.intents.ClientSonyPacketIntent;
import com.projectswg.networking.Packet;
import com.projectswg.networking.client.ClientPacketSender;
import com.projectswg.networking.soe.Acknowledge;
import com.projectswg.networking.soe.OutOfOrder;
import com.projectswg.resources.ClientConnectionStatus;
/**
* This class is in charge of resending any unacknowledged packets
*/
public class ClientPacketResender {
private final ResenderWrapper resender;
private final PswgBasicThread executor;
private final AtomicBoolean running;
public ClientPacketResender(ClientPacketSender sender) {
this.resender = new ResenderWrapper(sender);
this.executor = new PswgBasicThread("packet-resender", this::resender);
this.running = new AtomicBoolean(false);
}
public void start(IntentManager intentManager) {
intentManager.registerForIntent(ClientSonyPacketIntent.class, cspi -> handleClientPacket(cspi.getPacket()));
intentManager.registerForIntent(ClientConnectionChangedIntent.class, ccci -> handleClientStatusChanged(ccci.getStatus()));
running.set(true);
executor.start();
}
public void stop() {
running.set(false);
executor.stop(true);
executor.awaitTermination(500);
resender.reset();
}
public void add(SequencedOutbound out) {
resender.add(out);
}
private void resender() {
while (running.get()) {
resender.resend();
}
}
private void handleClientPacket(Packet p) {
if (p instanceof Acknowledge)
resender.onAcknowledge(((Acknowledge) p).getSequence());
else if (p instanceof OutOfOrder)
resender.onOutOfOrder(((OutOfOrder) p).getSequence());
}
private void handleClientStatusChanged(ClientConnectionStatus status) {
resender.reset();
}
private static class ResenderWrapper {
private final List<SequencedOutbound> sentPackets;
private final CongestionAvoidance congAvoidance;
private final Lock sentPacketsLock;
private final Condition sentPacketsCondition;
private final Resender resender;
public ResenderWrapper(ClientPacketSender sender) {
this.sentPackets = new LinkedList<>();
this.congAvoidance = new CongestionAvoidance();
this.sentPacketsLock = new ReentrantLock(false);
this.sentPacketsCondition = sentPacketsLock.newCondition();
this.resender = new Resender(congAvoidance, sender);
}
public void add(SequencedOutbound seq) {
sentPacketsLock.lock();
try {
sentPackets.add(seq);
sentPacketsCondition.signal();
} finally {
sentPacketsLock.unlock();
}
}
public void reset() {
sentPacketsLock.lock();
try {
sentPackets.clear();
} finally {
sentPacketsLock.unlock();
}
}
public void onOutOfOrder(short sequence) {
Log.w("OOO %d", sequence);
sentPacketsLock.lock();
try {
congAvoidance.onOutOfOrder();
} finally {
sentPacketsLock.unlock();
}
}
public void onAcknowledge(short sequence) {
sentPacketsLock.lock();
try {
int seqInt = sequence & 0xFFFF;
SequencedOutbound out;
while (!sentPackets.isEmpty()) {
out = sentPackets.get(0);
if (out.getSequenceInt() <= seqInt || (seqInt < 100 && out.getSequenceInt() > Short.MAX_VALUE-100)) {
sentPackets.remove(0);
} else {
break;
}
}
congAvoidance.onAcknowledgement();
} finally {
sentPacketsLock.unlock();
}
}
public void resend() {
sentPacketsLock.lock();
try {
waitForPacket();
resender.handle(sentPackets);
} finally {
sentPacketsLock.unlock();
}
waitForTimeoutOrAck();
}
private void waitForPacket() {
if (!sentPackets.isEmpty())
return;
try {
while (sentPackets.isEmpty()) {
sentPacketsCondition.await();
}
} catch (InterruptedException e) {
}
congAvoidance.clearTimeSinceSent();
}
private void waitForTimeoutOrAck() {
double avg = congAvoidance.getAverageRTT();
if (avg == Double.MAX_VALUE)
Delay.sleepMicro(5);
else
Delay.sleepNano((long) Math.min(1E9, Math.max(5E6, avg)));
}
}
private static class Resender {
private final CongestionAvoidance congAvoidance;
private final ClientPacketSender sender;
public Resender(CongestionAvoidance congAvoidance, ClientPacketSender sender) {
this.congAvoidance = congAvoidance;
this.sender = sender;
}
public void handle(List<SequencedOutbound> sentPackets) {
congAvoidance.markBeginningOfWindow();
int sentPacketCount = sentPackets.size();
boolean lossEvent = sentPacketCount > 0 && (congAvoidance.isTimedOut() || congAvoidance.isTripleACK());
printStatus(lossEvent);
sendAllInWindow(sentPackets);
if (sentPacketCount < 100) {
casualWindowIteration(lossEvent);
} else {
intenseWindowIteration(lossEvent, sentPacketCount);
}
congAvoidance.markEndOfWindow();
}
private void printStatus(boolean lossEvent) {
if (congAvoidance.getWindow() != 50 && congAvoidance.getAverageRTT() != Double.MAX_VALUE)
Log.d("Resender: Congestion Window: %d Average RTT: %.3fms", congAvoidance.getWindow(), congAvoidance.getAverageRTT()/1E6);
if (lossEvent)
Log.w("Resender: Loss Event!");
}
/**
* This congestion avoidance algorithm focuses more on slow window
* increases/decreases. This is ideal for casual gameplay where there
* aren't many packets being sent at once.
*/
private void casualWindowIteration(boolean lossEvent) {
if (lossEvent) {
congAvoidance.setWindow(Math.max(10, congAvoidance.getWindow() - 5));
} else {
congAvoidance.setWindow(Math.min(50, congAvoidance.getWindow() + 5));
}
}
/**
* This congestion avoidance algorithm focuses on fast window
* increases/decreases. This is ideal for zone-in where as many packets as
* possible need to be sent as fast as possible.
*/
private void intenseWindowIteration(boolean lossEvent, int sentPackets) {
int window = congAvoidance.getWindow();
if (window > sentPackets)
return;
if (lossEvent) {
congAvoidance.setWindow(Math.max(50, window / 4));
} else {
congAvoidance.setWindow((int) (window * 1.5));
}
}
private void sendAllInWindow(List<SequencedOutbound> sentPackets) {
int max = congAvoidance.getWindow();
for (SequencedOutbound out : sentPackets) {
if (--max < 0)
break;
sender.sendRaw(out.getData());
congAvoidance.onSentPacket();
}
congAvoidance.updateTimeSinceSent();
}
}
private static class CongestionAvoidance {
// Higher-level properties
private long lastSent;
private double averageRtt;
private int congestionWindow;
// Lower-level properties
private int outOfOrders;
private int acknowledgements;
private int sentPackets;
private int missedWindows;
public CongestionAvoidance() {
this.averageRtt = Double.MAX_VALUE;
reset();
}
public void reset() {
this.lastSent = 0;
this.congestionWindow = 1;
this.outOfOrders = 0;
this.acknowledgements = 0;
this.sentPackets = 0;
this.missedWindows = 0;
}
public void onOutOfOrder() {
updateRtt();
this.outOfOrders++;
}
public void onAcknowledgement() {
updateRtt();
this.outOfOrders = 0;
this.acknowledgements++;
}
public void onSentPacket() {
this.sentPackets++;
}
public void markBeginningOfWindow() {
if (acknowledgements == 0 && sentPackets > 0)
missedWindows++;
else
missedWindows = 0;
this.sentPackets = 0;
}
public void markEndOfWindow() {
this.acknowledgements = 0;
}
public void setWindow(int congestionWindow) {
this.congestionWindow = congestionWindow;
}
public double getAverageRTT() {
return averageRtt;
}
public int getWindow() {
return congestionWindow;
}
public boolean isTripleACK() {
return outOfOrders >= 3;
}
public boolean isTimedOut() {
return missedWindows >= 2 && averageRtt != Double.MAX_VALUE;
}
public void updateTimeSinceSent() {
if (lastSent == 0)
lastSent = System.nanoTime();
}
private void updateRtt() {
long lastRtt = lastSent;
lastSent = 0;
if (lastRtt <= 0)
return;
lastRtt = System.nanoTime() - lastRtt;
if (this.averageRtt == Double.MAX_VALUE)
this.averageRtt = lastRtt;
else
this.averageRtt = averageRtt * 0.875 + lastRtt * 0.125;
}
public void clearTimeSinceSent() {
lastSent = 0;
}
}
}

View File

@@ -1,40 +0,0 @@
package com.projectswg.networking.client.sender;
import com.projectswg.networking.soe.SequencedPacket;
public class SequencedOutbound implements SequencedPacket {
private final int sequence;
private final byte [] data;
public SequencedOutbound(short sequence, byte [] data) {
this.sequence = sequence & 0xFFFF;
this.data = data;
}
@Override
public short getSequence() { return (short) sequence; }
public int getSequenceInt() { return sequence; }
public byte [] getData() { return data; }
@Override
public int compareTo(SequencedPacket p) {
if (getSequence() < p.getSequence())
return -1;
if (getSequence() == p.getSequence())
return 0;
return 1;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof SequencedOutbound))
return super.equals(o);
return ((SequencedOutbound) o).getSequence() == sequence;
}
@Override
public int hashCode() {
return sequence;
}
}

View File

@@ -1,193 +0,0 @@
package com.projectswg.networking.server;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import com.projectswg.common.concurrency.Delay;
import com.projectswg.common.concurrency.PswgBasicScheduledThread;
import com.projectswg.common.concurrency.PswgBasicThread;
import com.projectswg.common.control.IntentChain;
import com.projectswg.common.control.IntentManager;
import com.projectswg.common.control.Service;
import com.projectswg.common.debug.Assert;
import com.projectswg.common.debug.Log;
import com.projectswg.common.network.packets.swg.zone.HeartBeat;
import com.projectswg.connection.HolocoreSocket;
import com.projectswg.connection.ServerConnectionChangedReason;
import com.projectswg.connection.packets.RawPacket;
import com.projectswg.intents.ClientConnectionChangedIntent;
import com.projectswg.intents.ClientToServerPacketIntent;
import com.projectswg.intents.ServerConnectionChangedIntent;
import com.projectswg.intents.ServerToClientPacketIntent;
public class ServerConnectionService extends Service {
private static final long HOLOCORE_TIMEOUT = TimeUnit.SECONDS.toNanos(21);
private final HolocoreSocket socket;
private final ConnectionThread connectionThread;
private IntentChain socketIntentChain;
public ServerConnectionService(InetAddress addr, int port) {
this.socket = new HolocoreSocket(addr, port);
this.connectionThread = new ConnectionThread(socket);
}
@Override
public boolean initialize() {
socketIntentChain = new IntentChain(getIntentManager());
connectionThread.initialize(getIntentManager());
this.socket.setStatusChangedCallback((o, n, reason) -> socketIntentChain.broadcastAfter(new ServerConnectionChangedIntent(o, n, reason)));
registerForIntent(ClientConnectionChangedIntent.class, ccci -> processClientConnectionChanged(ccci));
registerForIntent(ClientToServerPacketIntent.class, ctspi -> send(ctspi.getData()));
return super.initialize();
}
@Override
public boolean terminate() {
socket.terminate();
stopServer();
return super.terminate();
}
public void setRemoteAddress(InetAddress addr, int port) {
socket.setRemoteAddress(addr, port);
}
public InetSocketAddress getRemoteAddress() {
return socket.getRemoteAddress();
}
public boolean send(byte [] raw) {
if (!socket.isConnected()) {
connectionThread.addToOutQueue(raw);
return false;
}
return socket.send(raw);
}
private void processClientConnectionChanged(ClientConnectionChangedIntent ccci) {
Log.d("processClientConnectionChanged(%s)", ccci.getStatus());
switch (ccci.getStatus()) {
case LOGIN_CONNECTED:
startServer();
break;
case DISCONNECTED:
stopServer();
connectionThread.reset();
break;
default:
break;
}
}
private void startServer() {
connectionThread.start();
}
private void stopServer() {
connectionThread.stop();
}
private static class ConnectionThread {
private final HolocoreSocket connection;
private final AtomicLong lastHeartbeat;
private final PswgBasicThread thread;
private final PswgBasicScheduledThread heartbeatThread;
private final Queue<byte []> outQueue;
private IntentChain recvIntentChain;
public ConnectionThread(HolocoreSocket connection) {
this.connection = connection;
this.lastHeartbeat = new AtomicLong(0);
this.thread = new PswgBasicThread("server-connection", this::run);
this.heartbeatThread = new PswgBasicScheduledThread("server-heartbeat", this::heartbeat);
this.outQueue = new LinkedList<>();
this.recvIntentChain = null;
}
public void initialize(IntentManager intentManager) {
this.recvIntentChain = new IntentChain(intentManager);
}
public void reset() {
outQueue.clear();
}
public void start() {
if (thread.isRunning())
return;
thread.start();
heartbeatThread.startWithFixedDelay(0, 10*1000);
}
public void stop() {
if (!thread.isRunning())
return;
thread.stop(true);
heartbeatThread.stop();
thread.awaitTermination(5000);
heartbeatThread.awaitTermination(1000);
}
public void addToOutQueue(byte [] raw) {
synchronized (outQueue) {
outQueue.add(raw);
}
}
private void heartbeat() {
if (connection.isConnected()) {
long lastHeartbeat = this.lastHeartbeat.get();
if (lastHeartbeat != 0 && System.nanoTime() - lastHeartbeat >= HOLOCORE_TIMEOUT) {
stop();
return;
}
connection.send(new HeartBeat().encode().array());
}
}
private void run() {
Log.i("Started ServerConnection");
try {
while (thread.isRunning()) {
if (connection.isDisconnected() && !tryConnect()) {
Delay.sleepMilli(1000);
continue;
}
Assert.test(connection.isConnected());
RawPacket packet = null;
while ((packet = connection.receive()) != null) {
if (packet.getCrc() == HeartBeat.CRC)
lastHeartbeat.set(System.nanoTime());
recvIntentChain.broadcastAfter(new ServerToClientPacketIntent(packet.getCrc(), packet.getData()));
}
}
} catch (Throwable t) {
Log.e(t);
} finally {
connection.disconnect(ServerConnectionChangedReason.NONE);
}
Log.i("Stopped ServerConnection");
}
private boolean tryConnect() {
if (!connection.connect(10000))
return false;
synchronized (outQueue) {
while (!outQueue.isEmpty()) {
if (!connection.send(outQueue.poll()))
return false;
}
}
return true;
}
}
}

View File

@@ -1,67 +0,0 @@
package com.projectswg.networking.server;
import java.net.InetAddress;
import com.projectswg.common.control.IntentManager;
import com.projectswg.connection.ServerConnectionStatus;
import com.projectswg.intents.ClientConnectionChangedIntent;
import com.projectswg.intents.ServerConnectionChangedIntent;
import com.projectswg.intents.ServerToClientPacketIntent;
import com.projectswg.resources.ClientConnectionStatus;
public class ServerConnectionWrapper {
private ServerConnectionService connection;
private ConnectionCallback callback;
private IntentManager intentManager;
public ServerConnectionWrapper(InetAddress addr, int port) {
connection = new ServerConnectionService(addr, port);
intentManager = null;
}
public void setConnectionCallback(ConnectionCallback callback) {
this.callback = callback;
}
public void connect() {
setupConnectionIntents();
connection.initialize();
connection.start();
new ClientConnectionChangedIntent(ClientConnectionStatus.DISCONNECTED, ClientConnectionStatus.LOGIN_CONNECTED).broadcast(connection.getIntentManager());
}
public void disconnect() {
new ClientConnectionChangedIntent(ClientConnectionStatus.LOGIN_CONNECTED, ClientConnectionStatus.DISCONNECTED).broadcast(connection.getIntentManager());
connection.stop();
connection.terminate();
connection.getIntentManager().terminate();
}
public void send(byte [] data) {
connection.send(data);
}
private void setupConnectionIntents() {
intentManager = new IntentManager(Runtime.getRuntime().availableProcessors());
connection.setIntentManager(intentManager);
intentManager.registerForIntent(ServerConnectionChangedIntent.class, scci -> onServerConnectionChanged(scci));
intentManager.registerForIntent(ServerToClientPacketIntent.class, scpi -> onServerData(scpi));
}
private void onServerConnectionChanged(ServerConnectionChangedIntent i) {
if (callback != null)
callback.onServerConnectionChanged(i.getOldStatus(), i.getStatus());
}
private void onServerData(ServerToClientPacketIntent i) {
if (callback != null)
callback.onServerPacket(i.getCrc(), i.getRawData());
}
public interface ConnectionCallback {
void onServerConnectionChanged(ServerConnectionStatus old, ServerConnectionStatus status);
void onServerPacket(int crc, byte [] data);
}
}

View File

@@ -1,7 +0,0 @@
package com.projectswg.resources;
public enum ClientConnectionStatus {
DISCONNECTED,
LOGIN_CONNECTED,
ZONE_CONNECTED
}

View File

@@ -1,51 +0,0 @@
package com.projectswg.resources;
import java.util.prefs.Preferences;
public class HolocorePreferences {
private static final HolocorePreferences INSTANCE = new HolocorePreferences();
private static final String NODE_NAME = "/com/projectswg/SWG_Forwarder";
private final Preferences pref;
private HolocorePreferences() {
pref = Preferences.userRoot().node(NODE_NAME);
}
public String getUsername() {
return pref.get(PreferenceKeys.USERNAME.getKey(), "");
}
public void setUsername(String username) {
pref.put(PreferenceKeys.USERNAME.getKey(), username);
}
public String getPassword() {
return pref.get(PreferenceKeys.PASSWORD.getKey(), "");
}
public void setPassword(String username) {
pref.put(PreferenceKeys.PASSWORD.getKey(), username);
}
public static HolocorePreferences getInstance() {
return INSTANCE;
}
private enum PreferenceKeys {
USERNAME ("USERNAME"),
PASSWORD ("PASSWORD");
private String key;
PreferenceKeys(String key) {
this.key = key;
}
public String getKey() {
return key;
}
}
}

View File

@@ -1,112 +0,0 @@
package com.projectswg.services;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import com.projectswg.common.control.Service;
import com.projectswg.common.debug.Assert;
import com.projectswg.common.debug.Log;
import com.projectswg.connection.ServerConnectionStatus;
import com.projectswg.intents.ClientToServerPacketIntent;
import com.projectswg.intents.ServerConnectionChangedIntent;
import com.projectswg.intents.ServerToClientPacketIntent;
import com.projectswg.recording.PacketRecorder;
public class PacketRecordingService extends Service {
private final Object recordingMutex;
private File recorderFile;
private PacketRecorder recorder;
private ServerConnectionStatus serverStatus;
private InetSocketAddress source;
private InetSocketAddress destination;
private boolean recording;
public PacketRecordingService() {
this.recordingMutex = new Object();
this.recorderFile = null;
this.recorder = null;
this.serverStatus = ServerConnectionStatus.DISCONNECTED;
this.source = null;
this.destination = null;
this.recording = false;
}
@Override
public boolean initialize() {
registerForIntent(ServerToClientPacketIntent.class, stcpi -> onServerToClient(stcpi));
registerForIntent(ClientToServerPacketIntent.class, ctspi -> onClientToServer(ctspi));
registerForIntent(ServerConnectionChangedIntent.class, scci -> onServerStatusChanged(scci));
return super.initialize();
}
public void setAddress(InetSocketAddress source, InetSocketAddress destination) {
this.source = source;
this.destination = destination;
}
private void startRecording() {
synchronized (recordingMutex) {
Assert.test(!recording);
recording = true;
try {
recorderFile = File.createTempFile("HolocoreRecording", ".hcap");
recorder = new PacketRecorder(recorderFile);
} catch (IOException e) {
Log.e(e);
recorderFile = null;
recorder = null;
recording = false;
}
}
}
private void stopRecording() {
synchronized (recordingMutex) {
Assert.test(recording);
recording = false;
try {
recorder.close();
} catch (IOException e) {
Log.e(e);
}
}
}
private void onServerStatusChanged(ServerConnectionChangedIntent scci) {
this.serverStatus = scci.getStatus();
updateRecordingState();
}
private void updateRecordingState() {
switch (serverStatus) {
case CONNECTING:
startRecording();
break;
case DISCONNECTED:
stopRecording();
break;
default:
break;
}
}
private void onServerToClient(ServerToClientPacketIntent s2c) {
synchronized (recordingMutex) {
if (!recording)
return;
recorder.record(true, destination, source, s2c.getRawData());
}
}
private void onClientToServer(ClientToServerPacketIntent c2s) {
synchronized (recordingMutex) {
if (!recording)
return;
recorder.record(false, source, destination, c2s.getData());
}
}
}

View File

@@ -0,0 +1,16 @@
package com.projectswg.forwarder;
import com.projectswg.forwarder.services.client.ClientConnectionManager;
import com.projectswg.forwarder.services.crash.CrashManager;
import com.projectswg.forwarder.services.server.ServerConnectionManager;
import me.joshlarson.jlcommon.control.Manager;
import me.joshlarson.jlcommon.control.ManagerStructure;
@ManagerStructure(children = {
ClientConnectionManager.class,
ServerConnectionManager.class,
CrashManager.class
})
public class ConnectionManager extends Manager {
}

View File

@@ -0,0 +1,175 @@
package com.projectswg.forwarder;
import com.projectswg.forwarder.intents.client.ClientConnectedIntent;
import com.projectswg.forwarder.intents.client.ClientDisconnectedIntent;
import com.projectswg.forwarder.intents.control.ClientCrashedIntent;
import com.projectswg.forwarder.intents.control.StartForwarderIntent;
import com.projectswg.forwarder.intents.control.StopForwarderIntent;
import me.joshlarson.jlcommon.concurrency.Delay;
import me.joshlarson.jlcommon.control.IntentManager;
import me.joshlarson.jlcommon.control.Manager;
import me.joshlarson.jlcommon.control.SafeMain;
import me.joshlarson.jlcommon.log.Log;
import me.joshlarson.jlcommon.log.Log.LogLevel;
import me.joshlarson.jlcommon.log.log_wrapper.ConsoleLogWrapper;
import me.joshlarson.jlcommon.utilities.ThreadUtilities;
import java.io.*;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public class Forwarder {
private final ForwarderData data;
private final IntentManager intentManager;
private final AtomicBoolean connected;
public Forwarder() {
this.data = new ForwarderData();
this.intentManager = new IntentManager(false, Runtime.getRuntime().availableProcessors());
this.connected = new AtomicBoolean(false);
}
public static void main(String [] args) {
SafeMain.main("forwarder", Forwarder::mainRunnable);
}
private static void mainRunnable() {
Log.addWrapper(new ConsoleLogWrapper(LogLevel.TRACE));
Forwarder forwarder = new Forwarder();
forwarder.getData().setAddress(new InetSocketAddress(44463));
forwarder.run();
ThreadUtilities.printActiveThreads();
}
public File readClientOutput(InputStream is) {
StringBuilder output = new StringBuilder();
byte [] buffer = new byte[2048];
int n;
try {
while ((n = is.read(buffer)) > 0) {
output.append(new String(buffer, 0, n, StandardCharsets.UTF_8));
}
} catch (IOException e) {
Log.w("IOException while reading client output");
Log.w(e);
}
return onClientClosed(output.toString());
}
private File onClientClosed(String clientOutput) {
if (!connected.get())
return null;
File output;
try {
output = Files.createTempFile("HolocoreCrashLog", ".zip").toFile();
} catch (IOException e) {
Log.e("Failed to write crash log! Could not create temp file.");
return null;
}
try (ZipOutputStream zip = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(output)))) {
{
byte [] data = clientOutput.getBytes(StandardCharsets.UTF_8);
ZipEntry entry = new ZipEntry("output.txt");
entry.setTime(System.currentTimeMillis());
entry.setSize(data.length);
entry.setMethod(ZipOutputStream.DEFLATED);
zip.putNextEntry(entry);
zip.write(data);
zip.closeEntry();
}
ClientCrashedIntent cci = new ClientCrashedIntent(zip);
cci.broadcast(intentManager);
long startSleep = System.nanoTime();
while (!cci.isComplete() && System.nanoTime() - startSleep < 1E9)
Delay.sleepMilli(10);
return output;
} catch (IOException e) {
Log.e("Failed to write crash log! %s: %s", e.getClass().getName(), e.getMessage());
return null;
}
}
public void run() {
intentManager.initialize();
intentManager.registerForIntent(ClientConnectedIntent.class, cci -> connected.set(true));
intentManager.registerForIntent(ClientDisconnectedIntent.class, cdi -> connected.set(false));
ConnectionManager primary = new ConnectionManager();
{
primary.setIntentManager(intentManager);
List<Manager> managers = Collections.singletonList(primary);
Manager.start(managers);
new StartForwarderIntent(data).broadcast(intentManager);
Manager.run(managers, 100);
new StopForwarderIntent().broadcast(intentManager);
Manager.stop(managers);
}
intentManager.terminate(false);
primary.setIntentManager(null);
}
public ForwarderData getData() {
return data;
}
public static class ForwarderData {
private InetSocketAddress address = null;
private String username = null;
private String password = null;
private int loginPort = 0;
private int zonePort = 0;
private ForwarderData() { }
public InetSocketAddress getAddress() {
return address;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
public int getLoginPort() {
return loginPort;
}
public int getZonePort() {
return zonePort;
}
public void setAddress(InetSocketAddress address) {
this.address = address;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public void setLoginPort(int loginPort) {
this.loginPort = loginPort;
}
public void setZonePort(int zonePort) {
this.zonePort = zonePort;
}
}
}

View File

@@ -0,0 +1,11 @@
package com.projectswg.forwarder.intents.client;
import me.joshlarson.jlcommon.control.Intent;
public class ClientConnectedIntent extends Intent {
public ClientConnectedIntent() {
}
}

View File

@@ -0,0 +1,11 @@
package com.projectswg.forwarder.intents.client;
import me.joshlarson.jlcommon.control.Intent;
public class ClientDisconnectedIntent extends Intent {
public ClientDisconnectedIntent() {
}
}

View File

@@ -0,0 +1,20 @@
package com.projectswg.forwarder.intents.client;
import me.joshlarson.jlcommon.control.Intent;
import javax.annotation.Nonnull;
public class DataPacketInboundIntent extends Intent {
private final byte [] data;
public DataPacketInboundIntent(@Nonnull byte [] data) {
this.data = data;
}
@Nonnull
public byte [] getData() {
return data;
}
}

View File

@@ -0,0 +1,20 @@
package com.projectswg.forwarder.intents.client;
import me.joshlarson.jlcommon.control.Intent;
import javax.annotation.Nonnull;
public class DataPacketOutboundIntent extends Intent {
private final byte [] data;
public DataPacketOutboundIntent(@Nonnull byte [] data) {
this.data = data;
}
@Nonnull
public byte [] getData() {
return data;
}
}

View File

@@ -0,0 +1,21 @@
package com.projectswg.forwarder.intents.client;
import com.projectswg.forwarder.resources.networking.packets.Packet;
import me.joshlarson.jlcommon.control.Intent;
import javax.annotation.Nonnull;
public class SonyPacketInboundIntent extends Intent {
private final Packet packet;
public SonyPacketInboundIntent(@Nonnull Packet packet) {
this.packet = packet;
}
@Nonnull
public Packet getPacket() {
return packet;
}
}

View File

@@ -0,0 +1,21 @@
package com.projectswg.forwarder.intents.client;
import com.projectswg.forwarder.resources.networking.data.ProtocolStack;
import me.joshlarson.jlcommon.control.Intent;
import javax.annotation.CheckForNull;
public class UpdateStackIntent extends Intent {
private final ProtocolStack stack;
public UpdateStackIntent(ProtocolStack stack) {
this.stack = stack;
}
@CheckForNull
public ProtocolStack getStack() {
return stack;
}
}

View File

@@ -0,0 +1,24 @@
package com.projectswg.forwarder.intents.control;
import me.joshlarson.jlcommon.control.Intent;
import java.util.zip.ZipOutputStream;
public class ClientCrashedIntent extends Intent {
private final ZipOutputStream outputStream;
private final Object fileMutex;
public ClientCrashedIntent(ZipOutputStream outputStream) {
this.outputStream = outputStream;
this.fileMutex = new Object();
}
public ZipOutputStream getOutputStream() {
return outputStream;
}
public Object getFileMutex() {
return fileMutex;
}
}

View File

@@ -0,0 +1,21 @@
package com.projectswg.forwarder.intents.control;
import com.projectswg.forwarder.Forwarder.ForwarderData;
import me.joshlarson.jlcommon.control.Intent;
public class StartForwarderIntent extends Intent {
private final ForwarderData data;
public StartForwarderIntent(ForwarderData data) {
this.data = data;
}
public ForwarderData getData() {
return data;
}
public static void broadcast(ForwarderData data) {
new StartForwarderIntent(data).broadcast();
}
}

View File

@@ -0,0 +1,11 @@
package com.projectswg.forwarder.intents.control;
import me.joshlarson.jlcommon.control.Intent;
public class StopForwarderIntent extends Intent {
public StopForwarderIntent() {
}
}

View File

@@ -0,0 +1,11 @@
package com.projectswg.forwarder.intents.server;
import me.joshlarson.jlcommon.control.Intent;
public class ServerConnectedIntent extends Intent {
public ServerConnectedIntent() {
}
}

View File

@@ -0,0 +1,11 @@
package com.projectswg.forwarder.intents.server;
import me.joshlarson.jlcommon.control.Intent;
public class ServerDisconnectedIntent extends Intent {
public ServerDisconnectedIntent() {
}
}

View File

@@ -0,0 +1,59 @@
package com.projectswg.forwarder.resources.client.state;
import me.joshlarson.jlcommon.log.Log;
public class OutboundDataTuner {
private long start;
private int maxSend;
private int outOfOrders;
public OutboundDataTuner() {
reset();
}
public int getMaxSend() {
return maxSend;
}
public void markStart() {
reset();
start = System.nanoTime();
}
public void markEnd() {
long time = System.nanoTime() - start;
Log.d("Max Send=%d Time to Zone=%.0fms OOO=%d", maxSend, time/1E6, outOfOrders);
reset();
}
public void markOOO() {
outOfOrders++;
maxSend -= 2;
capMaxSend();
}
public void markAck() {
maxSend += 2;
capMaxSend();
}
public void markLag() {
maxSend -= 50;
capMaxSend();
}
private void reset() {
start = 0;
outOfOrders = 0;
maxSend = 200;
}
private void capMaxSend() {
if (maxSend < 100)
maxSend = 100;
else if (maxSend > 5000)
maxSend = 5000;
}
}

View File

@@ -0,0 +1,6 @@
package com.projectswg.forwarder.resources.networking;
public enum ClientServer {
LOGIN,
ZONE
}

View File

@@ -0,0 +1,68 @@
package com.projectswg.forwarder.resources.networking;
import com.projectswg.common.data.encodables.galaxy.Galaxy;
import com.projectswg.common.network.NetBuffer;
import com.projectswg.common.network.packets.PacketType;
import com.projectswg.common.network.packets.swg.login.LoginClientId;
import com.projectswg.common.network.packets.swg.login.LoginClusterStatus;
import com.projectswg.forwarder.Forwarder.ForwarderData;
import me.joshlarson.jlcommon.log.Log;
import me.joshlarson.jlcommon.utilities.ByteUtilities;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
public class NetInterceptor {
private final ForwarderData data;
public NetInterceptor(ForwarderData data) {
this.data = data;
}
public byte[] interceptClient(byte[] data) {
if (data.length < 6)
return data;
ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
PacketType type = PacketType.fromCrc(bb.getInt(2));
switch (type) {
case LOGIN_CLIENT_ID:
return setAutoLogin(NetBuffer.wrap(bb));
default:
return data;
}
}
public byte[] interceptServer(byte[] data) {
if (data.length < 6)
return data;
ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
PacketType type = PacketType.fromCrc(bb.getInt(2));
switch (type) {
case LOGIN_CLUSTER_STATUS:
return getServerList(NetBuffer.wrap(bb));
default:
return data;
}
}
private byte[] setAutoLogin(NetBuffer data) {
LoginClientId id = new LoginClientId(data);
if (!id.getUsername().equals(this.data.getUsername()) || !id.getPassword().isEmpty())
return data.array();
id.setPassword(this.data.getPassword());
return id.encode().array();
}
private byte[] getServerList(NetBuffer data) {
LoginClusterStatus cluster = new LoginClusterStatus();
cluster.decode(data);
for (Galaxy g : cluster.getGalaxies()) {
g.setAddress("127.0.0.1");
g.setZonePort(this.data.getZonePort());
g.setPingPort(this.data.getZonePort());
}
return cluster.encode().array();
}
}

View File

@@ -25,23 +25,17 @@
* along with Holocore. If not, see <http://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
package com.projectswg.networking;
package com.projectswg.forwarder.resources.networking;
import me.joshlarson.jlcommon.concurrency.Delay;
import me.joshlarson.jlcommon.log.Log;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayDeque;
import java.net.*;
import java.util.Locale;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicBoolean;
import com.projectswg.common.concurrency.Delay;
import com.projectswg.common.debug.Assert;
import com.projectswg.common.debug.Log;
import java.util.function.Consumer;
/**
* This class represents a UDP server that listens for packets and
@@ -49,55 +43,49 @@ import com.projectswg.common.debug.Log;
*/
public class UDPServer {
private final Object waitingForPacket;
private final byte [] dataBuffer;
private final Queue <DatagramPacket> inbound;
private final AtomicBoolean running;
private final InetSocketAddress bindAddr;
private final Consumer<DatagramPacket> callback;
private DatagramSocket socket;
private UDPCallback callback;
private Thread thread;
public UDPServer(InetSocketAddress bindAddr) throws SocketException {
this(bindAddr, 1024);
public UDPServer(@Nonnull InetSocketAddress bindAddr, @Nonnull Consumer<DatagramPacket> callback) {
this(bindAddr, 1024, callback);
}
public UDPServer(InetSocketAddress bindAddr, int packetSize) {
this.waitingForPacket = new Object();
public UDPServer(@Nonnull InetSocketAddress bindAddr, int packetSize, @Nonnull Consumer<DatagramPacket> callback) {
this.dataBuffer = new byte[packetSize];
this.inbound = new ArrayDeque<>();
this.running = new AtomicBoolean(false);
this.bindAddr = bindAddr;
this.callback = null;
this.callback = callback;
}
public void bind() throws SocketException {
Assert.isNull(socket);
bind(null);
}
public void bind(Consumer<DatagramSocket> customizationCallback) throws SocketException {
assert socket == null : "binding twice";
socket = new DatagramSocket(bindAddr);
if (customizationCallback != null)
customizationCallback.accept(socket);
start();
}
public void close() {
Assert.notNull(socket);
assert socket != null : "socket already closed";
stop();
socket.close();
socket = null;
}
public DatagramPacket receive() {
return inbound.poll();
}
public int packetCount() {
return inbound.size();
}
public int getPort() {
int port = socket.getLocalPort();
while (port == 0) {
port = socket.getLocalPort();
if (Delay.sleepMilli(5))
if (!Delay.sleepMilli(5))
break;
}
return port;
@@ -107,20 +95,18 @@ public class UDPServer {
return running.get();
}
public void waitForPacket() {
synchronized (waitingForPacket) {
try {
while (inbound.size() == 0) {
waitingForPacket.wait();
}
} catch (InterruptedException e) {
public boolean send(DatagramPacket packet) {
try {
socket.send(packet);
return true;
} catch (IOException e) {
String msg = e.getMessage();
if (msg == null || !msg.toLowerCase(Locale.US).contains("socket closed")) {
Log.e(e);
close();
}
}
}
public boolean send(DatagramPacket packet) {
return sendRaw(packet);
return false;
}
public boolean send(int port, InetAddress addr, byte [] data) {
@@ -137,37 +123,41 @@ public class UDPServer {
}
public boolean send(InetSocketAddress addr, byte [] data) {
return send(addr.getPort(), addr.getAddress(), data);
}
public void setCallback(UDPCallback callback) {
this.callback = callback;
}
public void removeCallback() {
callback = null;
}
public interface UDPCallback {
public void onReceivedPacket(DatagramPacket packet);
return send(new DatagramPacket(data, data.length, addr));
}
private void start() {
Assert.test(!running.getAndSet(true));
thread = new Thread(() -> run());
running.set(true);
thread = new Thread(this::run);
thread.setName("UDPServer Port#" + getPort());
thread.start();
}
private void stop() {
Assert.test(running.getAndSet(false));
running.set(false);
thread.interrupt();
}
private void run() {
try {
while (running.get()) {
loop();
DatagramPacket packet = new DatagramPacket(dataBuffer, dataBuffer.length);
try {
socket.receive(packet);
if (packet.getLength() > 0) {
byte [] buffer = new byte[packet.getLength()];
System.arraycopy(packet.getData(), 0, buffer, 0, packet.getLength());
packet.setData(buffer);
callback.accept(packet);
}
} catch (IOException e) {
String msg = e.getMessage();
if (msg == null || !msg.toLowerCase(Locale.US).contains("socket closed")) {
Log.e(e);
close();
}
packet.setLength(0);
}
}
} catch (Exception e) {
Log.e(e);
@@ -176,53 +166,4 @@ public class UDPServer {
}
}
private void loop() {
DatagramPacket packet = receiveRaw();
if (packet.getLength() <= 0)
return;
if (callback != null)
callback.onReceivedPacket(packet);
else
inbound.add(packet);
notifyPacketReceived();
}
private void notifyPacketReceived() {
synchronized (waitingForPacket) {
waitingForPacket.notifyAll();
}
}
private boolean sendRaw(DatagramPacket packet) {
try {
socket.send(packet);
return true;
} catch (IOException e) {
String msg = e.getMessage();
if (msg == null || !msg.toLowerCase(Locale.US).contains("socket closed")) {
Log.e(e);
close();
}
}
return false;
}
private DatagramPacket receiveRaw() {
DatagramPacket packet = new DatagramPacket(dataBuffer, dataBuffer.length);
try {
socket.receive(packet);
byte [] buffer = new byte[packet.getLength()];
System.arraycopy(packet.getData(), 0, buffer, 0, packet.getLength());
packet.setData(buffer);
} catch (IOException e) {
String msg = e.getMessage();
if (msg == null || !msg.toLowerCase(Locale.US).contains("socket closed")) {
Log.e(e);
close();
}
packet.setLength(0);
}
return packet;
}
}

View File

@@ -1,17 +1,17 @@
package com.projectswg.networking.client.receiver;
package com.projectswg.forwarder.resources.networking.data;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import com.projectswg.networking.Packet;
import com.projectswg.networking.soe.Fragmented;
import com.projectswg.forwarder.resources.networking.packets.Fragmented;
import com.projectswg.forwarder.resources.networking.packets.Packet;
public class ClientFragmentedProcessor {
public class FragmentedProcessor {
private final List<Fragmented> fragmentedBuffer;
public ClientFragmentedProcessor() {
public FragmentedProcessor() {
this.fragmentedBuffer = new ArrayList<>();
}

View File

@@ -0,0 +1,82 @@
package com.projectswg.forwarder.resources.networking.data;
import com.projectswg.forwarder.resources.networking.packets.DataChannel;
import com.projectswg.forwarder.resources.networking.packets.Fragmented;
import java.nio.ByteBuffer;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicInteger;
public class Packager {
private final AtomicInteger size;
private final DataChannel channel;
private final Queue<byte[]> outboundRaw;
private final Queue<SequencedOutbound> outboundPackaged;
private final ProtocolStack stack;
public Packager(Queue<byte[]> outboundRaw, Queue<SequencedOutbound> outboundPackaged, ProtocolStack stack) {
this.size = new AtomicInteger(8);
this.channel = new DataChannel();
this.outboundRaw = outboundRaw;
this.outboundPackaged = outboundPackaged;
this.stack = stack;
}
public void handle(int maxPackaged) {
byte [] packet;
int packetSize;
while (!outboundRaw.isEmpty() && outboundPackaged.size() < maxPackaged) {
packet = outboundRaw.poll();
if (packet == null)
break;
packetSize = getPacketLength(packet);
if (size.get() + packetSize >= 496) // overflowed previous packet
sendDataChannel();
if (packetSize < 496) {
addToDataChannel(packet, packetSize);
} else {
sendFragmented(packet);
}
}
sendDataChannel();
}
private void addToDataChannel(byte [] packet, int packetSize) {
channel.addPacket(packet);
size.getAndAdd(packetSize);
}
private void sendDataChannel() {
if (channel.getPacketCount() == 0)
return;
channel.setSequence(stack.getAndIncrementTxSequence());
outboundPackaged.add(new SequencedOutbound(channel.getSequence(), channel.encode().array()));
reset();
}
private void sendFragmented(byte [] packet) {
Fragmented[] frags = Fragmented.encode(ByteBuffer.wrap(packet), stack.getTxSequence());
stack.getAndIncrementTxSequence(frags.length);
for (Fragmented frag : frags) {
outboundPackaged.add(new SequencedOutbound(frag.getSequence(), frag.encode().array()));
}
}
private void reset() {
channel.clearPackets();
size.set(8);
}
private static int getPacketLength(byte [] data) {
int len = data.length;
if (len >= 255)
return len + 3;
return len + 1;
}
}

View File

@@ -0,0 +1,169 @@
package com.projectswg.forwarder.resources.networking.data;
import com.projectswg.forwarder.resources.networking.ClientServer;
import com.projectswg.forwarder.resources.networking.packets.Fragmented;
import com.projectswg.forwarder.resources.networking.packets.Packet;
import com.projectswg.forwarder.resources.networking.packets.SequencedPacket;
import javax.annotation.Nonnull;
import java.net.InetSocketAddress;
import java.util.*;
import java.util.function.BiConsumer;
public class ProtocolStack {
private final PriorityQueue<SequencedPacket> sequenced;
private final FragmentedProcessor fragmentedProcessor;
private final InetSocketAddress source;
private final BiConsumer<InetSocketAddress, byte[]> sender;
private final ClientServer server;
private final Queue<byte []> outboundRaw;
private final Queue<SequencedOutbound> outboundPackaged;
private final Packager packager;
private final Object txMutex;
private InetSocketAddress pingSource;
private int connectionId;
private short rxSequence;
private short txSequence;
private boolean txOverflow;
public ProtocolStack(InetSocketAddress source, ClientServer server, BiConsumer<InetSocketAddress, byte[]> sender) {
this.sequenced = new PriorityQueue<>();
this.fragmentedProcessor = new FragmentedProcessor();
this.source = source;
this.sender = sender;
this.server = server;
this.outboundRaw = new LinkedList<>();
this.outboundPackaged = new LinkedList<>();
this.packager = new Packager(outboundRaw, outboundPackaged, this);
this.txMutex = new Object();
this.connectionId = 0;
this.rxSequence = 0;
this.txSequence = 0;
this.txOverflow = false;
}
public void send(Packet packet) {
send(packet.encode().array());
}
public void send(byte [] data) {
sender.accept(source, data);
}
public void sendPing(byte [] data) {
InetSocketAddress pingSource = this.pingSource;
if (pingSource != null)
sender.accept(pingSource, data);
}
public InetSocketAddress getSource() {
return source;
}
public ClientServer getServer() {
return server;
}
public int getConnectionId() {
return connectionId;
}
public short getRxSequence() {
return rxSequence;
}
public short getTxSequence() {
return txSequence;
}
public void setPingSource(InetSocketAddress source) {
this.pingSource = source;
}
public void setConnectionId(int connectionId) {
this.connectionId = connectionId;
}
public short getAndIncrementTxSequence() {
return getAndIncrementTxSequence(1);
}
public short getAndIncrementTxSequence(int amount) {
synchronized (txMutex) {
short prev = this.txSequence;
short next = prev;
next += amount;
if (prev > next)
txOverflow = true;
this.txSequence = next;
return prev;
}
}
public boolean addIncoming(@Nonnull SequencedPacket packet) {
synchronized (sequenced) {
if (packet.getSequence() < rxSequence)
return true;
// If it already exists in here, don't add it again
for (SequencedPacket seq : sequenced) {
if (seq.getSequence() == packet.getSequence())
return true;
}
sequenced.add(packet);
packet = sequenced.peek();
assert packet != null : "the world is on fire";
return packet.getSequence() == rxSequence;
}
}
public SequencedPacket getNextIncoming() {
synchronized (sequenced) {
SequencedPacket peek = sequenced.peek();
if (peek == null || peek.getSequence() != rxSequence)
return null;
rxSequence++;
return sequenced.poll();
}
}
public byte [] addFragmented(Fragmented frag) {
return fragmentedProcessor.addFragmented(frag);
}
public void addOutbound(@Nonnull byte [] data) {
outboundRaw.offer(data);
}
public short getFirstUnacknowledgedOutbound() {
SequencedOutbound out = outboundPackaged.peek();
if (out == null)
return -1;
return out.getSequence();
}
public void clearAcknowledgedOutbound(short sequence) {
synchronized (txMutex) {
if (txOverflow && sequence <= txSequence) {
outboundPackaged.removeIf(out -> out.getSequence() > txSequence);
txOverflow = false;
}
SequencedOutbound out = outboundPackaged.peek();
while (out != null && out.getSequence() <= sequence) {
outboundPackaged.poll();
out = outboundPackaged.peek();
}
}
}
public void fillOutboundPackagedBuffer(int maxPackaged) {
packager.handle(maxPackaged);
}
public Collection<SequencedOutbound> getOutboundPackagedBuffer() {
return Collections.unmodifiableCollection(outboundPackaged);
}
}

View File

@@ -0,0 +1,31 @@
package com.projectswg.forwarder.resources.networking.data;
public class SequencedOutbound {
private final short sequence;
private final byte [] data;
private boolean sent;
public SequencedOutbound(short sequence, byte [] data) {
this.sequence = sequence;
this.data = data;
this.sent = false;
}
public short getSequence() {
return sequence;
}
public byte[] getData() {
return data;
}
public boolean isSent() {
return sent;
}
public void setSent(boolean sent) {
this.sent = sent;
}
}

View File

@@ -1,4 +1,4 @@
package com.projectswg.networking.encryption;
package com.projectswg.forwarder.resources.networking.encryption;
import net.jpountz.lz4.LZ4Compressor;
import net.jpountz.lz4.LZ4Factory;

View File

@@ -25,14 +25,15 @@
* along with Holocore. If not, see <http://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
package com.projectswg.networking.encryption;
package com.projectswg.forwarder.resources.networking.encryption;
import me.joshlarson.jlcommon.log.Log;
import java.nio.ByteBuffer;
import java.util.zip.DataFormatException;
import java.util.zip.Deflater;
import java.util.zip.Inflater;
import com.projectswg.common.debug.Log;
public class Encryption {

View File

@@ -1,31 +1,31 @@
/***********************************************************************************
* Copyright (c) 2015 /// Project SWG /// www.projectswg.com *
* *
* ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on *
* July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. *
* Our goal is to create an emulator which will provide a server for players to *
* continue playing a game similar to the one they used to play. We are basing *
* it on the final publish of the game prior to end-game events. *
* *
* This file is part of Holocore. *
* *
* -------------------------------------------------------------------------------- *
* *
* Holocore is free software: you can redistribute it and/or modify *
* it under the terms of the GNU Affero General Public License as *
* published by the Free Software Foundation, either version 3 of the *
* License, or (at your option) any later version. *
* *
* Holocore is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU Affero General Public License for more details. *
* *
* You should have received a copy of the GNU Affero General Public License *
* along with Holocore. If not, see <http://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
package com.projectswg.networking.encryption;
/***********************************************************************************
* Copyright (c) 2015 /// Project SWG /// www.projectswg.com *
* *
* ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on *
* July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. *
* Our goal is to create an emulator which will provide a server for players to *
* continue playing a game similar to the one they used to play. We are basing *
* it on the final publish of the game prior to end-game events. *
* *
* This file is part of Holocore. *
* *
* -------------------------------------------------------------------------------- *
* *
* Holocore is free software: you can redistribute it and/or modify *
* it under the terms of the GNU Affero General Public License as *
* published by the Free Software Foundation, either version 3 of the *
* License, or (at your option) any later version. *
* *
* Holocore is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU Affero General Public License for more details. *
* *
* You should have received a copy of the GNU Affero General Public License *
* along with Holocore. If not, see <http://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
package com.projectswg.forwarder.resources.networking.encryption;
import java.nio.ByteBuffer;
@@ -84,7 +84,7 @@ class EncryptionCRC {
short gen = generate(data, 0, data.length-2, crcSeed);
short crc = (short)((data[data.length-2] << 8) + (data[data.length-1] & 0xff));
return crc == gen;
} catch (java.lang.ArrayIndexOutOfBoundsException e) {
} catch (ArrayIndexOutOfBoundsException e) {
}
return false;

View File

@@ -25,13 +25,14 @@
* along with Holocore. If not, see <http://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
package com.projectswg.networking.encryption;
package com.projectswg.forwarder.resources.networking.encryption;
import me.joshlarson.jlcommon.log.Log;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import com.projectswg.common.debug.Log;
public class MD5 {
@@ -41,9 +42,7 @@ public class MD5 {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(text.getBytes("iso-8859-1"), 0, text.length());
result = convertToHex(md.digest());
} catch (NoSuchAlgorithmException e) {
Log.e(e);
} catch (UnsupportedEncodingException e) {
} catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
Log.e(e);
}
return result;
@@ -56,4 +55,4 @@ public class MD5 {
}
return result.toString();
}
}
}

View File

@@ -25,13 +25,10 @@
* along with Holocore. If not, see <http://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
package com.projectswg.networking.soe;
package com.projectswg.forwarder.resources.networking.packets;
import java.nio.ByteBuffer;
import com.projectswg.common.debug.Assert;
import com.projectswg.networking.Packet;
public class Acknowledge extends Packet {
private short sequence;
@@ -49,7 +46,7 @@ public class Acknowledge extends Packet {
}
public void decode(ByteBuffer data) {
Assert.test(data.array().length == 4);
assert data.array().length == 4;
data.position(2);
sequence = getNetShort(data);
}

View File

@@ -25,13 +25,10 @@
* along with Holocore. If not, see <http://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
package com.projectswg.networking.soe;
package com.projectswg.forwarder.resources.networking.packets;
import java.nio.ByteBuffer;
import com.projectswg.networking.Packet;
public class ClientNetworkStatusUpdate extends Packet {
private int clientTickCount;

View File

@@ -25,14 +25,13 @@
* along with Holocore. If not, see <http://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
package com.projectswg.networking.soe;
package com.projectswg.forwarder.resources.networking.packets;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import com.projectswg.common.network.NetBuffer;
import com.projectswg.networking.Packet;
public class DataChannel extends Packet implements SequencedPacket {

View File

@@ -25,13 +25,10 @@
* along with Holocore. If not, see <http://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
package com.projectswg.networking.soe;
package com.projectswg.forwarder.resources.networking.packets;
import java.nio.ByteBuffer;
import com.projectswg.networking.Packet;
public class Disconnect extends Packet {
private int connectionId;

View File

@@ -25,13 +25,10 @@
* along with Holocore. If not, see <http://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
package com.projectswg.networking.soe;
package com.projectswg.forwarder.resources.networking.packets;
import java.nio.ByteBuffer;
import com.projectswg.networking.Packet;
public class Fragmented extends Packet implements SequencedPacket {
private short sequence;

View File

@@ -1,9 +1,7 @@
package com.projectswg.networking.soe;
package com.projectswg.forwarder.resources.networking.packets;
import java.nio.ByteBuffer;
import com.projectswg.networking.Packet;
public class KeepAlive extends Packet {
public KeepAlive() {

View File

@@ -25,15 +25,13 @@
* along with Holocore. If not, see <http://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
package com.projectswg.networking.soe;
package com.projectswg.forwarder.resources.networking.packets;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import com.projectswg.common.network.NetBuffer;
import com.projectswg.networking.Packet;
public class MultiPacket extends Packet {

View File

@@ -25,13 +25,10 @@
* along with Holocore. If not, see <http://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
package com.projectswg.networking.soe;
package com.projectswg.forwarder.resources.networking.packets;
import java.nio.ByteBuffer;
import com.projectswg.networking.Packet;
public class OutOfOrder extends Packet {
private short sequence;

View File

@@ -25,7 +25,7 @@
* along with Holocore. If not, see <http://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
package com.projectswg.networking;
package com.projectswg.forwarder.resources.networking.packets;
import java.net.InetAddress;
import java.nio.ByteBuffer;

View File

@@ -0,0 +1,27 @@
package com.projectswg.forwarder.resources.networking.packets;
import java.nio.ByteBuffer;
public class PingPacket extends Packet {
private byte [] payload;
public PingPacket(byte [] payload) {
this.payload = payload;
}
@Override
public void decode(ByteBuffer data) {
this.payload = data.array();
}
@Override
public ByteBuffer encode() {
return ByteBuffer.wrap(payload);
}
public byte [] getPayload() {
return payload;
}
}

View File

@@ -25,72 +25,17 @@
* along with Holocore. If not, see <http://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
package com.projectswg.utilities;
package com.projectswg.forwarder.resources.networking.packets;
import java.nio.ByteBuffer;
public class ByteUtilities {
public class RawSWGPacket extends Packet {
private static final ByteBuffer buffer = ByteBuffer.allocate(Long.SIZE);
private static final char [] HEX = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
private byte [] data;
public static String getHexString(byte [] bytes) {
char [] data = new char[bytes.length*2+(bytes.length>0?bytes.length-1:0)];
byte b;
for (int i = 0; i < bytes.length; i++) {
b = bytes[i];
data[i*3+0] = HEX[(b&0xFF) >>> 4];
data[i*3+1] = HEX[b & 0x0F];
if (i*3+2 < data.length)
data[i*3+2] = ' ';
}
return new String(data);
public RawSWGPacket(byte [] data) {
this.data = data;
}
public static byte [] getHexStringArray(String string) {
int len = string.length();
if (len % 2 != 0)
return new byte[0];
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(string.charAt(i), 16) << 4) + Character.digit(string.charAt(i+1), 16));
}
public byte[] getRawData() {
return data;
}
public static byte [] longToBytes(long l) {
byte [] b = new byte[Long.SIZE];
synchronized (buffer) {
buffer.putLong(0, l);
System.arraycopy(buffer.array(), 0, b, 0, Long.SIZE);
}
return b;
}
public static long bytesToLong(byte [] a) {
return bytesToLong(a, 0);
}
public static long bytesToLong(byte [] a, int offset) {
long l = 0;
synchronized (buffer) {
for (int i = 0; i < Long.SIZE; i++) {
if (i < a.length)
buffer.put(i, a[i+offset]);
else
buffer.put(i, (byte)0);
}
l = buffer.getLong(0);
}
return l;
}
public static String nextString(ByteBuffer data) {
byte [] bData = data.array();
StringBuilder str = new StringBuilder();
for (int i = data.position(); i < bData.length && bData[i] >= ' ' && bData[i] <= '~'; i++)
str.append((char) data.get());
return str.toString();
}
}

View File

@@ -1,4 +1,4 @@
package com.projectswg.networking.soe;
package com.projectswg.forwarder.resources.networking.packets;
public interface SequencedPacket extends Comparable<SequencedPacket> {
short getSequence();

View File

@@ -1,9 +1,7 @@
package com.projectswg.networking.soe;
package com.projectswg.forwarder.resources.networking.packets;
import java.nio.ByteBuffer;
import com.projectswg.networking.Packet;
public class SeriousErrorAcknowledge extends Packet {
public SeriousErrorAcknowledge() {

View File

@@ -1,9 +1,7 @@
package com.projectswg.networking.soe;
package com.projectswg.forwarder.resources.networking.packets;
import java.nio.ByteBuffer;
import com.projectswg.networking.Packet;
public class SeriousErrorAcknowledgeReply extends Packet {
public SeriousErrorAcknowledgeReply() {

View File

@@ -25,12 +25,10 @@
* along with Holocore. If not, see <http://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
package com.projectswg.networking.soe;
package com.projectswg.forwarder.resources.networking.packets;
import java.nio.ByteBuffer;
import com.projectswg.networking.Packet;
public class ServerNetworkStatusUpdate extends Packet {

View File

@@ -25,13 +25,11 @@
* along with Holocore. If not, see <http://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
package com.projectswg.networking.soe;
package com.projectswg.forwarder.resources.networking.packets;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import com.projectswg.networking.Packet;
public class SessionRequest extends Packet {

View File

@@ -25,14 +25,11 @@
* along with Holocore. If not, see <http://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
package com.projectswg.networking.soe;
package com.projectswg.forwarder.resources.networking.packets;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import com.projectswg.networking.Packet;
public class SessionResponse extends Packet {
private int connectionId;

View File

@@ -1,12 +1,6 @@
package com.projectswg.recording;
package com.projectswg.forwarder.resources.recording;
import java.io.Closeable;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.*;
import java.lang.management.ManagementFactory;
import java.lang.management.OperatingSystemMXBean;
import java.net.InetSocketAddress;
@@ -15,18 +9,14 @@ import java.util.Map.Entry;
import java.util.TimeZone;
import java.util.TreeMap;
import com.projectswg.common.debug.Assert;
public class PacketRecorder implements AutoCloseable, Closeable {
private static final byte VERSION = 1;
private static final byte VERSION = 2;
private final DataOutputStream dataOut;
private final OutputStream out;
public PacketRecorder(File file) throws FileNotFoundException {
out = new FileOutputStream(file);
dataOut = new DataOutputStream(out);
dataOut = new DataOutputStream(new FileOutputStream(file));
writeHeader();
}
@@ -34,13 +24,11 @@ public class PacketRecorder implements AutoCloseable, Closeable {
dataOut.close();
}
public void record(boolean server, InetSocketAddress source, InetSocketAddress destination, byte [] data) {
public void record(boolean server, byte [] data) {
synchronized (dataOut) {
try {
dataOut.writeByte(server?1:0);
dataOut.writeLong(System.currentTimeMillis());
recordSocketAddress(source);
recordSocketAddress(destination);
dataOut.writeShort(data.length);
dataOut.write(data);
} catch (IOException e) {
@@ -49,15 +37,6 @@ public class PacketRecorder implements AutoCloseable, Closeable {
}
}
private void recordSocketAddress(InetSocketAddress addr) throws IOException {
Assert.notNull(addr);
Assert.test(!addr.isUnresolved());
byte [] raw = addr.getAddress().getAddress();
dataOut.writeByte(raw.length);
dataOut.write(raw);
dataOut.writeShort(addr.getPort());
}
private void writeHeader() {
try {
dataOut.writeByte(VERSION);

View File

@@ -0,0 +1,14 @@
package com.projectswg.forwarder.services.client;
import me.joshlarson.jlcommon.control.Manager;
import me.joshlarson.jlcommon.control.ManagerStructure;
@ManagerStructure(children = {
ClientInboundDataService.class,
ClientOutboundDataService.class,
ClientProtocolService.class,
ClientServerService.class
})
public class ClientConnectionManager extends Manager {
}

View File

@@ -0,0 +1,105 @@
package com.projectswg.forwarder.services.client;
import com.projectswg.common.network.packets.PacketType;
import com.projectswg.common.network.packets.swg.zone.HeartBeat;
import com.projectswg.forwarder.intents.client.DataPacketInboundIntent;
import com.projectswg.forwarder.intents.client.SonyPacketInboundIntent;
import com.projectswg.forwarder.intents.client.UpdateStackIntent;
import com.projectswg.forwarder.resources.networking.data.ProtocolStack;
import com.projectswg.forwarder.resources.networking.packets.*;
import me.joshlarson.jlcommon.control.IntentChain;
import me.joshlarson.jlcommon.control.IntentHandler;
import me.joshlarson.jlcommon.control.IntentMultiplexer;
import me.joshlarson.jlcommon.control.IntentMultiplexer.Multiplexer;
import me.joshlarson.jlcommon.control.Service;
import me.joshlarson.jlcommon.log.Log;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.concurrent.atomic.AtomicReference;
public class ClientInboundDataService extends Service {
private final IntentMultiplexer multiplexer;
private final AtomicReference<ProtocolStack> stack;
private final IntentChain intentChain;
public ClientInboundDataService() {
this.multiplexer = new IntentMultiplexer(this, ProtocolStack.class, Packet.class);
this.stack = new AtomicReference<>(null);
this.intentChain = new IntentChain();
}
@IntentHandler
private void handleSonyPacketInboundIntent(SonyPacketInboundIntent spii) {
ProtocolStack stack = this.stack.get();
assert stack != null : "stack is null";
multiplexer.call(stack, spii.getPacket());
}
@IntentHandler
private void handleUpdateStackIntent(UpdateStackIntent sci) {
stack.set(sci.getStack());
}
@Multiplexer
private void handlePingPacket(ProtocolStack stack, PingPacket ping) {
onData(new HeartBeat(ping.getPayload()).encode().array());
}
@Multiplexer
private void handleRawSwgPacket(ProtocolStack stack, RawSWGPacket data) {
onData(data.getRawData());
}
@Multiplexer
private void handleDataChannel(ProtocolStack stack, DataChannel data) {
if (stack.addIncoming(data)) {
readAvailablePackets(stack);
} else {
Log.d("Inbound Out of Order %d (data)", data.getSequence());
for (short seq = stack.getRxSequence(); seq < data.getSequence(); seq++)
stack.send(new OutOfOrder(seq));
}
}
@Multiplexer
private void handleFragmented(ProtocolStack stack, Fragmented frag) {
if (stack.addIncoming(frag)) {
readAvailablePackets(stack);
} else {
Log.d("Inbound Out of Order %d (frag)", frag.getSequence());
for (short seq = stack.getRxSequence(); seq < frag.getSequence(); seq++)
stack.send(new OutOfOrder(seq));
}
}
private void readAvailablePackets(ProtocolStack stack) {
short highestSequence = -1;
SequencedPacket packet = stack.getNextIncoming();
while (packet != null) {
if (packet instanceof DataChannel) {
for (byte [] data : ((DataChannel) packet).getPackets())
onData(data);
} else if (packet instanceof Fragmented) {
byte [] data = stack.addFragmented((Fragmented) packet);
if (data != null)
onData(data);
}
Log.t("Data Inbound: %s", packet);
highestSequence = packet.getSequence();
packet = stack.getNextIncoming();
}
if (highestSequence != -1) {
Log.t("Inbound Acknowledge %d", highestSequence);
stack.send(new Acknowledge(highestSequence));
}
}
private void onData(byte [] data) {
PacketType type = PacketType.fromCrc(ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN).getInt(2));
Log.d("Incoming Data: %s", type);
intentChain.broadcastAfter(getIntentManager(), new DataPacketInboundIntent(data));
}
}

View File

@@ -0,0 +1,178 @@
package com.projectswg.forwarder.services.client;
import com.projectswg.common.network.NetBuffer;
import com.projectswg.common.network.packets.PacketType;
import com.projectswg.common.network.packets.swg.zone.HeartBeat;
import com.projectswg.forwarder.intents.client.DataPacketInboundIntent;
import com.projectswg.forwarder.intents.client.DataPacketOutboundIntent;
import com.projectswg.forwarder.intents.client.SonyPacketInboundIntent;
import com.projectswg.forwarder.intents.client.ClientConnectedIntent;
import com.projectswg.forwarder.intents.client.ClientDisconnectedIntent;
import com.projectswg.forwarder.intents.client.UpdateStackIntent;
import com.projectswg.forwarder.resources.client.state.OutboundDataTuner;
import com.projectswg.forwarder.resources.networking.data.ProtocolStack;
import com.projectswg.forwarder.resources.networking.data.SequencedOutbound;
import com.projectswg.forwarder.resources.networking.packets.Acknowledge;
import com.projectswg.forwarder.resources.networking.packets.OutOfOrder;
import com.projectswg.forwarder.resources.networking.packets.Packet;
import me.joshlarson.jlcommon.concurrency.Delay;
import me.joshlarson.jlcommon.concurrency.ScheduledThreadPool;
import me.joshlarson.jlcommon.control.IntentHandler;
import me.joshlarson.jlcommon.control.IntentMultiplexer;
import me.joshlarson.jlcommon.control.IntentMultiplexer.Multiplexer;
import me.joshlarson.jlcommon.control.Service;
import me.joshlarson.jlcommon.log.Log;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.concurrent.atomic.AtomicReference;
public class ClientOutboundDataService extends Service {
private static final int TIMER_DELAY = 20;
private final IntentMultiplexer multiplexer;
private final AtomicReference<ProtocolStack> stack;
private final ScheduledThreadPool timerThread;
private final OutboundDataTuner tuner;
private final Object outboundMutex;
public ClientOutboundDataService() {
this.multiplexer = new IntentMultiplexer(this, ProtocolStack.class, Packet.class);
this.stack = new AtomicReference<>(null);
this.timerThread = new ScheduledThreadPool(2, 5, "outbound-sender-%d");
this.tuner = new OutboundDataTuner();
this.outboundMutex = new Object();
}
@IntentHandler
private void handleSonyPacketInboundIntent(SonyPacketInboundIntent spii) {
ProtocolStack stack = this.stack.get();
assert stack != null : "stack is null";
multiplexer.call(stack, spii.getPacket());
}
@IntentHandler
private void handleClientConnectedIntent(ClientConnectedIntent cci) {
if (timerThread.isRunning())
return;
timerThread.start();
timerThread.executeWithFixedDelay(TIMER_DELAY, TIMER_DELAY, this::timerCallback);
timerThread.executeWithFixedRate(0, 5000, this::clearSentBit);
}
@IntentHandler
private void handleClientDisconnectedIntent(ClientDisconnectedIntent cdi) {
if (!timerThread.isRunning())
return;
timerThread.stop();
timerThread.awaitTermination(500);
}
@IntentHandler
private void handleUpdateStackIntent(UpdateStackIntent sci) {
stack.set(sci.getStack());
}
@IntentHandler
private void handleDataPacketInboundIntent(DataPacketInboundIntent dpii) {
ProtocolStack stack = this.stack.get();
if (stack == null)
return;
PacketType type = PacketType.fromCrc(ByteBuffer.wrap(dpii.getData()).order(ByteOrder.LITTLE_ENDIAN).getInt(2));
switch (type) {
case LAG_REQUEST:
tuner.markLag();
break;
}
}
@IntentHandler
private void handleDataPacketOutboundIntent(DataPacketOutboundIntent dpoi) {
ProtocolStack stack = this.stack.get();
if (stack == null)
return;
PacketType type = PacketType.fromCrc(ByteBuffer.wrap(dpoi.getData()).order(ByteOrder.LITTLE_ENDIAN).getInt(2));
switch (type) {
case CMD_START_SCENE:
tuner.markStart();
break;
case CMD_SCENE_READY:
tuner.markEnd();
break;
case HEART_BEAT_MESSAGE: {
HeartBeat heartbeat = new HeartBeat();
heartbeat.decode(NetBuffer.wrap(dpoi.getData()));
if (heartbeat.getPayload().length > 0) {
stack.sendPing(heartbeat.getPayload());
return;
}
break;
}
}
synchronized (outboundMutex) {
stack.addOutbound(dpoi.getData());
}
}
@Multiplexer
private void handleAcknowledgement(ProtocolStack stack, Acknowledge ack) {
Log.t("Acknowledged: %d. Min Sequence: %d", ack.getSequence(), stack.getFirstUnacknowledgedOutbound());
tuner.markAck();
synchronized (outboundMutex) {
stack.clearAcknowledgedOutbound(ack.getSequence());
for (SequencedOutbound outbound : stack.getOutboundPackagedBuffer()) {
outbound.setSent(false);
}
}
}
@Multiplexer
private void handleOutOfOrder(ProtocolStack stack, OutOfOrder ooo) {
tuner.markOOO();
synchronized (outboundMutex) {
for (SequencedOutbound outbound : stack.getOutboundPackagedBuffer()) {
if (outbound.getSequence() > ooo.getSequence())
break;
outbound.setSent(false);
}
}
}
private void clearSentBit() {
ProtocolStack stack = this.stack.get();
if (stack == null)
return;
synchronized (outboundMutex) {
for (SequencedOutbound outbound : stack.getOutboundPackagedBuffer()) {
outbound.setSent(false);
}
}
}
private void timerCallback() {
ProtocolStack stack = this.stack.get();
if (stack == null)
return;
int maxSend = tuner.getMaxSend();
synchronized (outboundMutex) {
stack.fillOutboundPackagedBuffer(maxSend);
int sent = 0;
int runStart = Integer.MIN_VALUE;
int runEnd = 0;
for (SequencedOutbound outbound : stack.getOutboundPackagedBuffer()) {
runEnd = outbound.getSequence();
if (runStart == Integer.MIN_VALUE) {
runStart = runEnd;
}
stack.send(outbound.getData());
Delay.sleepMicro(50);
if (sent++ >= maxSend)
break;
}
if (runStart != Integer.MIN_VALUE)
Log.t("Sending to %s: %d - %d", stack.getSource(), runStart, runEnd);
}
}
}

View File

@@ -0,0 +1,58 @@
package com.projectswg.forwarder.services.client;
import com.projectswg.forwarder.intents.client.SonyPacketInboundIntent;
import com.projectswg.forwarder.intents.client.UpdateStackIntent;
import com.projectswg.forwarder.resources.networking.data.ProtocolStack;
import com.projectswg.forwarder.resources.networking.packets.ClientNetworkStatusUpdate;
import com.projectswg.forwarder.resources.networking.packets.KeepAlive;
import com.projectswg.forwarder.resources.networking.packets.Packet;
import com.projectswg.forwarder.resources.networking.packets.ServerNetworkStatusUpdate;
import me.joshlarson.jlcommon.control.IntentHandler;
import me.joshlarson.jlcommon.control.IntentMultiplexer;
import me.joshlarson.jlcommon.control.IntentMultiplexer.Multiplexer;
import me.joshlarson.jlcommon.control.Service;
import java.util.concurrent.atomic.AtomicReference;
public class ClientProtocolService extends Service {
private static final int GALACTIC_BASE_TIME = 1323043200;
private final AtomicReference<ProtocolStack> stack;
private final IntentMultiplexer multiplexer;
public ClientProtocolService() {
this.stack = new AtomicReference<>(null);
this.multiplexer = new IntentMultiplexer(this, ProtocolStack.class, Packet.class);
}
@IntentHandler
private void handleSonyPacketInboundIntent(SonyPacketInboundIntent spii) {
ProtocolStack stack = this.stack.get();
assert stack != null : "stack is null";
multiplexer.call(stack, spii.getPacket());
}
@IntentHandler
private void handleUpdateStackIntent(UpdateStackIntent sci) {
stack.set(sci.getStack());
}
@Multiplexer
private void handleClientNetworkStatus(ProtocolStack stack, ClientNetworkStatusUpdate update) {
ServerNetworkStatusUpdate serverNet = new ServerNetworkStatusUpdate();
serverNet.setClientTickCount((short) update.getTick());
serverNet.setServerSyncStampLong((int) (System.currentTimeMillis()-GALACTIC_BASE_TIME));
serverNet.setClientPacketsSent(update.getSent());
serverNet.setClientPacketsRecv(update.getRecv());
serverNet.setServerPacketsSent(stack.getTxSequence());
serverNet.setServerPacketsRecv(stack.getRxSequence());
stack.send(serverNet);
}
@Multiplexer
private void handleKeepAlive(ProtocolStack stack, KeepAlive keepAlive) {
stack.send(new KeepAlive());
}
}

View File

@@ -0,0 +1,212 @@
package com.projectswg.forwarder.services.client;
import com.projectswg.forwarder.Forwarder.ForwarderData;
import com.projectswg.forwarder.intents.client.SonyPacketInboundIntent;
import com.projectswg.forwarder.intents.client.ClientConnectedIntent;
import com.projectswg.forwarder.intents.client.ClientDisconnectedIntent;
import com.projectswg.forwarder.intents.client.UpdateStackIntent;
import com.projectswg.forwarder.intents.control.StartForwarderIntent;
import com.projectswg.forwarder.intents.control.StopForwarderIntent;
import com.projectswg.forwarder.resources.networking.ClientServer;
import com.projectswg.forwarder.resources.networking.UDPServer;
import com.projectswg.forwarder.resources.networking.data.ProtocolStack;
import com.projectswg.forwarder.resources.networking.packets.*;
import com.projectswg.forwarder.resources.networking.packets.Disconnect.DisconnectReason;
import me.joshlarson.jlcommon.control.IntentChain;
import me.joshlarson.jlcommon.control.IntentHandler;
import me.joshlarson.jlcommon.control.Service;
import me.joshlarson.jlcommon.log.Log;
import me.joshlarson.jlcommon.utilities.ByteUtilities;
import java.net.*;
import java.nio.ByteBuffer;
import java.util.concurrent.atomic.AtomicReference;
public class ClientServerService extends Service {
private final IntentChain intentChain;
private final AtomicReference<ProtocolStack> stack;
private ForwarderData data;
private UDPServer loginServer;
private UDPServer zoneServer;
public ClientServerService() {
this.intentChain = new IntentChain();
this.stack = new AtomicReference<>(null);
this.data = null;
this.loginServer = null;
this.zoneServer = null;
}
@Override
public boolean isOperational() {
return data == null || (loginServer != null && zoneServer != null && loginServer.isRunning() && zoneServer.isRunning());
}
@Override
public boolean stop() {
setStack(null);
return true;
}
@IntentHandler
private void handleStartForwarderIntent(StartForwarderIntent sfi) {
try {
ForwarderData data = sfi.getData();
Log.t("Initializing login udp server...");
loginServer = new UDPServer(new InetSocketAddress(InetAddress.getLoopbackAddress(), data.getLoginPort()), 496, this::onLoginPacket);
Log.t("Initializing zone udp server...");
zoneServer = new UDPServer(new InetSocketAddress(InetAddress.getLoopbackAddress(), data.getZonePort()), 496, this::onZonePacket);
Log.t("Binding to login server...");
loginServer.bind(this::customizeUdpServer);
Log.t("Binding to zone server...");
zoneServer.bind(this::customizeUdpServer);
data.setLoginPort(loginServer.getPort());
data.setZonePort(zoneServer.getPort());
Log.i("Initialized login (%d) and zone servers (%d)", loginServer.getPort(), zoneServer.getPort());
} catch (SocketException e) {
Log.a(e);
if (loginServer != null)
loginServer.close();
if (zoneServer != null)
zoneServer.close();
loginServer = null;
zoneServer = null;
}
data = sfi.getData();
}
@IntentHandler
private void handleStopForwarderIntent(StopForwarderIntent sfi) {
Log.t("Closing the login udp server...");
if (loginServer != null)
loginServer.close();
Log.t("Closing the zone udp server...");
if (zoneServer != null)
zoneServer.close();
loginServer = null;
zoneServer = null;
Log.i("Closed the login and zone udp servers");
}
private void customizeUdpServer(DatagramSocket socket) {
try {
socket.setReuseAddress(false);
socket.setTrafficClass(0x02 | 0x04 | 0x08 | 0x10);
socket.setBroadcast(false);
socket.setReceiveBufferSize(496 * 2048);
socket.setSendBufferSize(496 * 2048);
} catch (SocketException e) {
Log.w(e);
}
}
private void onLoginPacket(DatagramPacket packet) {
process((InetSocketAddress) packet.getSocketAddress(), ClientServer.LOGIN, packet.getData());
}
private void onZonePacket(DatagramPacket packet) {
process((InetSocketAddress) packet.getSocketAddress(), ClientServer.ZONE, packet.getData());
}
private void send(InetSocketAddress addr, ClientServer server, byte [] data) {
switch (server) {
case LOGIN:
loginServer.send(addr, data);
break;
case ZONE:
zoneServer.send(addr, data);
break;
}
}
private void process(InetSocketAddress source, ClientServer server, byte [] data) {
Packet parsed = parse(data);
if (parsed == null)
return;
if (parsed instanceof MultiPacket) {
for (byte [] child : ((MultiPacket) parsed).getPackets()) {
process(source, server, child);
}
} else {
broadcast(source, server, parsed);
}
}
private void broadcast(InetSocketAddress source, ClientServer server, Packet parsed) {
ProtocolStack stack = this.stack.get();
if (parsed instanceof SessionRequest) {
stack = new ProtocolStack(source, server, (remote, data) -> send(remote, server, data));
stack.setConnectionId(((SessionRequest) parsed).getConnectionId());
setStack(stack);
stack.send(new SessionResponse(((SessionRequest) parsed).getConnectionId(), 0, (byte) 0, (byte) 0, (byte) 0, 496));
}
if (stack != null && parsed instanceof PingPacket) {
stack.setPingSource(source);
} else if (stack == null || !stack.getSource().equals(source) || stack.getServer() != server) {
Log.t("[%s]@%s DROPPED %s", source, server, parsed);
return;
}
Log.t("[%s]@%s sent: %s", source, server, parsed);
intentChain.broadcastAfter(getIntentManager(), new SonyPacketInboundIntent(parsed));
if (parsed instanceof Disconnect) {
setStack(null);
}
}
private void setStack(ProtocolStack stack) {
ProtocolStack oldStack = this.stack.getAndSet(stack);
if (oldStack != null) {
oldStack.send(new Disconnect(oldStack.getConnectionId(), DisconnectReason.MANAGER_DELETED));
if (stack == null)
intentChain.broadcastAfter(getIntentManager(), new ClientDisconnectedIntent());
}
if (stack != null && stack.getServer() == ClientServer.LOGIN) {
intentChain.broadcastAfter(getIntentManager(), new ClientConnectedIntent());
}
intentChain.broadcastAfter(getIntentManager(), new UpdateStackIntent(stack));
}
private static Packet parse(byte [] rawData) {
if (rawData.length < 4)
return null;
ByteBuffer data = ByteBuffer.wrap(rawData);
short opcode = data.getShort(0);
switch (opcode) {
case 0x01: return new SessionRequest(data);
case 0x03: return new MultiPacket(data);
case 0x05: return new Disconnect(data);
case 0x06: return new KeepAlive(data);
case 0x07: return new ClientNetworkStatusUpdate(data);
case 0x09:
case 0x0A:
case 0x0B:
case 0x0C: return new DataChannel(data);
case 0x0D:
case 0x0E:
case 0x0F:
case 0x10: return new Fragmented(data);
case 0x11:
case 0x12:
case 0x13:
case 0x14: return new OutOfOrder(data);
case 0x15:
case 0x16:
case 0x17:
case 0x18: return new Acknowledge(data);
default:
if (rawData.length == 4)
return new PingPacket(rawData);
if (rawData.length >= 6)
return new RawSWGPacket(rawData);
Log.w("Unknown SOE packet: %d %s", opcode, ByteUtilities.getHexString(data.array()));
return null;
}
}
}

View File

@@ -0,0 +1,12 @@
package com.projectswg.forwarder.services.crash;
import me.joshlarson.jlcommon.control.Manager;
import me.joshlarson.jlcommon.control.ManagerStructure;
@ManagerStructure(children = {
PacketRecordingService.class,
IntentRecordingService.class
})
public class CrashManager extends Manager {
}

View File

@@ -0,0 +1,149 @@
package com.projectswg.forwarder.services.crash;
import com.projectswg.forwarder.Forwarder.ForwarderData;
import com.projectswg.forwarder.intents.client.ClientConnectedIntent;
import com.projectswg.forwarder.intents.client.ClientDisconnectedIntent;
import com.projectswg.forwarder.intents.client.UpdateStackIntent;
import com.projectswg.forwarder.intents.control.ClientCrashedIntent;
import com.projectswg.forwarder.intents.control.StartForwarderIntent;
import com.projectswg.forwarder.intents.control.StopForwarderIntent;
import com.projectswg.forwarder.intents.server.ServerConnectedIntent;
import com.projectswg.forwarder.intents.server.ServerDisconnectedIntent;
import com.projectswg.forwarder.resources.networking.data.ProtocolStack;
import me.joshlarson.jlcommon.control.Intent;
import me.joshlarson.jlcommon.control.IntentHandler;
import me.joshlarson.jlcommon.control.Service;
import me.joshlarson.jlcommon.log.Log;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public class IntentRecordingService extends Service {
private final DateTimeFormatter dateTimeFormatter;
private Path logPath;
private FileWriter logWriter;
private ForwarderData data;
public IntentRecordingService() {
this.dateTimeFormatter = DateTimeFormatter.ofPattern("dd-MM-yy HH:mm:ss.SSS zzz").withZone(ZoneId.systemDefault());
this.logPath = null;
this.logWriter = null;
this.data = null;
}
@Override
public boolean initialize() {
try {
logPath = Files.createTempFile("HolocoreIntents", ".txt");
logWriter = new FileWriter(logPath.toFile());
} catch (IOException e) {
Log.a(e);
return false;
}
return true;
}
@Override
public boolean terminate() {
try {
if (logWriter != null)
logWriter.close();
if (logPath != null)
return logPath.toFile().delete();
} catch (IOException e) {
Log.w(e);
return false;
}
return true;
}
@IntentHandler
private void handleClientConnectedIntent(ClientConnectedIntent cci) {
ForwarderData data = this.data;
if (data == null)
log(cci, "");
else
log(cci, "Address='%s' Username='%s' Login='%d' Zone='%d'", data.getAddress(), data.getUsername(), data.getLoginPort(), data.getZonePort());
}
@IntentHandler
private void handleClientDisconnectedIntent(ClientDisconnectedIntent cdi) {
log(cdi, "");
}
@IntentHandler
private void handleUpdateStackIntent(UpdateStackIntent usi) {
ProtocolStack stack = usi.getStack();
if (stack == null)
log(usi, "Stack='null'");
else
log(usi, "Server='%s' Source='%s' ConnectionId='%d'", stack.getServer(), stack.getSource(), stack.getConnectionId());
}
@IntentHandler
private void handleStartForwarderIntent(StartForwarderIntent sfi) {
this.data = sfi.getData();
}
@IntentHandler
private void handleStopForwarderIntent(StopForwarderIntent sfi) {
ForwarderData data = this.data;
if (data == null)
log(sfi, "");
else
log(sfi, "Address='%s' Username='%s' Login='%d' Zone='%d'", data.getAddress(), data.getUsername(), data.getLoginPort(), data.getZonePort());
}
@IntentHandler
private void handleServerConnectedIntent(ServerConnectedIntent sci) {
ForwarderData data = this.data;
if (data == null)
log(sci, "");
else
log(sci, "Address='%s' Username='%s' Login='%d' Zone='%d'", data.getAddress(), data.getUsername(), data.getLoginPort(), data.getZonePort());
}
@IntentHandler
private void handleServerDisconnectedIntent(ServerDisconnectedIntent sdi) {
log(sdi, "");
}
@IntentHandler
private void handleClientCrashedIntent(ClientCrashedIntent cci) {
log(cci, "");
try {
logWriter.flush();
byte[] data = Files.readAllBytes(logPath);
ZipEntry entry = new ZipEntry("log.txt");
entry.setTime(System.currentTimeMillis());
entry.setSize(data.length);
entry.setMethod(ZipOutputStream.DEFLATED);
synchronized (cci.getFileMutex()) {
cci.getOutputStream().putNextEntry(entry);
cci.getOutputStream().write(data);
cci.getOutputStream().closeEntry();
}
} catch (IOException e) {
Log.w("Failed to write intent data to crash log - IOException");
Log.w(e);
}
}
private synchronized void log(Intent i, String message, Object ... args) {
try {
logWriter.write(dateTimeFormatter.format(Instant.now()) + ": " + i.getClass().getSimpleName() + ' ' + String.format(message, args) + '\r' + '\n');
} catch (IOException e) {
Log.e("Failed to write to intent log");
}
}
}

View File

@@ -0,0 +1,79 @@
package com.projectswg.forwarder.services.crash;
import com.projectswg.forwarder.intents.client.DataPacketInboundIntent;
import com.projectswg.forwarder.intents.client.DataPacketOutboundIntent;
import com.projectswg.forwarder.intents.control.ClientCrashedIntent;
import com.projectswg.forwarder.resources.recording.PacketRecorder;
import me.joshlarson.jlcommon.control.IntentHandler;
import me.joshlarson.jlcommon.control.Service;
import me.joshlarson.jlcommon.log.Log;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public class PacketRecordingService extends Service {
private Path recorderPath;
private PacketRecorder recorder;
public PacketRecordingService() {
this.recorder = null;
}
@Override
public boolean initialize() {
try {
recorderPath = Files.createTempFile("HolocorePackets", ".hcap");
recorder = new PacketRecorder(recorderPath.toFile());
} catch (IOException e) {
Log.a(e);
return false;
}
return true;
}
@Override
public boolean terminate() {
try {
if (recorder != null)
recorder.close();
return recorderPath.toFile().delete();
} catch (IOException e) {
Log.w(e);
return false;
}
}
@IntentHandler
private void handleClientCrashedIntent(ClientCrashedIntent cci) {
try {
byte[] data = Files.readAllBytes(recorderPath);
ZipEntry entry = new ZipEntry("packet_log.hcap");
entry.setTime(System.currentTimeMillis());
entry.setSize(data.length);
entry.setMethod(ZipOutputStream.DEFLATED);
synchronized (cci.getFileMutex()) {
cci.getOutputStream().putNextEntry(entry);
cci.getOutputStream().write(data);
cci.getOutputStream().closeEntry();
}
} catch (IOException e) {
Log.w("Failed to write packet data to crash log - IOException");
Log.w(e);
}
}
@IntentHandler
private void handleDataPacketInboundIntent(DataPacketInboundIntent dpii) {
recorder.record(false, dpii.getData());
}
@IntentHandler
private void handleDataPacketOutboundIntent(DataPacketOutboundIntent dpoi) {
recorder.record(true, dpoi.getData());
}
}

View File

@@ -0,0 +1,11 @@
package com.projectswg.forwarder.services.server;
import me.joshlarson.jlcommon.control.Manager;
import me.joshlarson.jlcommon.control.ManagerStructure;
@ManagerStructure(children = {
ServerConnectionService.class
})
public class ServerConnectionManager extends Manager {
}

View File

@@ -0,0 +1,129 @@
package com.projectswg.forwarder.services.server;
import com.projectswg.connection.HolocoreSocket;
import com.projectswg.connection.ServerConnectionChangedReason;
import com.projectswg.connection.packets.RawPacket;
import com.projectswg.forwarder.Forwarder.ForwarderData;
import com.projectswg.forwarder.intents.client.DataPacketInboundIntent;
import com.projectswg.forwarder.intents.client.DataPacketOutboundIntent;
import com.projectswg.forwarder.intents.client.ClientConnectedIntent;
import com.projectswg.forwarder.intents.client.ClientDisconnectedIntent;
import com.projectswg.forwarder.intents.control.StartForwarderIntent;
import com.projectswg.forwarder.intents.control.StopForwarderIntent;
import com.projectswg.forwarder.intents.server.ServerConnectedIntent;
import com.projectswg.forwarder.intents.server.ServerDisconnectedIntent;
import com.projectswg.forwarder.resources.networking.NetInterceptor;
import me.joshlarson.jlcommon.concurrency.BasicThread;
import me.joshlarson.jlcommon.concurrency.Delay;
import me.joshlarson.jlcommon.control.IntentChain;
import me.joshlarson.jlcommon.control.IntentHandler;
import me.joshlarson.jlcommon.control.Service;
import me.joshlarson.jlcommon.log.Log;
import java.util.concurrent.atomic.AtomicBoolean;
public class ServerConnectionService extends Service {
private final IntentChain intentChain;
private final AtomicBoolean running;
private final BasicThread thread;
private HolocoreSocket holocore;
private NetInterceptor interceptor;
private ForwarderData data;
public ServerConnectionService() {
this.intentChain = new IntentChain();
this.running = new AtomicBoolean(false);
this.thread = new BasicThread("server-connection", this::runningLoop);
this.holocore = null;
this.interceptor = null;
this.data = null;
}
@Override
public boolean stop() {
return stopRunningLoop();
}
@IntentHandler
private void handleStartForwarderIntent(StartForwarderIntent sfi) {
interceptor = new NetInterceptor(sfi.getData());
data = sfi.getData();
}
@IntentHandler
private void handleStopForwarderIntent(StopForwarderIntent sfi) {
stopRunningLoop();
}
@IntentHandler
private void handleClientConnectedIntent(ClientConnectedIntent cci) {
stopRunningLoop();
thread.start();
}
@IntentHandler
private void handleClientDisconnectedIntent(ClientDisconnectedIntent cdi) {
stopRunningLoop();
}
@IntentHandler
private void handleDataPacketInboundIntent(DataPacketInboundIntent dpii) {
if (running.get())
holocore.send(interceptor.interceptClient(dpii.getData()));
}
private boolean stopRunningLoop() {
if (running.getAndSet(false)) {
thread.stop(true);
return thread.awaitTermination(500);
}
return true;
}
private void runningLoop() {
holocore = new HolocoreSocket(data.getAddress().getAddress(), data.getAddress().getPort());
running.set(true);
while (running.get()) {
try {
connectedLoop();
} catch (Throwable t) {
Log.w(t);
Log.i("Disconnected from server. Sleeping 3 seconds");
holocore.disconnect(ServerConnectionChangedReason.UNKNOWN);
Delay.sleepMilli(3000);
}
}
Log.t("Destroying holocore connection");
holocore.terminate();
holocore = null;
}
private void connectedLoop() {
Log.t("Attempting to connect to server at %s", holocore.getRemoteAddress());
if (!holocore.connect(5000)) {
Log.t("Failed to connect to server. Sleeping 3 seconds");
Delay.sleepMilli(3000);
return;
}
intentChain.broadcastAfter(getIntentManager(), new ServerConnectedIntent());
Log.i("Successfully connected to server at %s", holocore.getRemoteAddress());
while (holocore.isConnected()) {
if (!running.get()) {
holocore.disconnect(ServerConnectionChangedReason.CLIENT_DISCONNECT);
break;
}
RawPacket inbound = holocore.receive();
if (inbound == null) {
holocore.disconnect(ServerConnectionChangedReason.SOCKET_CLOSED);
break;
}
intentChain.broadcastAfter(getIntentManager(), new DataPacketOutboundIntent(interceptor.interceptServer(inbound.getData())));
}
intentChain.broadcastAfter(getIntentManager(), new ServerDisconnectedIntent());
Log.i("Disconnected from server");
}
}