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 09b1b80..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
+
diff --git a/G-Earth/src/main/java/gearth/protocol/HConnection.java b/G-Earth/src/main/java/gearth/protocol/HConnection.java
index db1c083..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) {
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..5ba1172
--- /dev/null
+++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/NitroProxyProvider.java
@@ -0,0 +1,53 @@
+package gearth.protocol.connection.proxy.nitro;
+
+import gearth.protocol.HConnection;
+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 java.io.IOException;
+
+public class NitroProxyProvider implements ProxyProvider {
+
+ private final HProxySetter proxySetter;
+ private final HStateSetter stateSetter;
+ private final HConnection hConnection;
+ private final NitroHttpProxy nitroProxy;
+
+ public NitroProxyProvider(HProxySetter proxySetter, HStateSetter stateSetter, HConnection hConnection) {
+ this.proxySetter = proxySetter;
+ this.stateSetter = stateSetter;
+ this.hConnection = hConnection;
+ this.nitroProxy = new NitroHttpProxy();
+ }
+
+ @Override
+ public void start() throws IOException {
+ if (!nitroProxy.start()) {
+ System.out.println("Failed to start nitro proxy");
+
+ stateSetter.setState(HState.NOT_CONNECTED);
+ return;
+ }
+
+ stateSetter.setState(HState.WAITING_FOR_CLIENT);
+ }
+
+ @Override
+ public void abort() {
+ stateSetter.setState(HState.ABORTING);
+
+ new Thread(() -> {
+ try {
+ nitroProxy.stop();
+ } catch (Exception e) {
+ e.printStackTrace();
+ } finally {
+ stateSetter.setState(HState.NOT_CONNECTED);
+ }
+ }).start();
+ }
+
+}
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/NitroHttpProxy.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxy.java
new file mode 100644
index 0000000..13acc03
--- /dev/null
+++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxy.java
@@ -0,0 +1,89 @@
+package gearth.protocol.connection.proxy.nitro.http;
+
+import gearth.protocol.connection.proxy.nitro.os.NitroOsFunctions;
+import gearth.protocol.connection.proxy.nitro.os.NitroOsFunctionsFactory;
+import org.littleshoot.proxy.HttpProxyServer;
+import org.littleshoot.proxy.impl.DefaultHttpProxyServer;
+import org.littleshoot.proxy.mitm.Authority;
+import org.littleshoot.proxy.mitm.CertificateSniffingMitmManager;
+import org.littleshoot.proxy.mitm.RootCertificateException;
+
+public class NitroHttpProxy {
+
+ private final Authority authority;
+ private final NitroOsFunctions osFunctions;
+
+ private HttpProxyServer proxyServer = null;
+
+ public NitroHttpProxy() {
+ this.authority = new NitroAuthority();
+ this.osFunctions = NitroOsFunctionsFactory.create();
+ }
+
+ private boolean initializeCertificate() {
+ 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", 9090);
+ }
+
+ /**
+ * Unregister HTTP(s) proxy from system.
+ */
+ private boolean unregisterProxy() {
+ return this.osFunctions.unregisterSystemProxy();
+ }
+
+ public boolean start() {
+
+
+ try {
+ proxyServer = DefaultHttpProxyServer.bootstrap()
+ .withPort(9090)
+ .withManInTheMiddle(new CertificateSniffingMitmManager(authority))
+ // TODO: Replace lambda with some class
+ .withFiltersSource(new NitroHttpProxyFilterSource((configUrl, websocketUrl) -> {
+ System.out.printf("Found %s at %s%n", websocketUrl, configUrl);
+
+ return "wss://127.0.0.1:2096";
+ }))
+ .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 stop() {
+ if (!unregisterProxy()) {
+ System.out.println("Failed to unregister system proxy, please check manually");
+ }
+
+ if (proxyServer == null) {
+ return;
+ }
+
+ proxyServer.stop();
+ proxyServer = null;
+ }
+}
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..28a33ae
--- /dev/null
+++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxyFilter.java
@@ -0,0 +1,124 @@
+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 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 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);
+ }
+ }
+
+ return httpObject;
+ }
+
+ 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..fae9deb
--- /dev/null
+++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxyFilterSource.java
@@ -0,0 +1,46 @@
+package gearth.protocol.connection.proxy.nitro.http;
+
+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 1024 * 1024 * 1024;
+ }
+}
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..d77a4c6
--- /dev/null
+++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/os/NitroOsFunctions.java
@@ -0,0 +1,13 @@
+package gearth.protocol.connection.proxy.nitro.os;
+
+import java.io.File;
+
+public interface NitroOsFunctions {
+
+ 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..27c7b12
--- /dev/null
+++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/os/windows/NitroWindows.java
@@ -0,0 +1,42 @@
+package gearth.protocol.connection.proxy.nitro.os.windows;
+
+import gearth.protocol.connection.proxy.nitro.os.NitroOsFunctions;
+
+import java.io.File;
+
+public class NitroWindows implements NitroOsFunctions {
+
+ @Override
+ public boolean installRootCertificate(File certificate) {
+ // TODO: Prompt registration
+ System.out.println(certificate.toString());
+ 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 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/ui/connection/ConnectionController.java b/G-Earth/src/main/java/gearth/ui/connection/ConnectionController.java
index 3a559da..b908666 100644
--- a/G-Earth/src/main/java/gearth/ui/connection/ConnectionController.java
+++ b/G-Earth/src/main/java/gearth/ui/connection/ConnectionController.java
@@ -246,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);
}
}
@@ -255,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();
}
@@ -297,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;
+ }
}