diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..6ba3356
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,88 @@
+name: Build G-Earth
+
+on:
+ push:
+ paths:
+ - '.github/workflows/**'
+ - 'G-Earth/**'
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+
+ steps:
+ - name: Checkout G-Earth
+ uses: actions/checkout@v2
+
+ - name: Checkout G-Wasm
+ uses: actions/checkout@v2
+ with:
+ repository: sirjonasxx/G-Wasm
+ path: gwasm
+ ref: minimal
+
+ - name: Set up JDK 8
+ uses: actions/setup-java@v2
+ with:
+ java-version: '8'
+ java-package: jdk+fx
+ distribution: 'liberica'
+
+ - name: Install G-Wasm
+ working-directory: gwasm
+ run: mvn -B install
+
+ - name: Build G-Earth
+ run: mvn -B package
+
+ - name: Zip Build/Mac
+ run: |
+ cd ${{ github.workspace }}/Build/Mac/
+ zip -r ../../build-mac.zip *
+
+ - name: Zip Build/Linux
+ run: |
+ cd ${{ github.workspace }}/Build/Linux/
+ zip -r ../../build-linux.zip *
+
+ - name: Zip Build/Windows_32bit
+ run: |
+ cd ${{ github.workspace }}/Build/Windows_32bit/
+ zip -r ../../build-win32.zip *
+
+ - name: Zip Build/Windows_64bit
+ run: |
+ cd ${{ github.workspace }}/Build/Windows_64bit/
+ zip -r ../../build-win64.zip *
+
+ - name: Upload Mac OSX
+ uses: actions/upload-artifact@v2
+ with:
+ name: Mac OSX
+ path: build-mac.zip
+ retention-days: 7
+
+ - name: Upload Linux
+ uses: actions/upload-artifact@v2
+ with:
+ name: Linux
+ path: build-linux.zip
+ retention-days: 7
+
+ - name: Upload Windows x32
+ uses: actions/upload-artifact@v2
+ with:
+ name: Windows x32
+ path: build-win32.zip
+ retention-days: 7
+
+ - name: Upload Windows x64
+ uses: actions/upload-artifact@v2
+ with:
+ name: Windows x64
+ path: build-win64.zip
+ retention-days: 7
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 902afbc..beed7d5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,3 +21,7 @@ Extensions/BlockReplacePackets/.classpath
*.settings/org.eclipse.jdt.apt.core.prefs
*.settings/org.eclipse.jdt.core.prefs
*.settings/org.eclipse.m2e.core.prefs
+
+# Certificates
+*.p12
+*.pem
\ No newline at end of file
diff --git a/G-Earth/pom.xml b/G-Earth/pom.xml
index 37b04bb..9164616 100644
--- a/G-Earth/pom.xml
+++ b/G-Earth/pom.xml
@@ -224,7 +224,11 @@
bytes
1.5.0
-
+
+ com.github.ganskef
+ littleproxy-mitm
+ 1.1.0
+
@@ -232,7 +236,7 @@
G-Earth
G-Wasm
- 1.0
+ 1.0.1
diff --git a/G-Earth/src/main/java/gearth/misc/RuntimeUtil.java b/G-Earth/src/main/java/gearth/misc/RuntimeUtil.java
new file mode 100644
index 0000000..0e17736
--- /dev/null
+++ b/G-Earth/src/main/java/gearth/misc/RuntimeUtil.java
@@ -0,0 +1,31 @@
+package gearth.misc;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+public final class RuntimeUtil {
+
+ public static String getCommandOutput(String[] command) throws IOException {
+ try {
+ final Runtime rt = Runtime.getRuntime();
+ final Process proc = rt.exec(command);
+
+ final BufferedReader stdInput = new BufferedReader(new InputStreamReader(proc.getInputStream()));
+ final StringBuilder result = new StringBuilder();
+
+ String line;
+
+ while ((line = stdInput.readLine()) != null) {
+ result.append(line);
+ result.append("\n");
+ }
+
+ return result.toString();
+ } catch (IOException e) {
+ e.printStackTrace();
+ throw e;
+ }
+ }
+
+}
diff --git a/G-Earth/src/main/java/gearth/protocol/HConnection.java b/G-Earth/src/main/java/gearth/protocol/HConnection.java
index 90029cf..70bd0c4 100644
--- a/G-Earth/src/main/java/gearth/protocol/HConnection.java
+++ b/G-Earth/src/main/java/gearth/protocol/HConnection.java
@@ -1,6 +1,7 @@
package gearth.protocol;
import gearth.misc.listenerpattern.Observable;
+import gearth.protocol.connection.proxy.nitro.NitroProxyProvider;
import gearth.services.packet_info.PacketInfoManager;
import gearth.protocol.connection.HClient;
import gearth.protocol.connection.HProxy;
@@ -68,6 +69,12 @@ public class HConnection {
startMITM();
}
+ public void startNitro() {
+ HConnection selff = this;
+ proxyProvider = new NitroProxyProvider(proxy -> selff.proxy = proxy, selff::setState, this);
+ startMITM();
+ }
+
private void startMITM() {
try {
if (proxyProvider != null) {
@@ -140,6 +147,7 @@ public class HConnection {
public boolean sendToClient(HPacket packet) {
+ HProxy proxy = this.proxy;
if (proxy == null) return false;
if (!packet.isPacketComplete()) {
@@ -149,10 +157,10 @@ public class HConnection {
if (!packet.isPacketComplete() || !packet.canSendToClient()) return false;
}
- proxy.getPacketSenderQueue().queueToClient(packet);
- return true;
+ return proxy.sendToClient(packet);
}
public boolean sendToServer(HPacket packet) {
+ HProxy proxy = this.proxy;
if (proxy == null) return false;
if (!packet.isPacketComplete()) {
@@ -162,8 +170,7 @@ public class HConnection {
if (!packet.isPacketComplete() || !packet.canSendToServer()) return false;
}
- proxy.getPacketSenderQueue().queueToServer(packet);
- return true;
+ return proxy.sendToServer(packet);
}
public String getClientHost() {
diff --git a/G-Earth/src/main/java/gearth/protocol/connection/HClient.java b/G-Earth/src/main/java/gearth/protocol/connection/HClient.java
index 54653c3..027fafd 100644
--- a/G-Earth/src/main/java/gearth/protocol/connection/HClient.java
+++ b/G-Earth/src/main/java/gearth/protocol/connection/HClient.java
@@ -2,5 +2,6 @@ package gearth.protocol.connection;
public enum HClient {
UNITY,
- FLASH
+ FLASH,
+ NITRO
}
diff --git a/G-Earth/src/main/java/gearth/protocol/connection/HProxy.java b/G-Earth/src/main/java/gearth/protocol/connection/HProxy.java
index 9b90790..e3bb228 100644
--- a/G-Earth/src/main/java/gearth/protocol/connection/HProxy.java
+++ b/G-Earth/src/main/java/gearth/protocol/connection/HProxy.java
@@ -1,5 +1,6 @@
package gearth.protocol.connection;
+import gearth.protocol.HPacket;
import gearth.services.packet_info.PacketInfoManager;
import gearth.protocol.packethandler.PacketHandler;
@@ -25,8 +26,6 @@ public class HProxy {
private volatile String clientIdentifier = "";
private volatile PacketInfoManager packetInfoManager = null;
- private volatile PacketSenderQueue packetSenderQueue = null;
-
public HProxy(HClient hClient, String input_domain, String actual_domain, int actual_port, int intercept_port, String intercept_host) {
this.hClient = hClient;
this.input_domain = input_domain;
@@ -46,7 +45,20 @@ public class HProxy {
this.hotelVersion = hotelVersion;
this.clientIdentifier = clientIdentifier;
this.packetInfoManager = PacketInfoManager.fromHotelVersion(hotelVersion, hClient);
- this.packetSenderQueue = new PacketSenderQueue(this);
+ }
+
+ public boolean sendToServer(HPacket packet) {
+ if (outHandler != null) {
+ return outHandler.sendToStream(packet.toBytes());
+ }
+ return false;
+ }
+
+ public boolean sendToClient(HPacket packet) {
+ if (inHandler != null) {
+ return inHandler.sendToStream(packet.toBytes());
+ }
+ return false;
}
public String getClientIdentifier() {
@@ -89,10 +101,6 @@ public class HProxy {
return hotelVersion;
}
- public PacketSenderQueue getPacketSenderQueue() {
- return packetSenderQueue;
- }
-
public HClient gethClient() {
return hClient;
}
diff --git a/G-Earth/src/main/java/gearth/protocol/connection/PacketSenderQueue.java b/G-Earth/src/main/java/gearth/protocol/connection/PacketSenderQueue.java
deleted file mode 100644
index bef2d2b..0000000
--- a/G-Earth/src/main/java/gearth/protocol/connection/PacketSenderQueue.java
+++ /dev/null
@@ -1,78 +0,0 @@
-package gearth.protocol.connection;
-
-import gearth.protocol.HPacket;
-
-import java.util.LinkedList;
-import java.util.Queue;
-
-public class PacketSenderQueue {
-
- private final HProxy proxy;
- private final Queue sendToClientQueue = new LinkedList<>();
- private final Queue sendToServerQueue = new LinkedList<>();
-
- PacketSenderQueue(HProxy proxy) {
- this.proxy = proxy;
- new Thread(() -> {
- while (true) {
- HPacket packet;
- synchronized (sendToClientQueue) {
- while ((packet = sendToClientQueue.poll()) != null) {
- sendToClient(packet);
- }
- }
-
- try {
- Thread.sleep(1);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }).start();
- new Thread(() -> {
- while (true) {
- HPacket packet;
- synchronized (sendToServerQueue) {
- while ((packet = sendToServerQueue.poll()) != null) {
- sendToServer(packet);
- }
- }
-
- try {
- Thread.sleep(1);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }).start();
- }
-
-
- private void sendToClient(HPacket message) {
- proxy.getInHandler().sendToStream(message.toBytes());
- }
- private void sendToServer(HPacket message) {
- proxy.getOutHandler().sendToStream(message.toBytes());
- }
-
- public void queueToClient(HPacket message) {
- synchronized (sendToClientQueue) {
- sendToClientQueue.add(message);
- }
-
- }
- public void queueToServer(HPacket message) {
- synchronized (sendToServerQueue) {
- sendToServerQueue.add(message);
- }
- }
-
- public void clear() {
- synchronized (sendToClientQueue) {
- sendToClientQueue.clear();
- }
- synchronized (sendToServerQueue) {
- sendToServerQueue.clear();
- }
- }
-}
diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/NitroConstants.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/NitroConstants.java
new file mode 100644
index 0000000..2d111f3
--- /dev/null
+++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/NitroConstants.java
@@ -0,0 +1,13 @@
+package gearth.protocol.connection.proxy.nitro;
+
+public final class NitroConstants {
+
+ public static final int HTTP_PORT = 9090;
+ public static final int HTTP_BUFFER_SIZE = 1024 * 1024 * 10;
+
+ public static final int WEBSOCKET_PORT = 2096;
+ public static final int WEBSOCKET_BUFFER_SIZE = 1024 * 1024 * 10;
+ public static final String WEBSOCKET_REVISION = "PRODUCTION-201611291003-338511768";
+ public static final String WEBSOCKET_CLIENT_IDENTIFIER = "HTML5";
+
+}
diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/NitroProxyProvider.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/NitroProxyProvider.java
new file mode 100644
index 0000000..5b90922
--- /dev/null
+++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/NitroProxyProvider.java
@@ -0,0 +1,129 @@
+package gearth.protocol.connection.proxy.nitro;
+
+import gearth.protocol.HConnection;
+import gearth.protocol.StateChangeListener;
+import gearth.protocol.connection.HProxySetter;
+import gearth.protocol.connection.HState;
+import gearth.protocol.connection.HStateSetter;
+import gearth.protocol.connection.proxy.ProxyProvider;
+import gearth.protocol.connection.proxy.nitro.http.NitroHttpProxy;
+import gearth.protocol.connection.proxy.nitro.http.NitroHttpProxyServerCallback;
+import gearth.protocol.connection.proxy.nitro.websocket.NitroWebsocketProxy;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class NitroProxyProvider implements ProxyProvider, NitroHttpProxyServerCallback, StateChangeListener {
+
+ private final HProxySetter proxySetter;
+ private final HStateSetter stateSetter;
+ private final HConnection connection;
+ private final NitroHttpProxy nitroHttpProxy;
+ private final NitroWebsocketProxy nitroWebsocketProxy;
+ private final AtomicBoolean abortLock;
+
+ private String originalWebsocketUrl;
+ private String originalOriginUrl;
+
+ public NitroProxyProvider(HProxySetter proxySetter, HStateSetter stateSetter, HConnection connection) {
+ this.proxySetter = proxySetter;
+ this.stateSetter = stateSetter;
+ this.connection = connection;
+ this.nitroHttpProxy = new NitroHttpProxy(this);
+ this.nitroWebsocketProxy = new NitroWebsocketProxy(proxySetter, stateSetter, connection, this);
+ this.abortLock = new AtomicBoolean();
+ }
+
+ public String getOriginalWebsocketUrl() {
+ return originalWebsocketUrl;
+ }
+
+ public String getOriginalOriginUrl() {
+ return originalOriginUrl;
+ }
+
+ @Override
+ public void start() throws IOException {
+ connection.getStateObservable().addListener(this);
+
+ if (!nitroHttpProxy.start()) {
+ System.out.println("Failed to start nitro proxy");
+ abort();
+ return;
+ }
+
+ if (!nitroWebsocketProxy.start()) {
+ System.out.println("Failed to start nitro websocket proxy");
+ abort();
+ return;
+ }
+
+ stateSetter.setState(HState.WAITING_FOR_CLIENT);
+ }
+
+ @Override
+ public void abort() {
+ if (abortLock.get()) {
+ return;
+ }
+
+ if (abortLock.compareAndSet(true, true)) {
+ return;
+ }
+
+ stateSetter.setState(HState.ABORTING);
+
+ new Thread(() -> {
+ try {
+ nitroHttpProxy.stop();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ try {
+ nitroWebsocketProxy.stop();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ stateSetter.setState(HState.NOT_CONNECTED);
+
+ connection.getStateObservable().removeListener(this);
+ }).start();
+ }
+
+ @Override
+ public String replaceWebsocketServer(String configUrl, String websocketUrl) {
+ originalWebsocketUrl = websocketUrl;
+ originalOriginUrl = extractOriginUrl(configUrl);
+
+ return String.format("ws://127.0.0.1:%d", NitroConstants.WEBSOCKET_PORT);
+ }
+
+ @Override
+ public void stateChanged(HState oldState, HState newState) {
+ if (oldState == HState.WAITING_FOR_CLIENT && newState == HState.CONNECTED) {
+ // Unregister but do not stop http proxy.
+ // We are not stopping the http proxy because some requests might still require it to be running.
+ nitroHttpProxy.pause();
+ }
+
+ // Catch setState ABORTING inside NitroWebsocketClient.
+ if (newState == HState.ABORTING) {
+ abort();
+ }
+ }
+
+ private static String extractOriginUrl(String url) {
+ try {
+ final URI uri = new URI(url);
+ return String.format("%s://%s/", uri.getScheme(), uri.getHost());
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ }
+
+ return null;
+ }
+}
diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroAuthority.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroAuthority.java
new file mode 100644
index 0000000..fd5bd4e
--- /dev/null
+++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroAuthority.java
@@ -0,0 +1,24 @@
+package gearth.protocol.connection.proxy.nitro.http;
+
+import org.littleshoot.proxy.mitm.Authority;
+
+import java.io.File;
+
+public class NitroAuthority extends Authority {
+
+ private static final String CERT_ALIAS = "gearth-nitro";
+ private static final String CERT_ORGANIZATION = "G-Earth Nitro";
+ private static final String CERT_DESCRIPTION = "G-Earth nitro support";
+
+ public NitroAuthority() {
+ super(new File("."),
+ CERT_ALIAS,
+ "verysecure".toCharArray(),
+ CERT_DESCRIPTION,
+ CERT_ORGANIZATION,
+ "Certificate Authority",
+ CERT_ORGANIZATION,
+ CERT_DESCRIPTION);
+ }
+
+}
diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroCertificateSniffingManager.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroCertificateSniffingManager.java
new file mode 100644
index 0000000..35f4c2d
--- /dev/null
+++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroCertificateSniffingManager.java
@@ -0,0 +1,99 @@
+package gearth.protocol.connection.proxy.nitro.http;
+
+import io.netty.handler.codec.http.HttpRequest;
+import org.littleshoot.proxy.MitmManager;
+import org.littleshoot.proxy.mitm.*;
+
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSession;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.List;
+
+/**
+ * {@link MitmManager} that uses the common name and subject alternative names
+ * from the upstream certificate to create a dynamic certificate with it.
+ */
+public class NitroCertificateSniffingManager implements MitmManager {
+
+ private static final boolean DEBUG = false;
+
+ private final BouncyCastleSslEngineSource sslEngineSource;
+
+ public NitroCertificateSniffingManager(Authority authority) throws RootCertificateException {
+ try {
+ sslEngineSource = new BouncyCastleSslEngineSource(authority, true, true, null);
+ } catch (final Exception e) {
+ throw new RootCertificateException("Errors during assembling root CA.", e);
+ }
+ }
+
+ public SSLEngine serverSslEngine(String peerHost, int peerPort) {
+ return sslEngineSource.newSslEngine(peerHost, peerPort);
+ }
+
+ public SSLEngine serverSslEngine() {
+ return sslEngineSource.newSslEngine();
+ }
+
+ public SSLEngine clientSslEngineFor(HttpRequest httpRequest, SSLSession serverSslSession) {
+ try {
+ X509Certificate upstreamCert = getCertificateFromSession(serverSslSession);
+ // TODO store the upstream cert by commonName to review it later
+
+ // A reasons to not use the common name and the alternative names
+ // from upstream certificate from serverSslSession to create the
+ // dynamic certificate:
+ //
+ // It's not necessary. The host name is accepted by the browser.
+ //
+ String commonName = getCommonName(upstreamCert);
+
+ SubjectAlternativeNameHolder san = new SubjectAlternativeNameHolder();
+
+ san.addAll(upstreamCert.getSubjectAlternativeNames());
+
+ if (DEBUG) {
+ System.out.println("[NitroCertificateSniffingManager] Subject Alternative Names");
+
+ for (List> name : upstreamCert.getSubjectAlternativeNames()) {
+ System.out.printf("[NitroCertificateSniffingManager] - %s%n", name.toString());
+ }
+ }
+
+ return sslEngineSource.createCertForHost(commonName, san);
+ } catch (Exception e) {
+ throw new FakeCertificateException("Creation dynamic certificate failed", e);
+ }
+ }
+
+ private X509Certificate getCertificateFromSession(SSLSession sslSession) throws SSLPeerUnverifiedException {
+ Certificate[] peerCerts = sslSession.getPeerCertificates();
+ Certificate peerCert = peerCerts[0];
+ if (peerCert instanceof java.security.cert.X509Certificate) {
+ return (java.security.cert.X509Certificate) peerCert;
+ }
+ throw new IllegalStateException("Required java.security.cert.X509Certificate, found: " + peerCert);
+ }
+
+ private String getCommonName(X509Certificate c) {
+ if (DEBUG) {
+ System.out.printf("[NitroCertificateSniffingManager] Subject DN principal name: %s%n", c.getSubjectDN().getName());
+ }
+
+ for (String each : c.getSubjectDN().getName().split(",\\s*")) {
+ if (each.startsWith("CN=")) {
+ String result = each.substring(3);
+
+ if (DEBUG) {
+ System.out.printf("[NitroCertificateSniffingManager] Common Name: %s%n", c.getSubjectDN().getName());
+ }
+
+ return result;
+ }
+ }
+
+ throw new IllegalStateException("Missed CN in Subject DN: " + c.getSubjectDN());
+ }
+}
\ No newline at end of file
diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxy.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxy.java
new file mode 100644
index 0000000..cc897a6
--- /dev/null
+++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxy.java
@@ -0,0 +1,152 @@
+package gearth.protocol.connection.proxy.nitro.http;
+
+import gearth.misc.ConfirmationDialog;
+import gearth.protocol.connection.proxy.nitro.NitroConstants;
+import gearth.protocol.connection.proxy.nitro.os.NitroOsFunctions;
+import gearth.protocol.connection.proxy.nitro.os.NitroOsFunctionsFactory;
+import javafx.application.Platform;
+import javafx.scene.control.Alert;
+import javafx.scene.control.ButtonType;
+import org.littleshoot.proxy.HttpProxyServer;
+import org.littleshoot.proxy.impl.DefaultHttpProxyServer;
+import org.littleshoot.proxy.mitm.Authority;
+import org.littleshoot.proxy.mitm.RootCertificateException;
+
+import java.io.File;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class NitroHttpProxy {
+
+ private static final String ADMIN_WARNING_KEY = "admin_warning_dialog";
+ private static final AtomicBoolean SHUTDOWN_HOOK = new AtomicBoolean();
+
+ private final Authority authority;
+ private final NitroOsFunctions osFunctions;
+ private final NitroHttpProxyServerCallback serverCallback;
+
+ private HttpProxyServer proxyServer = null;
+
+ public NitroHttpProxy(NitroHttpProxyServerCallback serverCallback) {
+ this.serverCallback = serverCallback;
+ this.authority = new NitroAuthority();
+ this.osFunctions = NitroOsFunctionsFactory.create();
+ }
+
+ private boolean initializeCertificate() {
+ final File certificate = this.authority.aliasFile(".pem");
+
+ // All good if certificate is already trusted.
+ if (this.osFunctions.isRootCertificateTrusted(certificate)) {
+ return true;
+ }
+
+ // Let the user know about admin permissions.
+ final Semaphore waitForDialog = new Semaphore(0);
+ final AtomicBoolean shouldInstall = new AtomicBoolean();
+
+ Platform.runLater(() -> {
+ Alert alert = ConfirmationDialog.createAlertWithOptOut(Alert.AlertType.WARNING, ADMIN_WARNING_KEY,
+ "Root certificate installation", null,
+ "G-Earth detected that you do not have the root certificate authority installed. " +
+ "This is required for Nitro to work, do you want to continue? " +
+ "G-Earth will ask you for Administrator permission if you do so.", "Remember my choice",
+ ButtonType.YES, ButtonType.NO
+ );
+
+ shouldInstall.set(alert.showAndWait().filter(t -> t == ButtonType.YES).isPresent());
+ waitForDialog.release();
+ });
+
+ // Wait for dialog choice.
+ try {
+ waitForDialog.acquire();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ return false;
+ }
+
+ // User opted out.
+ if (!shouldInstall.get()) {
+ return false;
+ }
+
+ return this.osFunctions.installRootCertificate(this.authority.aliasFile(".pem"));
+ }
+
+ /**
+ * Register HTTP(s) proxy on the system.
+ */
+ private boolean registerProxy() {
+ return this.osFunctions.registerSystemProxy("127.0.0.1", NitroConstants.HTTP_PORT);
+ }
+
+ /**
+ * Unregister HTTP(s) proxy from system.
+ */
+ private boolean unregisterProxy() {
+ return this.osFunctions.unregisterSystemProxy();
+ }
+
+ public boolean start() {
+ setupShutdownHook();
+
+ try {
+ proxyServer = DefaultHttpProxyServer.bootstrap()
+ .withPort(NitroConstants.HTTP_PORT)
+ .withManInTheMiddle(new NitroCertificateSniffingManager(authority))
+ .withFiltersSource(new NitroHttpProxyFilterSource(serverCallback))
+ .start();
+
+ if (!initializeCertificate()) {
+ proxyServer.stop();
+
+ System.out.println("Failed to initialize certificate");
+ return false;
+ }
+
+ if (!registerProxy()) {
+ proxyServer.stop();
+
+ System.out.println("Failed to register certificate");
+ return false;
+ }
+
+ return true;
+ } catch (RootCertificateException e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ public void pause() {
+ if (!unregisterProxy()) {
+ System.out.println("Failed to unregister system proxy, please check manually");
+ }
+ }
+
+ public void stop() {
+ pause();
+
+ if (proxyServer == null) {
+ return;
+ }
+
+ proxyServer.stop();
+ proxyServer = null;
+ }
+
+ /**
+ * Ensure the system proxy is removed when G-Earth exits.
+ * Otherwise, users might complain that their browsers / discord stop working when closing G-Earth incorrectly.
+ */
+ private static void setupShutdownHook() {
+ if (SHUTDOWN_HOOK.get()) {
+ return;
+ }
+
+ if (SHUTDOWN_HOOK.compareAndSet(false, true)) {
+ Runtime.getRuntime().addShutdownHook(new Thread(() -> NitroOsFunctionsFactory.create().unregisterSystemProxy()));
+ }
+ }
+}
diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxyFilter.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxyFilter.java
new file mode 100644
index 0000000..ea0cddc
--- /dev/null
+++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxyFilter.java
@@ -0,0 +1,152 @@
+package gearth.protocol.connection.proxy.nitro.http;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.http.*;
+import io.netty.util.CharsetUtil;
+import org.littleshoot.proxy.HttpFiltersAdapter;
+
+import java.nio.charset.StandardCharsets;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class NitroHttpProxyFilter extends HttpFiltersAdapter {
+
+ private static final String NitroConfigSearch = "\"socket.url\"";
+ private static final String NitroClientSearch = "configurationUrls:";
+ private static final Pattern NitroConfigPattern = Pattern.compile("\"socket\\.url\":.?\"(wss?://.*?)\"", Pattern.MULTILINE);
+
+ private static final String HeaderAcceptEncoding = "Accept-Encoding";
+ private static final String HeaderAge = "Age";
+ private static final String HeaderCacheControl = "Cache-Control";
+ private static final String HeaderContentSecurityPolicy = "Content-Security-Policy";
+ private static final String HeaderETag = "ETag";
+ private static final String HeaderIfNoneMatch = "If-None-Match";
+ private static final String HeaderIfModifiedSince = "If-Modified-Since";
+ private static final String HeaderLastModified = "Last-Modified";
+
+ private final NitroHttpProxyServerCallback callback;
+ private final String url;
+
+ public NitroHttpProxyFilter(HttpRequest originalRequest, ChannelHandlerContext ctx, NitroHttpProxyServerCallback callback, String url) {
+ super(originalRequest, ctx);
+ this.callback = callback;
+ this.url = url;
+ }
+
+ @Override
+ public HttpResponse clientToProxyRequest(HttpObject httpObject) {
+ if (httpObject instanceof HttpRequest) {
+ HttpRequest request = (HttpRequest) httpObject;
+ HttpHeaders headers = request.headers();
+
+ // Only support gzip or deflate.
+ // The LittleProxy library does not support brotli.
+ if (headers.contains(HeaderAcceptEncoding)) {
+ String encoding = headers.get(HeaderAcceptEncoding);
+
+ if (encoding.contains("br")) {
+ if (encoding.contains("gzip") && encoding.contains("deflate")) {
+ headers.set(HeaderAcceptEncoding, "gzip, deflate");
+ } else if (encoding.contains("gzip")) {
+ headers.set(HeaderAcceptEncoding, "gzip, deflate");
+ } else {
+ headers.remove(HeaderAcceptEncoding);
+ }
+ }
+ }
+
+ // Disable caching.
+ stripCacheHeaders(headers);
+ }
+
+ return super.clientToProxyRequest(httpObject);
+ }
+
+ @Override
+ public HttpObject serverToProxyResponse(HttpObject httpObject) {
+ if (httpObject instanceof FullHttpResponse) {
+ final FullHttpResponse response = (FullHttpResponse) httpObject;
+
+ // Find nitro configuration file.
+ boolean responseModified = false;
+ String responseBody = responseRead(response);
+
+ if (responseBody.contains(NitroConfigSearch)) {
+ final Matcher matcher = NitroConfigPattern.matcher(responseBody);
+
+ if (matcher.find()) {
+ final String originalWebsocket = matcher.group(1);
+ final String replacementWebsocket = callback.replaceWebsocketServer(url, originalWebsocket);
+
+ if (replacementWebsocket != null) {
+ responseBody = responseBody.replace(originalWebsocket, replacementWebsocket);
+ responseModified = true;
+ }
+ }
+ }
+
+ // Apply changes.
+ if (responseModified) {
+ responseWrite(response, responseBody);
+ }
+
+ // CSP.
+ if (responseBody.contains(NitroClientSearch)) {
+ stripContentSecurityPolicy(response);
+ }
+ }
+
+ return httpObject;
+ }
+
+ /**
+ * Modify Content-Security-Policy header, which could prevent Nitro from connecting with G-Earth.
+ */
+ private void stripContentSecurityPolicy(FullHttpResponse response) {
+ final HttpHeaders headers = response.headers();
+
+ if (!headers.contains(HeaderContentSecurityPolicy)){
+ return;
+ }
+
+ String csp = headers.get(HeaderContentSecurityPolicy);
+
+ if (csp.contains("connect-src")) {
+ csp = csp.replace("connect-src", "connect-src *");
+ } else if (csp.contains("default-src")) {
+ csp = csp.replace("default-src", "default-src *");
+ }
+
+ headers.set(HeaderContentSecurityPolicy, csp);
+ }
+
+ private static String responseRead(FullHttpResponse response) {
+ final ByteBuf contentBuf = response.content();
+ return contentBuf.toString(CharsetUtil.UTF_8);
+ }
+
+ private static void responseWrite(FullHttpResponse response, String content) {
+ final byte[] body = content.getBytes(StandardCharsets.UTF_8);
+
+ // Update content.
+ response.content().clear().writeBytes(body);
+
+ // Update content-length.
+ HttpHeaders.setContentLength(response, body.length);
+
+ // Ensure modified response is not cached.
+ stripCacheHeaders(response.headers());
+ }
+
+ private static void stripCacheHeaders(HttpHeaders headers) {
+ headers.remove(HeaderAcceptEncoding);
+ headers.remove(HeaderAge);
+ headers.remove(HeaderCacheControl);
+ headers.remove(HeaderETag);
+ headers.remove(HeaderIfNoneMatch);
+ headers.remove(HeaderIfModifiedSince);
+ headers.remove(HeaderLastModified);
+ }
+
+}
diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxyFilterSource.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxyFilterSource.java
new file mode 100644
index 0000000..c736458
--- /dev/null
+++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxyFilterSource.java
@@ -0,0 +1,47 @@
+package gearth.protocol.connection.proxy.nitro.http;
+
+import gearth.protocol.connection.proxy.nitro.NitroConstants;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.http.HttpMethod;
+import io.netty.handler.codec.http.HttpRequest;
+import io.netty.util.AttributeKey;
+import org.littleshoot.proxy.HttpFilters;
+import org.littleshoot.proxy.HttpFiltersAdapter;
+import org.littleshoot.proxy.HttpFiltersSourceAdapter;
+
+public class NitroHttpProxyFilterSource extends HttpFiltersSourceAdapter {
+
+ private static final AttributeKey CONNECTED_URL = AttributeKey.valueOf("connected_url");
+
+ private final NitroHttpProxyServerCallback callback;
+
+ public NitroHttpProxyFilterSource(NitroHttpProxyServerCallback callback) {
+ this.callback = callback;
+ }
+
+ @Override
+ public HttpFilters filterRequest(HttpRequest originalRequest, ChannelHandlerContext ctx) {
+ // https://github.com/ganskef/LittleProxy-mitm#resolving-uri-in-case-of-https
+ String uri = originalRequest.getUri();
+ if (originalRequest.getMethod() == HttpMethod.CONNECT) {
+ if (ctx != null) {
+ String prefix = "https://" + uri.replaceFirst(":443$", "");
+ ctx.channel().attr(CONNECTED_URL).set(prefix);
+ }
+ return new HttpFiltersAdapter(originalRequest, ctx);
+ }
+
+ String connectedUrl = ctx.channel().attr(CONNECTED_URL).get();
+ if (connectedUrl == null) {
+ return new NitroHttpProxyFilter(originalRequest, ctx, callback, uri);
+ }
+
+ return new NitroHttpProxyFilter(originalRequest, ctx, callback, connectedUrl + uri);
+ }
+
+ @Override
+ public int getMaximumResponseBufferSizeInBytes() {
+ // Increasing this causes LittleProxy to output "FullHttpResponse" objects.
+ return NitroConstants.HTTP_BUFFER_SIZE;
+ }
+}
diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxyServerCallback.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxyServerCallback.java
new file mode 100644
index 0000000..3f04f11
--- /dev/null
+++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxyServerCallback.java
@@ -0,0 +1,14 @@
+package gearth.protocol.connection.proxy.nitro.http;
+
+public interface NitroHttpProxyServerCallback {
+
+ /**
+ * Specify a replacement for the given websocket url.
+ *
+ * @param configUrl The url at which the websocket url was found.
+ * @param websocketUrl The hotel websocket url.
+ * @return Return null to not replace anything, otherwise specify an alternative websocket url.
+ */
+ String replaceWebsocketServer(String configUrl, String websocketUrl);
+
+}
diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/os/NitroOsFunctions.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/os/NitroOsFunctions.java
new file mode 100644
index 0000000..0bfaf29
--- /dev/null
+++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/os/NitroOsFunctions.java
@@ -0,0 +1,15 @@
+package gearth.protocol.connection.proxy.nitro.os;
+
+import java.io.File;
+
+public interface NitroOsFunctions {
+
+ boolean isRootCertificateTrusted(File certificate);
+
+ boolean installRootCertificate(File certificate);
+
+ boolean registerSystemProxy(String host, int port);
+
+ boolean unregisterSystemProxy();
+
+}
diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/os/NitroOsFunctionsFactory.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/os/NitroOsFunctionsFactory.java
new file mode 100644
index 0000000..18bbf7c
--- /dev/null
+++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/os/NitroOsFunctionsFactory.java
@@ -0,0 +1,21 @@
+package gearth.protocol.connection.proxy.nitro.os;
+
+import gearth.misc.OSValidator;
+import gearth.protocol.connection.proxy.nitro.os.windows.NitroWindows;
+import org.apache.commons.lang3.NotImplementedException;
+
+public final class NitroOsFunctionsFactory {
+
+ public static NitroOsFunctions create() {
+ if (OSValidator.isWindows()) {
+ return new NitroWindows();
+ }
+
+ if (OSValidator.isUnix()) {
+ throw new NotImplementedException("unix nitro is not implemented yet");
+ }
+
+ throw new NotImplementedException("macOS nitro is not implemented yet");
+ }
+
+}
diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/os/windows/NitroWindows.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/os/windows/NitroWindows.java
new file mode 100644
index 0000000..c5f4655
--- /dev/null
+++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/os/windows/NitroWindows.java
@@ -0,0 +1,89 @@
+package gearth.protocol.connection.proxy.nitro.os.windows;
+
+import com.sun.jna.platform.win32.Kernel32;
+import com.sun.jna.platform.win32.WinBase;
+import com.sun.jna.platform.win32.WinDef;
+import com.sun.jna.ptr.IntByReference;
+import gearth.misc.RuntimeUtil;
+import gearth.protocol.connection.proxy.nitro.os.NitroOsFunctions;
+
+import java.io.File;
+import java.io.IOException;
+
+public class NitroWindows implements NitroOsFunctions {
+
+ /**
+ * Semicolon separated hosts to ignore for proxying.
+ */
+ private static final String PROXY_IGNORE = "discord.com;discordapp.com;github.com;";
+
+ /**
+ * Checks if the certificate is trusted by the local machine.
+ * @param certificate Absolute path to the certificate.
+ * @return true if trusted
+ */
+ @Override
+ public boolean isRootCertificateTrusted(File certificate) {
+ try {
+ final String output = RuntimeUtil.getCommandOutput(new String[] {"cmd", "/c", " certutil.exe -f -verify " + certificate.getAbsolutePath()});
+
+ return !output.contains("CERT_TRUST_IS_UNTRUSTED_ROOT") &&
+ output.contains("dwInfoStatus=10c dwErrorStatus=0");
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean installRootCertificate(File certificate) {
+ final String certificatePath = certificate.getAbsolutePath();
+
+ // Prompt UAC elevation.
+ WinDef.HINSTANCE result = NitroWindowsShell32.INSTANCE.ShellExecuteA(null, "runas", "cmd.exe", "/S /C \"certutil -addstore root " + certificatePath + "\"", null, 1);
+
+ // Wait for exit.
+ Kernel32.INSTANCE.WaitForSingleObject(result, WinBase.INFINITE);
+
+ // Exit code for certutil.
+ final IntByReference statusRef = new IntByReference(-1);
+ Kernel32.INSTANCE.GetExitCodeProcess(result, statusRef);
+
+ // Check if process exited without errors
+ if (statusRef.getValue() != -1) {
+ System.out.printf("Certutil command exited with exit code %s%n", statusRef.getValue());
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean registerSystemProxy(String host, int port) {
+ try {
+ final String proxy = String.format("%s:%d", host, port);
+ Runtime.getRuntime().exec("reg add \"HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\" /v ProxyServer /t REG_SZ /d \"" + proxy + "\" /f");
+ Runtime.getRuntime().exec("reg add \"HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\" /v ProxyOverride /t REG_SZ /d \"" + PROXY_IGNORE + "\" /f");
+ Runtime.getRuntime().exec("reg add \"HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\" /v ProxyEnable /t REG_DWORD /d 1 /f");
+ return true;
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean unregisterSystemProxy() {
+ try {
+ Runtime.getRuntime().exec("reg add \"HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\" /v ProxyEnable /t REG_DWORD /d 0 /f");
+ return true;
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ return false;
+ }
+
+}
diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/os/windows/NitroWindowsShell32.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/os/windows/NitroWindowsShell32.java
new file mode 100644
index 0000000..69087aa
--- /dev/null
+++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/os/windows/NitroWindowsShell32.java
@@ -0,0 +1,17 @@
+package gearth.protocol.connection.proxy.nitro.os.windows;
+
+import com.sun.jna.Native;
+import com.sun.jna.platform.win32.ShellAPI;
+import com.sun.jna.platform.win32.WinDef;
+import com.sun.jna.win32.StdCallLibrary;
+
+public interface NitroWindowsShell32 extends ShellAPI, StdCallLibrary {
+ NitroWindowsShell32 INSTANCE = Native.loadLibrary("shell32", NitroWindowsShell32.class);
+
+ WinDef.HINSTANCE ShellExecuteA(WinDef.HWND hwnd,
+ String lpOperation,
+ String lpFile,
+ String lpParameters,
+ String lpDirectory,
+ int nShowCmd);
+}
diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroPacketHandler.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroPacketHandler.java
new file mode 100644
index 0000000..70ed0cb
--- /dev/null
+++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroPacketHandler.java
@@ -0,0 +1,70 @@
+package gearth.protocol.connection.proxy.nitro.websocket;
+
+import gearth.protocol.HMessage;
+import gearth.protocol.HPacket;
+import gearth.protocol.packethandler.PacketHandler;
+import gearth.protocol.packethandler.PayloadBuffer;
+import gearth.services.extension_handler.ExtensionHandler;
+import gearth.services.extension_handler.OnHMessageHandled;
+
+import javax.websocket.Session;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+public class NitroPacketHandler extends PacketHandler {
+
+ private final HMessage.Direction direction;
+ private final NitroSession session;
+ private final PayloadBuffer payloadBuffer;
+ private final Object payloadLock;
+
+ protected NitroPacketHandler(HMessage.Direction direction, NitroSession session, ExtensionHandler extensionHandler, Object[] trafficObservables) {
+ super(extensionHandler, trafficObservables);
+ this.direction = direction;
+ this.session = session;
+ this.payloadBuffer = new PayloadBuffer();
+ this.payloadLock = new Object();
+ }
+
+ @Override
+ public boolean sendToStream(byte[] buffer) {
+ final Session localSession = session.getSession();
+
+ if (localSession == null) {
+ return false;
+ }
+
+ // Required to prevent garbage buffer within the UI logger.
+ if (direction == HMessage.Direction.TOSERVER) {
+ buffer = buffer.clone();
+ }
+
+ localSession.getAsyncRemote().sendBinary(ByteBuffer.wrap(buffer));
+ return true;
+ }
+
+ @Override
+ public void act(byte[] buffer) throws IOException {
+ payloadBuffer.push(buffer);
+
+ synchronized (payloadLock) {
+ for (HPacket packet : payloadBuffer.receive()) {
+ HMessage hMessage = new HMessage(packet, direction, currentIndex);
+
+ OnHMessageHandled afterExtensionIntercept = hMessage1 -> {
+ notifyListeners(2, hMessage1);
+
+ if (!hMessage1.isBlocked()) {
+ sendToStream(hMessage1.getPacket().toBytes());
+ }
+ };
+
+ notifyListeners(0, hMessage);
+ notifyListeners(1, hMessage);
+ extensionHandler.handle(hMessage, afterExtensionIntercept);
+
+ currentIndex++;
+ }
+ }
+ }
+}
diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroSession.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroSession.java
new file mode 100644
index 0000000..f85854e
--- /dev/null
+++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroSession.java
@@ -0,0 +1,9 @@
+package gearth.protocol.connection.proxy.nitro.websocket;
+
+import javax.websocket.Session;
+
+public interface NitroSession {
+
+ Session getSession();
+
+}
diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroWebsocketClient.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroWebsocketClient.java
new file mode 100644
index 0000000..ca7e88b
--- /dev/null
+++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroWebsocketClient.java
@@ -0,0 +1,118 @@
+package gearth.protocol.connection.proxy.nitro.websocket;
+
+import gearth.protocol.HConnection;
+import gearth.protocol.HMessage;
+import gearth.protocol.connection.*;
+import gearth.protocol.connection.proxy.nitro.NitroConstants;
+import gearth.protocol.connection.proxy.nitro.NitroProxyProvider;
+
+import javax.websocket.*;
+import javax.websocket.server.ServerEndpoint;
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+@ServerEndpoint(value = "/")
+public class NitroWebsocketClient implements NitroSession {
+
+ private final HProxySetter proxySetter;
+ private final HStateSetter stateSetter;
+ private final HConnection connection;
+ private final NitroProxyProvider proxyProvider;
+ private final NitroWebsocketServer server;
+ private final NitroPacketHandler packetHandler;
+ private final AtomicBoolean shutdownLock;
+
+ private Session activeSession = null;
+
+ public NitroWebsocketClient(HProxySetter proxySetter, HStateSetter stateSetter, HConnection connection, NitroProxyProvider proxyProvider) {
+ this.proxySetter = proxySetter;
+ this.stateSetter = stateSetter;
+ this.connection = connection;
+ this.proxyProvider = proxyProvider;
+ this.server = new NitroWebsocketServer(connection, this);
+ this.packetHandler = new NitroPacketHandler(HMessage.Direction.TOSERVER, server, connection.getExtensionHandler(), connection.getTrafficObservables());
+ this.shutdownLock = new AtomicBoolean();
+ }
+
+ @OnOpen
+ public void onOpen(Session session) throws IOException {
+ activeSession = session;
+ activeSession.setMaxBinaryMessageBufferSize(NitroConstants.WEBSOCKET_BUFFER_SIZE);
+
+ server.connect(proxyProvider.getOriginalWebsocketUrl(), proxyProvider.getOriginalOriginUrl());
+
+ final HProxy proxy = new HProxy(HClient.NITRO, "", "", -1, -1, "");
+
+ proxy.verifyProxy(
+ this.server.getPacketHandler(),
+ this.packetHandler,
+ NitroConstants.WEBSOCKET_REVISION,
+ NitroConstants.WEBSOCKET_CLIENT_IDENTIFIER
+ );
+
+ proxySetter.setProxy(proxy);
+ stateSetter.setState(HState.CONNECTED);
+ }
+
+ @OnMessage
+ public void onMessage(byte[] b, Session session) throws IOException {
+ packetHandler.act(b);
+ }
+
+ @OnClose
+ public void onClose(Session session) throws IOException {
+ activeSession = null;
+ shutdownProxy();
+ }
+
+ @OnError
+ public void onError(Session session, Throwable throwable) {
+ throwable.printStackTrace();
+
+ // Shutdown.
+ shutdownProxy();
+ }
+
+ @Override
+ public Session getSession() {
+ return activeSession;
+ }
+
+ /**
+ * Shutdown and clean up the client connection.
+ */
+ private void shutdown() {
+ if (activeSession == null) {
+ return;
+ }
+
+ try {
+ activeSession.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ activeSession = null;
+ }
+ }
+
+ /**
+ * Shutdown all connections and reset program state.
+ */
+ public void shutdownProxy() {
+ if (shutdownLock.get()) {
+ return;
+ }
+
+ if (shutdownLock.compareAndSet(false, true)) {
+ // Close client connection.
+ shutdown();
+
+ // Close server connection.
+ server.shutdown();
+
+ // Reset program state.
+ proxySetter.setProxy(null);
+ stateSetter.setState(HState.ABORTING);
+ }
+ }
+}
diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroWebsocketProxy.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroWebsocketProxy.java
new file mode 100644
index 0000000..11122d1
--- /dev/null
+++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroWebsocketProxy.java
@@ -0,0 +1,66 @@
+package gearth.protocol.connection.proxy.nitro.websocket;
+
+import gearth.protocol.HConnection;
+import gearth.protocol.connection.HProxySetter;
+import gearth.protocol.connection.HStateSetter;
+import gearth.protocol.connection.proxy.nitro.NitroConstants;
+import gearth.protocol.connection.proxy.nitro.NitroProxyProvider;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.handler.HandlerList;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer;
+
+import javax.websocket.server.ServerContainer;
+import javax.websocket.server.ServerEndpointConfig;
+
+public class NitroWebsocketProxy {
+
+ private final HProxySetter proxySetter;
+ private final HStateSetter stateSetter;
+ private final HConnection connection;
+ private final NitroProxyProvider proxyProvider;
+
+ private final Server server;
+
+ public NitroWebsocketProxy(HProxySetter proxySetter, HStateSetter stateSetter, HConnection connection, NitroProxyProvider proxyProvider) {
+ this.proxySetter = proxySetter;
+ this.stateSetter = stateSetter;
+ this.connection = connection;
+ this.proxyProvider = proxyProvider;
+ this.server = new Server(NitroConstants.WEBSOCKET_PORT);
+ }
+
+ public boolean start() {
+ try {
+ final ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
+ context.setContextPath("/");
+
+ final HandlerList handlers = new HandlerList();
+ handlers.setHandlers(new Handler[] { context });
+
+ final ServerContainer wscontainer = WebSocketServerContainerInitializer.configureContext(context);
+ wscontainer.addEndpoint(ServerEndpointConfig.Builder
+ .create(NitroWebsocketClient.class, "/")
+ .configurator(new NitroWebsocketServerConfigurator(proxySetter, stateSetter, connection, proxyProvider))
+ .build());
+
+ server.setHandler(handlers);
+ server.start();
+
+ return true;
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ return false;
+ }
+
+ public void stop() {
+ try {
+ server.stop();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroWebsocketServer.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroWebsocketServer.java
new file mode 100644
index 0000000..b54b96c
--- /dev/null
+++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroWebsocketServer.java
@@ -0,0 +1,102 @@
+package gearth.protocol.connection.proxy.nitro.websocket;
+
+import gearth.protocol.HConnection;
+import gearth.protocol.HMessage;
+import gearth.protocol.connection.proxy.nitro.NitroConstants;
+import gearth.protocol.packethandler.PacketHandler;
+
+import javax.websocket.*;
+import java.io.IOException;
+import java.net.URI;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+public class NitroWebsocketServer extends Endpoint implements NitroSession {
+
+ private final PacketHandler packetHandler;
+ private final NitroWebsocketClient client;
+ private Session activeSession = null;
+
+ public NitroWebsocketServer(HConnection connection, NitroWebsocketClient client) {
+ this.client = client;
+ this.packetHandler = new NitroPacketHandler(HMessage.Direction.TOCLIENT, client, connection.getExtensionHandler(), connection.getTrafficObservables());
+ }
+
+ public void connect(String websocketUrl, String originUrl) throws IOException {
+ try {
+ ClientEndpointConfig.Builder builder = ClientEndpointConfig.Builder.create();
+
+ if (originUrl != null) {
+ builder.configurator(new ClientEndpointConfig.Configurator() {
+ @Override
+ public void beforeRequest(Map> headers) {
+ headers.put("Origin", Collections.singletonList(originUrl));
+ }
+ });
+ }
+
+ ClientEndpointConfig config = builder.build();
+
+ ContainerProvider.getWebSocketContainer().connectToServer(this, config, URI.create(websocketUrl));
+ } catch (DeploymentException e) {
+ throw new IOException("Failed to deploy websocket client", e);
+ }
+ }
+
+ @Override
+ public void onOpen(Session session, EndpointConfig config) {
+ this.activeSession = session;
+ this.activeSession.setMaxBinaryMessageBufferSize(NitroConstants.WEBSOCKET_BUFFER_SIZE);
+ this.activeSession.addMessageHandler(new MessageHandler.Whole() {
+ @Override
+ public void onMessage(byte[] message) {
+ try {
+ packetHandler.act(message);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onClose(Session session, CloseReason closeReason) {
+ // Hotel closed connection.
+ client.shutdownProxy();
+ }
+
+ @Override
+ public void onError(Session session, Throwable throwable) {
+ throwable.printStackTrace();
+
+ // Shutdown.
+ client.shutdownProxy();
+ }
+
+ @Override
+ public Session getSession() {
+ return activeSession;
+ }
+
+ public PacketHandler getPacketHandler() {
+ return packetHandler;
+ }
+
+ /**
+ * Shutdown and clean up the server connection.
+ */
+ public void shutdown() {
+ if (activeSession == null) {
+ return;
+ }
+
+ try {
+ activeSession.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ activeSession = null;
+ }
+ }
+}
diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroWebsocketServerConfigurator.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroWebsocketServerConfigurator.java
new file mode 100644
index 0000000..8155f27
--- /dev/null
+++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroWebsocketServerConfigurator.java
@@ -0,0 +1,28 @@
+package gearth.protocol.connection.proxy.nitro.websocket;
+
+import gearth.protocol.HConnection;
+import gearth.protocol.connection.HProxySetter;
+import gearth.protocol.connection.HStateSetter;
+import gearth.protocol.connection.proxy.nitro.NitroProxyProvider;
+
+import javax.websocket.server.ServerEndpointConfig;
+
+public class NitroWebsocketServerConfigurator extends ServerEndpointConfig.Configurator {
+
+ private final HProxySetter proxySetter;
+ private final HStateSetter stateSetter;
+ private final HConnection connection;
+ private final NitroProxyProvider proxyProvider;
+
+ public NitroWebsocketServerConfigurator(HProxySetter proxySetter, HStateSetter stateSetter, HConnection connection, NitroProxyProvider proxyProvider) {
+ this.proxySetter = proxySetter;
+ this.stateSetter = stateSetter;
+ this.connection = connection;
+ this.proxyProvider = proxyProvider;
+ }
+
+ @Override
+ public T getEndpointInstance(Class endpointClass) {
+ return (T) new NitroWebsocketClient(proxySetter, stateSetter, connection, proxyProvider);
+ }
+}
diff --git a/G-Earth/src/main/java/gearth/protocol/packethandler/PacketHandler.java b/G-Earth/src/main/java/gearth/protocol/packethandler/PacketHandler.java
index 1336b7d..a0d2677 100644
--- a/G-Earth/src/main/java/gearth/protocol/packethandler/PacketHandler.java
+++ b/G-Earth/src/main/java/gearth/protocol/packethandler/PacketHandler.java
@@ -20,7 +20,7 @@ public abstract class PacketHandler {
}
- public abstract void sendToStream(byte[] buffer);
+ public abstract boolean sendToStream(byte[] buffer);
public abstract void act(byte[] buffer) throws IOException;
diff --git a/G-Earth/src/main/java/gearth/protocol/packethandler/flash/FlashPacketHandler.java b/G-Earth/src/main/java/gearth/protocol/packethandler/flash/FlashPacketHandler.java
index 44c2e59..018bca1 100644
--- a/G-Earth/src/main/java/gearth/protocol/packethandler/flash/FlashPacketHandler.java
+++ b/G-Earth/src/main/java/gearth/protocol/packethandler/flash/FlashPacketHandler.java
@@ -51,7 +51,9 @@ public abstract class FlashPacketHandler extends PacketHandler {
public void act(byte[] buffer) throws IOException {
if (!isDataStream) {
- out.write(buffer);
+ synchronized (sendLock) {
+ out.write(buffer);
+ }
return;
}
@@ -113,7 +115,7 @@ public abstract class FlashPacketHandler extends PacketHandler {
isTempBlocked = false;
}
- public void sendToStream(byte[] buffer) {
+ public boolean sendToStream(byte[] buffer) {
synchronized (sendLock) {
try {
out.write(
@@ -121,8 +123,10 @@ public abstract class FlashPacketHandler extends PacketHandler {
? buffer
: encryptcipher.rc4(buffer)
);
+ return true;
} catch (IOException e) {
e.printStackTrace();
+ return false;
}
}
}
diff --git a/G-Earth/src/main/java/gearth/protocol/packethandler/unity/UnityPacketHandler.java b/G-Earth/src/main/java/gearth/protocol/packethandler/unity/UnityPacketHandler.java
index 2455bbc..6bcca62 100644
--- a/G-Earth/src/main/java/gearth/protocol/packethandler/unity/UnityPacketHandler.java
+++ b/G-Earth/src/main/java/gearth/protocol/packethandler/unity/UnityPacketHandler.java
@@ -23,11 +23,12 @@ public class UnityPacketHandler extends PacketHandler {
}
@Override
- public void sendToStream(byte[] buffer) {
+ public boolean sendToStream(byte[] buffer) {
byte[] prefix = new byte[]{(direction == HMessage.Direction.TOCLIENT ? ((byte)0) : ((byte)1))};
byte[] combined = ByteArrayUtils.combineByteArrays(prefix, buffer);
session.getAsyncRemote().sendBinary(ByteBuffer.wrap(combined));
+ return true;
}
@Override
diff --git a/G-Earth/src/main/java/gearth/services/packet_info/PacketInfoManager.java b/G-Earth/src/main/java/gearth/services/packet_info/PacketInfoManager.java
index 27904b4..af1fda8 100644
--- a/G-Earth/src/main/java/gearth/services/packet_info/PacketInfoManager.java
+++ b/G-Earth/src/main/java/gearth/services/packet_info/PacketInfoManager.java
@@ -114,8 +114,7 @@ public class PacketInfoManager {
if (clientType == HClient.UNITY) {
result.addAll(new GEarthUnityPacketInfoProvider(hotelversion).provide());
- }
- else if (clientType == HClient.FLASH) {
+ } else if (clientType == HClient.FLASH || clientType == HClient.NITRO) {
try {
List providers = new ArrayList<>();
providers.add(new HarblePacketInfoProvider(hotelversion));
diff --git a/G-Earth/src/main/java/gearth/services/unity_tools/codepatcher/IncomingPacketPatcher.java b/G-Earth/src/main/java/gearth/services/unity_tools/codepatcher/IncomingPacketPatcher.java
index 48e269b..b3482cd 100644
--- a/G-Earth/src/main/java/gearth/services/unity_tools/codepatcher/IncomingPacketPatcher.java
+++ b/G-Earth/src/main/java/gearth/services/unity_tools/codepatcher/IncomingPacketPatcher.java
@@ -7,7 +7,6 @@ import wasm.disassembly.modules.sections.code.Locals;
import wasm.disassembly.types.FuncType;
import wasm.disassembly.types.ResultType;
import wasm.disassembly.types.ValType;
-import wasm.misc.CodeCompare;
import wasm.misc.StreamReplacement;
import java.util.Arrays;
@@ -38,23 +37,23 @@ public class IncomingPacketPatcher implements StreamReplacement {
}
@Override
- public CodeCompare getCodeCompare() {
- return code -> {
- if (!(code.getLocalss().equals(Collections.singletonList(new Locals(1, ValType.I32)))))
- return false;
+ public boolean codeMatches(Func code) {
+ if (!(code.getLocalss().equals(Collections.singletonList(new Locals(1, ValType.I32)))))
+ return false;
- List expectedExpr = Arrays.asList(InstrType.I32_CONST, InstrType.I32_LOAD8_S,
- InstrType.I32_EQZ, InstrType.IF, InstrType.LOCAL_GET, InstrType.I32_LOAD, InstrType.LOCAL_TEE,
- InstrType.IF);
+ List expectedExpr = Arrays.asList(InstrType.I32_CONST, InstrType.I32_LOAD8_S,
+ InstrType.I32_EQZ, InstrType.IF, InstrType.LOCAL_GET, InstrType.I32_LOAD, InstrType.LOCAL_TEE,
+ InstrType.IF);
- if (code.getExpression().getInstructions().size() != expectedExpr.size()) return false;
+ if (code.getExpression().getInstructions().size() != expectedExpr.size()) return false;
- for (int j = 0; j < code.getExpression().getInstructions().size(); j++) {
- Instr instr = code.getExpression().getInstructions().get(j);
- if (instr.getInstrType() != expectedExpr.get(j)) return false;
- }
+ for (int j = 0; j < code.getExpression().getInstructions().size(); j++) {
+ Instr instr = code.getExpression().getInstructions().get(j);
+ if (instr.getInstrType() != expectedExpr.get(j)) return false;
+ }
- return true;
- };
+ return true;
}
+
+
}
diff --git a/G-Earth/src/main/java/gearth/services/unity_tools/codepatcher/OutgoingPacketPatcher.java b/G-Earth/src/main/java/gearth/services/unity_tools/codepatcher/OutgoingPacketPatcher.java
index 86921e3..026fee3 100644
--- a/G-Earth/src/main/java/gearth/services/unity_tools/codepatcher/OutgoingPacketPatcher.java
+++ b/G-Earth/src/main/java/gearth/services/unity_tools/codepatcher/OutgoingPacketPatcher.java
@@ -6,7 +6,6 @@ import wasm.disassembly.modules.sections.code.Func;
import wasm.disassembly.types.FuncType;
import wasm.disassembly.types.ResultType;
import wasm.disassembly.types.ValType;
-import wasm.misc.CodeCompare;
import wasm.misc.StreamReplacement;
import java.util.Arrays;
@@ -36,18 +35,16 @@ public class OutgoingPacketPatcher implements StreamReplacement {
}
@Override
- public CodeCompare getCodeCompare() {
- return code -> {
- if (code.getLocalss().size() != 0) return false;
- List expression = code.getExpression().getInstructions();
- if (expression.get(0).getInstrType() != InstrType.LOCAL_GET) return false;
- if (expression.get(1).getInstrType() != InstrType.LOCAL_GET) return false;
- if (expression.get(2).getInstrType() != InstrType.LOCAL_GET) return false;
- if (expression.get(3).getInstrType() != InstrType.I32_LOAD) return false;
- if (expression.get(4).getInstrType() != InstrType.I32_CONST) return false;
- if (expression.get(5).getInstrType() != InstrType.CALL) return false;
+ public boolean codeMatches(Func code) {
+ if (code.getLocalss().size() != 0) return false;
+ List expression = code.getExpression().getInstructions();
+ if (expression.get(0).getInstrType() != InstrType.LOCAL_GET) return false;
+ if (expression.get(1).getInstrType() != InstrType.LOCAL_GET) return false;
+ if (expression.get(2).getInstrType() != InstrType.LOCAL_GET) return false;
+ if (expression.get(3).getInstrType() != InstrType.I32_LOAD) return false;
+ if (expression.get(4).getInstrType() != InstrType.I32_CONST) return false;
+ if (expression.get(5).getInstrType() != InstrType.CALL) return false;
- return true;
- };
+ return true;
}
}
diff --git a/G-Earth/src/main/java/gearth/services/unity_tools/codepatcher/ReturnBytePatcher.java b/G-Earth/src/main/java/gearth/services/unity_tools/codepatcher/ReturnBytePatcher.java
index 4d90c5c..6f8040c 100644
--- a/G-Earth/src/main/java/gearth/services/unity_tools/codepatcher/ReturnBytePatcher.java
+++ b/G-Earth/src/main/java/gearth/services/unity_tools/codepatcher/ReturnBytePatcher.java
@@ -6,7 +6,6 @@ import wasm.disassembly.modules.sections.code.Func;
import wasm.disassembly.types.FuncType;
import wasm.disassembly.types.ResultType;
import wasm.disassembly.types.ValType;
-import wasm.misc.CodeCompare;
import wasm.misc.StreamReplacement;
import java.util.Arrays;
@@ -36,13 +35,11 @@ public class ReturnBytePatcher implements StreamReplacement {
}
@Override
- public CodeCompare getCodeCompare() {
- return code -> {
- if (code.getLocalss().size() != 0) return false;
- if (code.getExpression().getInstructions().size() != 30) return false;
- List expr = code.getExpression().getInstructions();
- if (expr.get(expr.size() - 1).getInstrType() != InstrType.I32_XOR) return false;
- return true;
- };
+ public boolean codeMatches(Func code) {
+ if (code.getLocalss().size() != 0) return false;
+ if (code.getExpression().getInstructions().size() != 30) return false;
+ List expr = code.getExpression().getInstructions();
+ if (expr.get(expr.size() - 1).getInstrType() != InstrType.I32_XOR) return false;
+ return true;
}
}
diff --git a/G-Earth/src/main/java/gearth/services/unity_tools/codepatcher/SetKeyPatcher.java b/G-Earth/src/main/java/gearth/services/unity_tools/codepatcher/SetKeyPatcher.java
index f952105..41fab8a 100644
--- a/G-Earth/src/main/java/gearth/services/unity_tools/codepatcher/SetKeyPatcher.java
+++ b/G-Earth/src/main/java/gearth/services/unity_tools/codepatcher/SetKeyPatcher.java
@@ -7,7 +7,6 @@ import wasm.disassembly.modules.sections.code.Locals;
import wasm.disassembly.types.FuncType;
import wasm.disassembly.types.ResultType;
import wasm.disassembly.types.ValType;
-import wasm.misc.CodeCompare;
import wasm.misc.StreamReplacement;
import java.util.Arrays;
@@ -38,23 +37,21 @@ public class SetKeyPatcher implements StreamReplacement {
}
@Override
- public CodeCompare getCodeCompare() {
- return code -> {
- if (!(code.getLocalss().equals(Collections.singletonList(new Locals(1, ValType.I32)))))
- return false;
- List expectedExpr = Arrays.asList(InstrType.I32_CONST, InstrType.I32_LOAD8_S,
- InstrType.I32_EQZ, InstrType.IF, InstrType.BLOCK, InstrType.LOCAL_GET, InstrType.I32_CONST,
- InstrType.LOCAL_GET, InstrType.I32_LOAD, InstrType.I32_CONST, InstrType.I32_CONST, InstrType.I32_CONST,
- InstrType.CALL);
+ public boolean codeMatches(Func code) {
+ if (!(code.getLocalss().equals(Collections.singletonList(new Locals(1, ValType.I32)))))
+ return false;
+ List expectedExpr = Arrays.asList(InstrType.I32_CONST, InstrType.I32_LOAD8_S,
+ InstrType.I32_EQZ, InstrType.IF, InstrType.BLOCK, InstrType.LOCAL_GET, InstrType.I32_CONST,
+ InstrType.LOCAL_GET, InstrType.I32_LOAD, InstrType.I32_CONST, InstrType.I32_CONST, InstrType.I32_CONST,
+ InstrType.CALL);
- if (code.getExpression().getInstructions().size() != expectedExpr.size()) return false;
+ if (code.getExpression().getInstructions().size() != expectedExpr.size()) return false;
- for (int j = 0; j < code.getExpression().getInstructions().size(); j++) {
- Instr instr = code.getExpression().getInstructions().get(j);
- if (instr.getInstrType() != expectedExpr.get(j)) return false;
- }
+ for (int j = 0; j < code.getExpression().getInstructions().size(); j++) {
+ Instr instr = code.getExpression().getInstructions().get(j);
+ if (instr.getInstrType() != expectedExpr.get(j)) return false;
+ }
- return true;
- };
+ return true;
}
}
diff --git a/G-Earth/src/main/java/gearth/ui/connection/ConnectionController.java b/G-Earth/src/main/java/gearth/ui/connection/ConnectionController.java
index 287d70a..b908666 100644
--- a/G-Earth/src/main/java/gearth/ui/connection/ConnectionController.java
+++ b/G-Earth/src/main/java/gearth/ui/connection/ConnectionController.java
@@ -2,6 +2,7 @@ package gearth.ui.connection;
import gearth.Main;
import gearth.misc.Cacher;
+import gearth.protocol.connection.HClient;
import gearth.protocol.connection.HState;
import gearth.protocol.connection.proxy.ProxyProviderFactory;
import gearth.services.Constants;
@@ -38,10 +39,11 @@ public class ConnectionController extends SubForm {
private volatile int fullyInitialized = 0;
- public static final String USE_UNITY_CLIENT_CACHE_KEY = "use_unity";
+ public static final String CLIENT_CACHE_KEY = "last_client_mode";
public ToggleGroup tgl_clientMode;
public RadioButton rd_unity;
public RadioButton rd_flash;
+ public RadioButton rd_nitro;
public GridPane grd_clientSelection;
private volatile int initcount = 0;
@@ -54,9 +56,18 @@ public class ConnectionController extends SubForm {
Constants.UNITY_PACKETS = rd_unity.isSelected();
});
- if (Cacher.getCacheContents().has(USE_UNITY_CLIENT_CACHE_KEY)) {
- rd_unity.setSelected(Cacher.getCacheContents().getBoolean(USE_UNITY_CLIENT_CACHE_KEY));
- rd_flash.setSelected(!Cacher.getCacheContents().getBoolean(USE_UNITY_CLIENT_CACHE_KEY));
+ if (Cacher.getCacheContents().has(CLIENT_CACHE_KEY)) {
+ switch (Cacher.getCacheContents().getEnum(HClient.class, CLIENT_CACHE_KEY)) {
+ case FLASH:
+ rd_flash.setSelected(true);
+ break;
+ case UNITY:
+ rd_unity.setSelected(true);
+ break;
+ case NITRO:
+ rd_nitro.setSelected(true);
+ break;
+ }
}
@@ -235,6 +246,10 @@ public class ConnectionController extends SubForm {
Platform.runLater(() -> rd_unity.setSelected(true));
getHConnection().startUnity();
}
+ else if (connectMode.equals("nitro")) {
+ Platform.runLater(() -> rd_nitro.setSelected(true));
+ getHConnection().startNitro();
+ }
Platform.runLater(this::updateInputUI);
}
}
@@ -244,16 +259,16 @@ public class ConnectionController extends SubForm {
btnConnect.setDisable(true);
new Thread(() -> {
- if (useFlash()) {
+ if (isClientMode(HClient.FLASH)) {
if (cbx_autodetect.isSelected()) {
getHConnection().start();
- }
- else {
+ } else {
getHConnection().start(inpHost.getEditor().getText(), Integer.parseInt(inpPort.getEditor().getText()));
}
- }
- else {
+ } else if (isClientMode(HClient.UNITY)) {
getHConnection().startUnity();
+ } else if (isClientMode(HClient.NITRO)) {
+ getHConnection().startNitro();
}
@@ -269,7 +284,13 @@ public class ConnectionController extends SubForm {
@Override
protected void onExit() {
- Cacher.put(USE_UNITY_CLIENT_CACHE_KEY, rd_unity.isSelected());
+ if (rd_flash.isSelected()) {
+ Cacher.put(CLIENT_CACHE_KEY, HClient.FLASH);
+ } else if (rd_unity.isSelected()) {
+ Cacher.put(CLIENT_CACHE_KEY, HClient.UNITY);
+ } else if (rd_nitro.isSelected()) {
+ Cacher.put(CLIENT_CACHE_KEY, HClient.NITRO);
+ }
getHConnection().abort();
}
@@ -280,4 +301,17 @@ public class ConnectionController extends SubForm {
private boolean useFlash() {
return rd_flash.isSelected();
}
+
+ private boolean isClientMode(HClient client) {
+ switch (client) {
+ case FLASH:
+ return rd_flash.isSelected();
+ case UNITY:
+ return rd_unity.isSelected();
+ case NITRO:
+ return rd_nitro.isSelected();
+ }
+
+ return false;
+ }
}
diff --git a/G-Earth/src/main/resources/gearth/ui/connection/Connection.fxml b/G-Earth/src/main/resources/gearth/ui/connection/Connection.fxml
index 96b87e5..16982b0 100644
--- a/G-Earth/src/main/resources/gearth/ui/connection/Connection.fxml
+++ b/G-Earth/src/main/resources/gearth/ui/connection/Connection.fxml
@@ -5,7 +5,7 @@
-
+
@@ -132,9 +132,9 @@
-
-
-
+
+
+
@@ -156,7 +156,13 @@
-
+
+
+
+
+
+
+