Nitro http proxy rewrite to proxyee progress

This commit is contained in:
UnfamiliarLegacy 2024-10-23 06:15:54 +02:00
parent 97eb397d9e
commit fe97142bfe
17 changed files with 311 additions and 465 deletions

4
.gitignore vendored
View File

@ -25,4 +25,6 @@ Extensions/BlockReplacePackets/.classpath
# Certificates # Certificates
*.p12 *.p12
*.pem *.pem
*.crt
*.key

View File

@ -313,20 +313,38 @@
<version>1.5.0</version> <version>1.5.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.github.ganskef</groupId> <groupId>com.github.monkeywie</groupId>
<artifactId>littleproxy-mitm</artifactId> <artifactId>proxyee</artifactId>
<version>1.1.0</version> <version>1.7.6</version>
<exclusions> <exclusions>
<exclusion> <exclusion>
<groupId>org.slf4j</groupId> <groupId>org.bouncycastle</groupId>
<artifactId>slf4j-api</artifactId> <artifactId>bcprov-jdk15on</artifactId>
</exclusion> </exclusion>
<exclusion> <exclusion>
<groupId>org.slf4j</groupId> <groupId>org.bouncycastle</groupId>
<artifactId>slf4j-log4j12</artifactId> <artifactId>bcpkix-jdk15on</artifactId>
</exclusion> </exclusion>
</exclusions> </exclusions>
</dependency> </dependency>
<!-- https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk18on -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.78.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.bouncycastle/bcpkix-jdk18on -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<version>1.78.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.bouncycastle/bctls-jdk18on -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bctls-jdk18on</artifactId>
<version>1.78.1</version>
</dependency>
<dependency> <dependency>
<groupId>G-Earth</groupId> <groupId>G-Earth</groupId>
<artifactId>G-Wasm-Minimal</artifactId> <artifactId>G-Wasm-Minimal</artifactId>

View File

@ -122,8 +122,8 @@ public class GEarth extends Application {
primaryStage.show(); primaryStage.show();
primaryStage.setOnCloseRequest(event -> closeGEarth()); primaryStage.setOnCloseRequest(event -> closeGEarth());
AdminValidator.validate(); //AdminValidator.validate();
UpdateChecker.checkForUpdates(); //UpdateChecker.checkForUpdates();
} }

View File

@ -9,4 +9,22 @@ public final class NitroConstants {
public static final String WEBSOCKET_REVISION = "PRODUCTION-201611291003-338511768"; public static final String WEBSOCKET_REVISION = "PRODUCTION-201611291003-338511768";
public static final String WEBSOCKET_CLIENT_IDENTIFIER = "HTML5"; 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"
};
} }

View File

@ -6,8 +6,7 @@ import gearth.protocol.connection.HProxySetter;
import gearth.protocol.connection.HState; import gearth.protocol.connection.HState;
import gearth.protocol.connection.HStateSetter; import gearth.protocol.connection.HStateSetter;
import gearth.protocol.connection.proxy.ProxyProvider; import gearth.protocol.connection.proxy.ProxyProvider;
import gearth.protocol.connection.proxy.nitro.http.NitroAuthority; import gearth.protocol.connection.proxy.nitro.http.NitroCertificateFactory;
import gearth.protocol.connection.proxy.nitro.http.NitroCertificateSniffingManager;
import gearth.protocol.connection.proxy.nitro.http.NitroHttpProxy; import gearth.protocol.connection.proxy.nitro.http.NitroHttpProxy;
import gearth.protocol.connection.proxy.nitro.http.NitroHttpProxyServerCallback; import gearth.protocol.connection.proxy.nitro.http.NitroHttpProxyServerCallback;
import gearth.protocol.connection.proxy.nitro.websocket.NitroWebsocketProxy; import gearth.protocol.connection.proxy.nitro.websocket.NitroWebsocketProxy;
@ -33,8 +32,7 @@ public class NitroProxyProvider implements ProxyProvider, NitroHttpProxyServerCa
private String originalCookies; private String originalCookies;
public NitroProxyProvider(HProxySetter proxySetter, HStateSetter stateSetter, HConnection connection) { public NitroProxyProvider(HProxySetter proxySetter, HStateSetter stateSetter, HConnection connection) {
final NitroAuthority authority = new NitroAuthority(); final NitroCertificateFactory certificateManager = new NitroCertificateFactory();
final NitroCertificateSniffingManager certificateManager = new NitroCertificateSniffingManager(authority);
this.proxySetter = proxySetter; this.proxySetter = proxySetter;
this.stateSetter = stateSetter; this.stateSetter = stateSetter;
@ -103,7 +101,7 @@ public class NitroProxyProvider implements ProxyProvider, NitroHttpProxyServerCa
try { try {
nitroHttpProxy.stop(); nitroHttpProxy.stop();
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); logger.error("Failed to stop nitro http proxy", e);
} }
logger.info("Stopping nitro websocket proxy"); logger.info("Stopping nitro websocket proxy");
@ -111,7 +109,7 @@ public class NitroProxyProvider implements ProxyProvider, NitroHttpProxyServerCa
try { try {
nitroWebsocketProxy.stop(); nitroWebsocketProxy.stop();
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); logger.error("Failed to stop nitro websocket proxy", e);
} }
stateSetter.setState(HState.NOT_CONNECTED); stateSetter.setState(HState.NOT_CONNECTED);

