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; + } }