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 @@ - + + + + + + +