View File

@ -1,24 +1,9 @@
package gearth.protocol.connection.proxy.nitro.http; package gearth.protocol.connection.proxy.nitro.http;
import org.littleshoot.proxy.mitm.Authority; public class NitroAuthority {
import java.io.File; public static final String CERT_ALIAS = "gearth-nitro-v2";
public static final String CERT_ORGANIZATION = "G-Earth Nitro";
public class NitroAuthority extends Authority { public static final String CERT_DESCRIPTION = "G-Earth nitro support v2";
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);
}
} }

View File

@ -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");
}
}

View File

@ -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());
}
}

View File

@ -1,42 +1,57 @@
package gearth.protocol.connection.proxy.nitro.http; 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.misc.ConfirmationDialog;
import gearth.protocol.connection.proxy.nitro.NitroConstants; import gearth.protocol.connection.proxy.nitro.NitroConstants;
import gearth.protocol.connection.proxy.nitro.os.NitroOsFunctions; import gearth.protocol.connection.proxy.nitro.os.NitroOsFunctions;
import gearth.protocol.connection.proxy.nitro.os.NitroOsFunctionsFactory; import gearth.protocol.connection.proxy.nitro.os.NitroOsFunctionsFactory;
import gearth.ui.titlebar.TitleBarController; import gearth.ui.titlebar.TitleBarController;
import gearth.ui.translations.LanguageBundle; import gearth.ui.translations.LanguageBundle;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.scene.control.Alert; import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType; import javafx.scene.control.ButtonType;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import org.littleshoot.proxy.HttpProxyServer; import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.littleshoot.proxy.impl.DefaultHttpProxyServer; 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.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.Semaphore;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
public class NitroHttpProxy { 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 String ADMIN_WARNING_KEY = "admin_warning_dialog";
private static final AtomicBoolean SHUTDOWN_HOOK = new AtomicBoolean(); private static final AtomicBoolean SHUTDOWN_HOOK = new AtomicBoolean();
private final NitroOsFunctions osFunctions; private final NitroOsFunctions osFunctions;
private final NitroHttpProxyServerCallback serverCallback; private final NitroHttpProxyServerCallback serverCallback;
private final NitroCertificateSniffingManager certificateManager; private final NitroCertificateFactory certificateFactory;
private HttpProxyServer proxyServer = null; private HttpProxyServer proxyServer = null;
public NitroHttpProxy(NitroHttpProxyServerCallback serverCallback, NitroCertificateSniffingManager certificateManager) { public NitroHttpProxy(NitroHttpProxyServerCallback serverCallback, NitroCertificateFactory certificateManager) {
this.serverCallback = serverCallback; this.serverCallback = serverCallback;
this.certificateManager = certificateManager; this.certificateFactory = certificateManager;
this.osFunctions = NitroOsFunctionsFactory.create(); this.osFunctions = NitroOsFunctionsFactory.create();
} }
private boolean initializeCertificate() { 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. // All good if certificate is already trusted.
if (this.osFunctions.isRootCertificateTrusted(certificate)) { if (this.osFunctions.isRootCertificateTrusted(certificate)) {
@ -48,28 +63,31 @@ public class NitroHttpProxy {
final AtomicBoolean shouldInstall = new AtomicBoolean(); final AtomicBoolean shouldInstall = new AtomicBoolean();
Platform.runLater(() -> { 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 { 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() shouldInstall.set(TitleBarController.create(alert).showAlertAndWait()
.filter(t -> t == ButtonType.YES).isPresent()); .filter(t -> t == ButtonType.YES).isPresent());
} catch (IOException e) { } catch (Exception e) {
e.printStackTrace(); log.error("Failed to show install dialog", e);
} finally {
waitForDialog.release();
} }
waitForDialog.release();
}); });
// Wait for dialog choice. // Wait for dialog choice.
try { try {
waitForDialog.acquire(); waitForDialog.acquire();
} catch (InterruptedException e) { } catch (InterruptedException e) {
e.printStackTrace(); log.error("Interrupted while waiting for user input", e);
return false; return false;
} }
@ -78,7 +96,7 @@ public class NitroHttpProxy {
return false; 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() { public boolean start() {
setupShutdownHook(); setupShutdownHook();
proxyServer = DefaultHttpProxyServer.bootstrap()
.withPort(NitroConstants.HTTP_PORT)
.withManInTheMiddle(this.certificateManager)
.withFiltersSource(new NitroHttpProxyFilterSource(serverCallback))
.withTransparent(true)
.start();
if (!initializeCertificate()) { 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; return false;
} }
if (!registerProxy()) { if (!registerProxy()) {
proxyServer.stop(); proxyServer.close();
System.out.println("Failed to register certificate"); log.error("Failed to register system proxy");
return false; return false;
} }
@ -124,7 +162,7 @@ public class NitroHttpProxy {
public void pause() { public void pause() {
if (!unregisterProxy()) { 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; return;
} }
proxyServer.stop(); proxyServer.close();
proxyServer = null; proxyServer = null;
} }

View File

@ -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<String> 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<String> result = new ArrayList<>();
final List<String> 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);
}
}

