From fe97142bfec79beb9e9705a0d2dbd5da08e2835b Mon Sep 17 00:00:00 2001 From: UnfamiliarLegacy <74633542+UnfamiliarLegacy@users.noreply.github.com> Date: Wed, 23 Oct 2024 06:15:54 +0200 Subject: [PATCH] Nitro http proxy rewrite to proxyee progress --- .gitignore | 4 +- G-Earth/pom.xml | 32 ++- G-Earth/src/main/java/gearth/GEarth.java | 4 +- .../proxy/nitro/NitroConstants.java | 18 ++ .../proxy/nitro/NitroProxyProvider.java | 10 +- .../proxy/nitro/http/NitroAuthority.java | 23 +- .../nitro/http/NitroCertificateFactory.java | 114 ++++++++++ .../http/NitroCertificateSniffingManager.java | 118 ---------- .../proxy/nitro/http/NitroHttpProxy.java | 104 ++++++--- .../nitro/http/NitroHttpProxyFilter.java | 203 ------------------ .../http/NitroHttpProxyFilterSource.java | 47 ---- .../nitro/http/NitroHttpProxyIntercept.java | 35 +++ .../nitro/http/NitroSslContextFactory.java | 8 +- .../proxy/nitro/os/macos/NitroMacOS.java | 2 +- .../proxy/nitro/os/windows/NitroWindows.java | 42 ++-- .../nitro/websocket/NitroWebsocketProxy.java | 10 +- pom.xml | 2 +- 17 files changed, 311 insertions(+), 465 deletions(-) create mode 100644 G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroCertificateFactory.java delete mode 100644 G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroCertificateSniffingManager.java delete mode 100644 G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxyFilter.java delete mode 100644 G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxyFilterSource.java create mode 100644 G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxyIntercept.java diff --git a/.gitignore b/.gitignore index a2e2560..85bd273 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,6 @@ Extensions/BlockReplacePackets/.classpath # Certificates *.p12 -*.pem \ No newline at end of file +*.pem +*.crt +*.key \ No newline at end of file diff --git a/G-Earth/pom.xml b/G-Earth/pom.xml index 79c31b4..13dc8cf 100644 --- a/G-Earth/pom.xml +++ b/G-Earth/pom.xml @@ -313,20 +313,38 @@ 1.5.0 - com.github.ganskef - littleproxy-mitm - 1.1.0 + com.github.monkeywie + proxyee + 1.7.6 - org.slf4j - slf4j-api + org.bouncycastle + bcprov-jdk15on - org.slf4j - slf4j-log4j12 + org.bouncycastle + bcpkix-jdk15on + + + org.bouncycastle + bcprov-jdk18on + 1.78.1 + + + + org.bouncycastle + bcpkix-jdk18on + 1.78.1 + + + + org.bouncycastle + bctls-jdk18on + 1.78.1 + G-Earth G-Wasm-Minimal diff --git a/G-Earth/src/main/java/gearth/GEarth.java b/G-Earth/src/main/java/gearth/GEarth.java index a9eccc0..e04c271 100644 --- a/G-Earth/src/main/java/gearth/GEarth.java +++ b/G-Earth/src/main/java/gearth/GEarth.java @@ -122,8 +122,8 @@ public class GEarth extends Application { primaryStage.show(); primaryStage.setOnCloseRequest(event -> closeGEarth()); - AdminValidator.validate(); - UpdateChecker.checkForUpdates(); + //AdminValidator.validate(); + //UpdateChecker.checkForUpdates(); } 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 index a6b8fd7..8a7d095 100644 --- 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 @@ -9,4 +9,22 @@ public final class NitroConstants { public static final String WEBSOCKET_REVISION = "PRODUCTION-201611291003-338511768"; public static final String WEBSOCKET_CLIENT_IDENTIFIER = "HTML5"; + public static final String[] CIPHER_SUITES = new String[] { + "TLS_AES_128_GCM_SHA256", + "TLS_AES_256_GCM_SHA384", + "TLS_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_RSA_WITH_AES_128_GCM_SHA256", + "TLS_RSA_WITH_AES_256_GCM_SHA384", + "TLS_RSA_WITH_AES_128_CBC_SHA", + "TLS_RSA_WITH_AES_256_CBC_SHA" + }; + } 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 index 94d6869..90ea41a 100644 --- 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 @@ -6,8 +6,7 @@ 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.NitroAuthority; -import gearth.protocol.connection.proxy.nitro.http.NitroCertificateSniffingManager; +import gearth.protocol.connection.proxy.nitro.http.NitroCertificateFactory; import gearth.protocol.connection.proxy.nitro.http.NitroHttpProxy; import gearth.protocol.connection.proxy.nitro.http.NitroHttpProxyServerCallback; import gearth.protocol.connection.proxy.nitro.websocket.NitroWebsocketProxy; @@ -33,8 +32,7 @@ public class NitroProxyProvider implements ProxyProvider, NitroHttpProxyServerCa private String originalCookies; public NitroProxyProvider(HProxySetter proxySetter, HStateSetter stateSetter, HConnection connection) { - final NitroAuthority authority = new NitroAuthority(); - final NitroCertificateSniffingManager certificateManager = new NitroCertificateSniffingManager(authority); + final NitroCertificateFactory certificateManager = new NitroCertificateFactory(); this.proxySetter = proxySetter; this.stateSetter = stateSetter; @@ -103,7 +101,7 @@ public class NitroProxyProvider implements ProxyProvider, NitroHttpProxyServerCa try { nitroHttpProxy.stop(); } catch (Exception e) { - e.printStackTrace(); + logger.error("Failed to stop nitro http proxy", e); } logger.info("Stopping nitro websocket proxy"); @@ -111,7 +109,7 @@ public class NitroProxyProvider implements ProxyProvider, NitroHttpProxyServerCa try { nitroWebsocketProxy.stop(); } catch (Exception e) { - e.printStackTrace(); + logger.error("Failed to stop nitro websocket proxy", e); } stateSetter.setState(HState.NOT_CONNECTED); 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 index fd5bd4e..5e6d25f 100644 --- 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 @@ -1,24 +1,9 @@ package gearth.protocol.connection.proxy.nitro.http; -import org.littleshoot.proxy.mitm.Authority; +public class NitroAuthority { -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); - } + public static final String CERT_ALIAS = "gearth-nitro-v2"; + public static final String CERT_ORGANIZATION = "G-Earth Nitro"; + public static final String CERT_DESCRIPTION = "G-Earth nitro support v2"; } diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroCertificateFactory.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroCertificateFactory.java new file mode 100644 index 0000000..c18604c --- /dev/null +++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroCertificateFactory.java @@ -0,0 +1,114 @@ +package gearth.protocol.connection.proxy.nitro.http; + +import com.github.monkeywie.proxyee.crt.CertUtil; +import com.github.monkeywie.proxyee.server.HttpProxyCACertFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.SSLEngine; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Date; +import java.util.concurrent.TimeUnit; + +public class NitroCertificateFactory implements HttpProxyCACertFactory { + + private static final Logger log = LoggerFactory.getLogger(NitroCertificateFactory.class); + + private final File caCertFile; + private final File caKeyFile; + + private X509Certificate caCert; + private PrivateKey caKey; + + public NitroCertificateFactory() { + this.caCertFile = new File(String.format("./%s.crt", NitroAuthority.CERT_ALIAS)); + this.caKeyFile = new File(String.format("./%s.key", NitroAuthority.CERT_ALIAS)); + } + + public File getCaCertFile() { + return caCertFile; + } + + public File getCaKeyFile() { + return caKeyFile; + } + + public boolean loadOrCreate() { + if (this.caCertFile.exists() && this.caKeyFile.exists()) { + return this.loadCertificate(); + } + + // Delete any existing files + if (caCertFile.exists()) { + caCertFile.delete(); + } + + if (caKeyFile.exists()) { + caKeyFile.delete(); + } + + // Create the certificate and key files + return this.createCertificate(); + } + + private boolean createCertificate() { + try { + final KeyPair keyPair = CertUtil.genKeyPair(); + + final String subject = String.format("O=%s, OU=Certificate Authority, CN=%s", + NitroAuthority.CERT_ORGANIZATION, + NitroAuthority.CERT_DESCRIPTION); + + final X509Certificate rootCertificate = CertUtil.genCACert(subject, + new Date(), + new Date(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(3650)), + keyPair); + + this.caCert = rootCertificate; + this.caKey = keyPair.getPrivate(); + + Files.write(Paths.get(this.caCertFile.toURI()), this.caCert.getEncoded()); + Files.write(Paths.get(this.caKeyFile.toURI()), new PKCS8EncodedKeySpec(this.caKey.getEncoded()).getEncoded()); + + return true; + } catch (Exception e) { + log.error("Failed to create root certificate", e); + } + + return false; + } + + private boolean loadCertificate() { + try { + this.caCert = CertUtil.loadCert(this.caCertFile.toURI()); + this.caKey = CertUtil.loadPriKey(this.caKeyFile.toURI()); + + return true; + } catch (Exception e) { + log.error("Failed to load root certificate", e); + } + + return false; + } + + @Override + public X509Certificate getCACert() { + return this.caCert; + } + + @Override + public PrivateKey getCAPriKey() { + return this.caKey; + } + + public SSLEngine websocketSslEngine(String host) { + throw new UnsupportedOperationException("Not implemented"); + } + +} 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 deleted file mode 100644 index c5c3d63..0000000 --- a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroCertificateSniffingManager.java +++ /dev/null @@ -1,118 +0,0 @@ -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; - private final Authority authority; - - public NitroCertificateSniffingManager(Authority authority) { - this.authority = authority; - try { - sslEngineSource = new BouncyCastleSslEngineSource(authority, true, true, null); - } catch (final Exception e) { - throw new RuntimeException(new RootCertificateException("Errors during assembling root CA.", e)); - } - } - - public Authority getAuthority() { - return authority; - } - - public SSLEngine websocketSslEngine(String commonName) { - final SubjectAlternativeNameHolder san = new SubjectAlternativeNameHolder(); - - san.addDomainName("localhost"); - san.addIpAddress("127.0.0.1"); - - try { - return sslEngineSource.createCertForHost(commonName, san); - } catch (Exception e) { - throw new FakeCertificateException("Failed to create WebSocket certificate", 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 index cb7e0ba..7afc683 100644 --- 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 @@ -1,42 +1,57 @@ package gearth.protocol.connection.proxy.nitro.http; +import com.github.monkeywie.proxyee.server.HttpProxyServer; +import com.github.monkeywie.proxyee.server.HttpProxyServerConfig; 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 gearth.ui.titlebar.TitleBarController; import gearth.ui.translations.LanguageBundle; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; import javafx.application.Platform; import javafx.scene.control.Alert; import javafx.scene.control.ButtonType; import javafx.scene.control.Label; -import org.littleshoot.proxy.HttpProxyServer; -import org.littleshoot.proxy.impl.DefaultHttpProxyServer; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import javax.net.ssl.SSLException; import java.io.File; -import java.io.IOException; +import java.security.Security; +import java.util.Arrays; +import java.util.HashSet; import java.util.concurrent.Semaphore; import java.util.concurrent.atomic.AtomicBoolean; public class NitroHttpProxy { + private static final Logger log = LoggerFactory.getLogger(NitroHttpProxy.class); + private static final String ADMIN_WARNING_KEY = "admin_warning_dialog"; private static final AtomicBoolean SHUTDOWN_HOOK = new AtomicBoolean(); private final NitroOsFunctions osFunctions; private final NitroHttpProxyServerCallback serverCallback; - private final NitroCertificateSniffingManager certificateManager; + private final NitroCertificateFactory certificateFactory; private HttpProxyServer proxyServer = null; - public NitroHttpProxy(NitroHttpProxyServerCallback serverCallback, NitroCertificateSniffingManager certificateManager) { + public NitroHttpProxy(NitroHttpProxyServerCallback serverCallback, NitroCertificateFactory certificateManager) { this.serverCallback = serverCallback; - this.certificateManager = certificateManager; + this.certificateFactory = certificateManager; this.osFunctions = NitroOsFunctionsFactory.create(); } private boolean initializeCertificate() { - final File certificate = this.certificateManager.getAuthority().aliasFile(".pem"); + if (!this.certificateFactory.loadOrCreate()) { + return false; + } + + final File certificate = this.certificateFactory.getCaCertFile(); // All good if certificate is already trusted. if (this.osFunctions.isRootCertificateTrusted(certificate)) { @@ -48,28 +63,31 @@ public class NitroHttpProxy { final AtomicBoolean shouldInstall = new AtomicBoolean(); Platform.runLater(() -> { - Alert alert = ConfirmationDialog.createAlertWithOptOut(Alert.AlertType.WARNING, ADMIN_WARNING_KEY, - LanguageBundle.get("alert.rootcertificate.title"), null, - "", LanguageBundle.get("alert.rootcertificate.remember"), - ButtonType.YES, ButtonType.NO - ); - - alert.getDialogPane().setContent(new Label(LanguageBundle.get("alert.rootcertificate.content").replaceAll("\\\\n", System.lineSeparator()))); - try { + Alert alert = ConfirmationDialog.createAlertWithOptOut(Alert.AlertType.WARNING, ADMIN_WARNING_KEY, + LanguageBundle.get("alert.rootcertificate.title"), null, + "", LanguageBundle.get("alert.rootcertificate.remember"), + ButtonType.YES, ButtonType.NO + ); + + alert.getDialogPane().setContent(new Label(LanguageBundle.get("alert.rootcertificate.content").replaceAll("\\\\n", System.lineSeparator()))); + + log.debug("Showing certificate install dialog"); + shouldInstall.set(TitleBarController.create(alert).showAlertAndWait() .filter(t -> t == ButtonType.YES).isPresent()); - } catch (IOException e) { - e.printStackTrace(); + } catch (Exception e) { + log.error("Failed to show install dialog", e); + } finally { + waitForDialog.release(); } - waitForDialog.release(); }); // Wait for dialog choice. try { waitForDialog.acquire(); } catch (InterruptedException e) { - e.printStackTrace(); + log.error("Interrupted while waiting for user input", e); return false; } @@ -78,7 +96,7 @@ public class NitroHttpProxy { return false; } - return this.osFunctions.installRootCertificate(this.certificateManager.getAuthority().aliasFile(".pem")); + return this.osFunctions.installRootCertificate(this.certificateFactory.getCaCertFile()); } /** @@ -98,24 +116,44 @@ public class NitroHttpProxy { public boolean start() { setupShutdownHook(); - proxyServer = DefaultHttpProxyServer.bootstrap() - .withPort(NitroConstants.HTTP_PORT) - .withManInTheMiddle(this.certificateManager) - .withFiltersSource(new NitroHttpProxyFilterSource(serverCallback)) - .withTransparent(true) - .start(); - if (!initializeCertificate()) { - proxyServer.stop(); + log.error("Failed to initialize certificate"); + return false; + } - System.out.println("Failed to initialize certificate"); + final HttpProxyServerConfig config = new HttpProxyServerConfig(); + + config.setHandleSsl(true); + + proxyServer = new HttpProxyServer() + .serverConfig(config) + .caCertFactory(this.certificateFactory) + .proxyInterceptInitializer(new NitroHttpProxyIntercept(serverCallback)); + + proxyServer.startAsync(NitroConstants.HTTP_PORT); + + // Hack to swap the SSL context. + try { + Security.addProvider(new BouncyCastleProvider()); + + config.setClientSslCtx(SslContextBuilder + .forClient() + .sslContextProvider(new BouncyCastleJsseProvider()) + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .protocols("TLSv1.3", "TLSv1.2") + .ciphers(new HashSet<>(Arrays.asList(NitroConstants.CIPHER_SUITES))) + .build()); + } catch (SSLException e) { + proxyServer.close(); + + log.error("Failed to create nitro proxy SSL context", e); return false; } if (!registerProxy()) { - proxyServer.stop(); + proxyServer.close(); - System.out.println("Failed to register certificate"); + log.error("Failed to register system proxy"); return false; } @@ -124,7 +162,7 @@ public class NitroHttpProxy { public void pause() { if (!unregisterProxy()) { - System.out.println("Failed to unregister system proxy, please check manually"); + log.error("Failed to unregister system proxy, please check manually"); } } @@ -135,7 +173,7 @@ public class NitroHttpProxy { return; } - proxyServer.stop(); + proxyServer.close(); 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 deleted file mode 100644 index 5d9978d..0000000 --- a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxyFilter.java +++ /dev/null @@ -1,203 +0,0 @@ -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.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -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[\"']:(\\s+)?[\"'](wss?:.*?)[\"']", Pattern.MULTILINE); - - // https://developers.cloudflare.com/fundamentals/get-started/reference/cloudflare-cookies/ - private static final HashSet CloudflareCookies = new HashSet<>(Arrays.asList( - "__cflb", - "__cf_bm", - "__cfseq", - "cf_ob_info", - "cf_use_ob", - "__cfwaitingroom", - "__cfruid", - "_cfuvid", - "cf_clearance", - "cf_chl_rc_i", - "cf_chl_rc_ni", - "cf_chl_rc_m" - )); - - 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; - private String cookies; - - 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); - - // Find relevant cookies for the WebSocket connection. - this.cookies = parseCookies(request); - } - - 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(2).replace("\\/", "/"); - final String replacementWebsocket = callback.replaceWebsocketServer(this.url, originalWebsocket); - - if (replacementWebsocket != null) { - responseBody = responseBody.replace(matcher.group(2), replacementWebsocket); - responseModified = true; - } - } - - callback.setOriginCookies(this.cookies); - } - - // Apply changes. - if (responseModified) { - responseWrite(response, responseBody); - } - - // CSP. - if (responseBody.contains(NitroClientSearch)) { - stripContentSecurityPolicy(response); - } - } - - return httpObject; - } - - /** - * Check if cookies from the request need to be recorded for the websocket connection to the origin server. - */ - private String parseCookies(final HttpRequest request) { - final List result = new ArrayList<>(); - final List cookieHeaders = request.headers().getAll("Cookie"); - - for (final String cookieHeader : cookieHeaders) { - final String[] cookies = cookieHeader.split(";"); - - for (final String cookie : cookies) { - final String[] parts = cookie.trim().split("="); - - if (CloudflareCookies.contains(parts[0])) { - result.add(cookie.trim()); - } - } - } - - if (result.size() == 0) { - return null; - } - - return String.join("; ", result); - } - - /** - * 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 deleted file mode 100644 index c736458..0000000 --- a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxyFilterSource.java +++ /dev/null @@ -1,47 +0,0 @@ -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/NitroHttpProxyIntercept.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxyIntercept.java new file mode 100644 index 0000000..81273e3 --- /dev/null +++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxyIntercept.java @@ -0,0 +1,35 @@ +package gearth.protocol.connection.proxy.nitro.http; + +import com.github.monkeywie.proxyee.intercept.HttpProxyInterceptInitializer; +import com.github.monkeywie.proxyee.intercept.HttpProxyInterceptPipeline; +import com.github.monkeywie.proxyee.intercept.common.FullResponseIntercept; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class NitroHttpProxyIntercept extends HttpProxyInterceptInitializer { + + private static final Logger log = LoggerFactory.getLogger(NitroHttpProxyIntercept.class); + + public NitroHttpProxyIntercept(NitroHttpProxyServerCallback serverCallback) { + + } + + @Override + public void init(HttpProxyInterceptPipeline pipeline) { + pipeline.addLast(new FullResponseIntercept() { + @Override + public boolean match(HttpRequest httpRequest, HttpResponse httpResponse, HttpProxyInterceptPipeline httpProxyInterceptPipeline) { + log.debug("Intercepting response for {}", httpRequest.uri()); + return false; + } + + @Override + public void handleResponse(HttpRequest httpRequest, FullHttpResponse httpResponse, HttpProxyInterceptPipeline pipeline) { + super.handleResponse(httpRequest, httpResponse, pipeline); + } + }); + } +} diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroSslContextFactory.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroSslContextFactory.java index 9e12b69..fd24029 100644 --- a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroSslContextFactory.java +++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroSslContextFactory.java @@ -6,15 +6,15 @@ import javax.net.ssl.SSLEngine; public class NitroSslContextFactory extends SslContextFactory.Server { - private final NitroCertificateSniffingManager certificateManager; + private final NitroCertificateFactory certificateFactory; - public NitroSslContextFactory(NitroCertificateSniffingManager certificateManager) { - this.certificateManager = certificateManager; + public NitroSslContextFactory(NitroCertificateFactory certificateFactory) { + this.certificateFactory = certificateFactory; } @Override public SSLEngine newSSLEngine(String host, int port) { System.out.printf("[NitroSslContextFactory] Creating SSLEngine for %s:%d%n", host, port); - return certificateManager.websocketSslEngine(host); + return certificateFactory.websocketSslEngine(host); } } diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/os/macos/NitroMacOS.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/os/macos/NitroMacOS.java index 35d5ff1..436cf7e 100644 --- a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/os/macos/NitroMacOS.java +++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/os/macos/NitroMacOS.java @@ -11,7 +11,7 @@ public class NitroMacOS implements NitroOsFunctions { /** * Semicolon separated hosts to ignore for proxying. */ - private static final String PROXY_IGNORE = "discord.com;discordapp.com;github.com;"; + private static final String PROXY_IGNORE = "discord.com;discordapp.com;canary.discord.com;canary.discordapp.com;github.com;"; /** * Checks if the certificate is trusted by the local machine. 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 index fb7d370..682119f 100644 --- 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 @@ -1,21 +1,21 @@ 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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; public class NitroWindows implements NitroOsFunctions { + private static final Logger log = LoggerFactory.getLogger(NitroWindows.class); + /** * Semicolon separated hosts to ignore for proxying. */ - // habba.io; private static final String PROXY_IGNORE = "discord.com;discordapp.com;github.com;challenges.cloudflare.com;"; /** @@ -31,7 +31,7 @@ public class NitroWindows implements NitroOsFunctions { return !output.contains("CERT_TRUST_IS_UNTRUSTED_ROOT") && output.contains("dwInfoStatus=10c dwErrorStatus=0"); } catch (IOException e) { - e.printStackTrace(); + log.error("Failed to check if root certificate is trusted", e); } return false; @@ -39,21 +39,27 @@ public class NitroWindows implements NitroOsFunctions { @Override public boolean installRootCertificate(File certificate) { - final String certificatePath = certificate.getAbsolutePath(); + final String certificatePath = certificate.toPath().normalize().toAbsolutePath().toString(); - // Prompt UAC elevation. - WinDef.HINSTANCE result = NitroWindowsShell32.INSTANCE.ShellExecuteA(null, "runas", "cmd.exe", "/S /C 'certutil -addstore root \"" + certificatePath + "\"'", null, 1); + // Correct the command for certutil + final String installCommand = "/c certutil -addstore root \"" + certificatePath + "\""; - // Wait for exit. - Kernel32.INSTANCE.WaitForSingleObject(result, WinBase.INFINITE); + log.debug("Installing root certificate with command: {}", installCommand); - // Exit code for certutil. - final IntByReference statusRef = new IntByReference(-1); - Kernel32.INSTANCE.GetExitCodeProcess(result, statusRef); + // Prompt UAC elevation using ShellExecuteA with "runas" + WinDef.HINSTANCE result = NitroWindowsShell32.INSTANCE.ShellExecuteA( + null, // Handle to parent window (optional) + "runas", // Use "runas" to request elevation + "cmd.exe", // Program to execute + installCommand, // Command to run with cmd.exe /c + null, // Directory (optional) + 1 // Show the window + ); - // Check if process exited without errors - if (statusRef.getValue() != -1) { - System.out.printf("Certutil command exited with exit code %s%n", statusRef.getValue()); + final int resultValue = result.toNative().hashCode(); + + if (resultValue <= 32) { // If the result is <= 32, an error occurred + log.error("Failed to start process for installing root certificate. Error code: {}", resultValue); return false; } @@ -69,7 +75,7 @@ public class NitroWindows implements NitroOsFunctions { 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(); + log.error("Failed to register system proxy", e); } return false; @@ -81,7 +87,7 @@ public class NitroWindows implements NitroOsFunctions { 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(); + log.error("Failed to unregister system proxy", e); } return false; 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 index a21244c..5149390 100644 --- 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 @@ -4,7 +4,7 @@ import gearth.protocol.HConnection; import gearth.protocol.connection.HProxySetter; import gearth.protocol.connection.HStateSetter; import gearth.protocol.connection.proxy.nitro.NitroProxyProvider; -import gearth.protocol.connection.proxy.nitro.http.NitroCertificateSniffingManager; +import gearth.protocol.connection.proxy.nitro.http.NitroCertificateFactory; import gearth.protocol.connection.proxy.nitro.http.NitroSslContextFactory; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Server; @@ -22,7 +22,7 @@ public class NitroWebsocketProxy { private final HStateSetter stateSetter; private final HConnection connection; private final NitroProxyProvider proxyProvider; - private final NitroCertificateSniffingManager certificateManager; + private final NitroCertificateFactory certificateFactory; private final Server server; private final int serverPort; @@ -31,12 +31,12 @@ public class NitroWebsocketProxy { HStateSetter stateSetter, HConnection connection, NitroProxyProvider proxyProvider, - NitroCertificateSniffingManager certificateManager) { + NitroCertificateFactory certificateFactory) { this.proxySetter = proxySetter; this.stateSetter = stateSetter; this.connection = connection; this.proxyProvider = proxyProvider; - this.certificateManager = certificateManager; + this.certificateFactory = certificateFactory; this.server = new Server(); this.serverPort = 0; } @@ -44,7 +44,7 @@ public class NitroWebsocketProxy { public boolean start() { try { // Configure SSL. - final NitroSslContextFactory sslContextFactory = new NitroSslContextFactory(this.certificateManager); + final NitroSslContextFactory sslContextFactory = new NitroSslContextFactory(this.certificateFactory); final ServerConnector sslConnector = new ServerConnector(server, sslContextFactory); sslConnector.setPort(this.serverPort); diff --git a/pom.xml b/pom.xml index 52d38ed..2b23a73 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ - 1.5.4-beta-9 + 1.5.4-beta-10 -SNAPSHOT 1.5.3