View File

@ -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<String> 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;
}
}

View File

@ -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);
}
});
}
}

View File

@ -6,15 +6,15 @@ import javax.net.ssl.SSLEngine;
public class NitroSslContextFactory extends SslContextFactory.Server { public class NitroSslContextFactory extends SslContextFactory.Server {
private final NitroCertificateSniffingManager certificateManager; private final NitroCertificateFactory certificateFactory;
public NitroSslContextFactory(NitroCertificateSniffingManager certificateManager) { public NitroSslContextFactory(NitroCertificateFactory certificateFactory) {
this.certificateManager = certificateManager; this.certificateFactory = certificateFactory;
} }
@Override @Override
public SSLEngine newSSLEngine(String host, int port) { public SSLEngine newSSLEngine(String host, int port) {
System.out.printf("[NitroSslContextFactory] Creating SSLEngine for %s:%d%n", host, port); System.out.printf("[NitroSslContextFactory] Creating SSLEngine for %s:%d%n", host, port);
return certificateManager.websocketSslEngine(host); return certificateFactory.websocketSslEngine(host);
} }
} }

View File

@ -11,7 +11,7 @@ public class NitroMacOS implements NitroOsFunctions {
/** /**
* Semicolon separated hosts to ignore for proxying. * 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. * Checks if the certificate is trusted by the local machine.

View File

@ -1,21 +1,21 @@
package gearth.protocol.connection.proxy.nitro.os.windows; 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.platform.win32.WinDef;
import com.sun.jna.ptr.IntByReference;
import gearth.misc.RuntimeUtil; import gearth.misc.RuntimeUtil;
import gearth.protocol.connection.proxy.nitro.os.NitroOsFunctions; import gearth.protocol.connection.proxy.nitro.os.NitroOsFunctions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
public class NitroWindows implements NitroOsFunctions { public class NitroWindows implements NitroOsFunctions {
private static final Logger log = LoggerFactory.getLogger(NitroWindows.class);
/** /**
* Semicolon separated hosts to ignore for proxying. * Semicolon separated hosts to ignore for proxying.
*/ */
// habba.io;
private static final String PROXY_IGNORE = "discord.com;discordapp.com;github.com;challenges.cloudflare.com;"; 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") && return !output.contains("CERT_TRUST_IS_UNTRUSTED_ROOT") &&
output.contains("dwInfoStatus=10c dwErrorStatus=0"); output.contains("dwInfoStatus=10c dwErrorStatus=0");
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); log.error("Failed to check if root certificate is trusted", e);
} }
return false; return false;
@ -39,21 +39,27 @@ public class NitroWindows implements NitroOsFunctions {
@Override @Override
public boolean installRootCertificate(File certificate) { public boolean installRootCertificate(File certificate) {
final String certificatePath = certificate.getAbsolutePath(); final String certificatePath = certificate.toPath().normalize().toAbsolutePath().toString();
// Prompt UAC elevation. // Correct the command for certutil
WinDef.HINSTANCE result = NitroWindowsShell32.INSTANCE.ShellExecuteA(null, "runas", "cmd.exe", "/S /C 'certutil -addstore root \"" + certificatePath + "\"'", null, 1); final String installCommand = "/c certutil -addstore root \"" + certificatePath + "\"";
// Wait for exit. log.debug("Installing root certificate with command: {}", installCommand);
Kernel32.INSTANCE.WaitForSingleObject(result, WinBase.INFINITE);
// Exit code for certutil. // Prompt UAC elevation using ShellExecuteA with "runas"
final IntByReference statusRef = new IntByReference(-1); WinDef.HINSTANCE result = NitroWindowsShell32.INSTANCE.ShellExecuteA(
Kernel32.INSTANCE.GetExitCodeProcess(result, statusRef); 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 final int resultValue = result.toNative().hashCode();
if (statusRef.getValue() != -1) {
System.out.printf("Certutil command exited with exit code %s%n", statusRef.getValue()); 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; 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"); Runtime.getRuntime().exec("reg add \"HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\" /v ProxyEnable /t REG_DWORD /d 1 /f");
return true; return true;
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); log.error("Failed to register system proxy", e);
} }
return false; 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"); Runtime.getRuntime().exec("reg add \"HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\" /v ProxyEnable /t REG_DWORD /d 0 /f");
return true; return true;
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); log.error("Failed to unregister system proxy", e);
} }
return false; return false;

View File

@ -4,7 +4,7 @@ import gearth.protocol.HConnection;
import gearth.protocol.connection.HProxySetter; import gearth.protocol.connection.HProxySetter;
import gearth.protocol.connection.HStateSetter; import gearth.protocol.connection.HStateSetter;
import gearth.protocol.connection.proxy.nitro.NitroProxyProvider; 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 gearth.protocol.connection.proxy.nitro.http.NitroSslContextFactory;
import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
@ -22,7 +22,7 @@ public class NitroWebsocketProxy {
private final HStateSetter stateSetter; private final HStateSetter stateSetter;
private final HConnection connection; private final HConnection connection;
private final NitroProxyProvider proxyProvider; private final NitroProxyProvider proxyProvider;
private final NitroCertificateSniffingManager certificateManager; private final NitroCertificateFactory certificateFactory;
private final Server server; private final Server server;
private final int serverPort; private final int serverPort;
@ -31,12 +31,12 @@ public class NitroWebsocketProxy {
HStateSetter stateSetter, HStateSetter stateSetter,
HConnection connection, HConnection connection,
NitroProxyProvider proxyProvider, NitroProxyProvider proxyProvider,
NitroCertificateSniffingManager certificateManager) { NitroCertificateFactory certificateFactory) {
this.proxySetter = proxySetter; this.proxySetter = proxySetter;
this.stateSetter = stateSetter; this.stateSetter = stateSetter;
this.connection = connection; this.connection = connection;
this.proxyProvider = proxyProvider; this.proxyProvider = proxyProvider;
this.certificateManager = certificateManager; this.certificateFactory = certificateFactory;
this.server = new Server(); this.server = new Server();
this.serverPort = 0; this.serverPort = 0;
} }
@ -44,7 +44,7 @@ public class NitroWebsocketProxy {
public boolean start() { public boolean start() {
try { try {
// Configure SSL. // Configure SSL.
final NitroSslContextFactory sslContextFactory = new NitroSslContextFactory(this.certificateManager); final NitroSslContextFactory sslContextFactory = new NitroSslContextFactory(this.certificateFactory);
final ServerConnector sslConnector = new ServerConnector(server, sslContextFactory); final ServerConnector sslConnector = new ServerConnector(server, sslContextFactory);
sslConnector.setPort(this.serverPort); sslConnector.setPort(this.serverPort);

View File

@ -11,7 +11,7 @@
<properties> <properties>
<!-- Version of the application. --> <!-- Version of the application. -->
<revision>1.5.4-beta-9</revision> <revision>1.5.4-beta-10</revision>
<changelist>-SNAPSHOT</changelist> <changelist>-SNAPSHOT</changelist>
<!-- Version for https://github.com/sirjonasxx/G-ExtensionStore to keep compatibility with beta versions. --> <!-- Version for https://github.com/sirjonasxx/G-ExtensionStore to keep compatibility with beta versions. -->
<storeVersion>1.5.3</storeVersion> <storeVersion>1.5.3</storeVersion>