mirror of
https://github.com/sirjonasxx/G-Earth.git
synced 2024-11-26 18:30:52 +01:00
Finish nitro mitm proxy rewrite
This commit is contained in:
parent
fe97142bfe
commit
df53183034
@ -12,6 +12,7 @@
|
|||||||
<javafx.version>1.8</javafx.version>
|
<javafx.version>1.8</javafx.version>
|
||||||
<jettyVersion>9.4.53.v20231009</jettyVersion>
|
<jettyVersion>9.4.53.v20231009</jettyVersion>
|
||||||
<logback.version>1.3.12</logback.version>
|
<logback.version>1.3.12</logback.version>
|
||||||
|
<bouncycastle.version>1.78.1</bouncycastle.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<parent>
|
<parent>
|
||||||
@ -331,19 +332,19 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.bouncycastle</groupId>
|
<groupId>org.bouncycastle</groupId>
|
||||||
<artifactId>bcprov-jdk18on</artifactId>
|
<artifactId>bcprov-jdk18on</artifactId>
|
||||||
<version>1.78.1</version>
|
<version>${bouncycastle.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- https://mvnrepository.com/artifact/org.bouncycastle/bcpkix-jdk18on -->
|
<!-- https://mvnrepository.com/artifact/org.bouncycastle/bcpkix-jdk18on -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.bouncycastle</groupId>
|
<groupId>org.bouncycastle</groupId>
|
||||||
<artifactId>bcpkix-jdk18on</artifactId>
|
<artifactId>bcpkix-jdk18on</artifactId>
|
||||||
<version>1.78.1</version>
|
<version>${bouncycastle.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- https://mvnrepository.com/artifact/org.bouncycastle/bctls-jdk18on -->
|
<!-- https://mvnrepository.com/artifact/org.bouncycastle/bctls-jdk18on -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.bouncycastle</groupId>
|
<groupId>org.bouncycastle</groupId>
|
||||||
<artifactId>bctls-jdk18on</artifactId>
|
<artifactId>bctls-jdk18on</artifactId>
|
||||||
<version>1.78.1</version>
|
<version>${bouncycastle.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>G-Earth</groupId>
|
<groupId>G-Earth</groupId>
|
||||||
|
@ -121,7 +121,7 @@ public class NitroProxyProvider implements ProxyProvider, NitroHttpProxyServerCa
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String replaceWebsocketServer(String configUrl, String websocketUrl) {
|
public String replaceWebsocketServer(String websocketUrl) {
|
||||||
originalWebsocketUrl = websocketUrl;
|
originalWebsocketUrl = websocketUrl;
|
||||||
|
|
||||||
return String.format("wss://127.0.0.1:%d", websocketPort);
|
return String.format("wss://127.0.0.1:%d", websocketPort);
|
||||||
|
@ -2,19 +2,35 @@ package gearth.protocol.connection.proxy.nitro.http;
|
|||||||
|
|
||||||
import com.github.monkeywie.proxyee.crt.CertUtil;
|
import com.github.monkeywie.proxyee.crt.CertUtil;
|
||||||
import com.github.monkeywie.proxyee.server.HttpProxyCACertFactory;
|
import com.github.monkeywie.proxyee.server.HttpProxyCACertFactory;
|
||||||
|
import com.github.monkeywie.proxyee.server.HttpProxyServerConfig;
|
||||||
|
import io.netty.buffer.ByteBufAllocator;
|
||||||
|
import io.netty.handler.ssl.SslContext;
|
||||||
|
import io.netty.handler.ssl.SslContextBuilder;
|
||||||
|
import org.bouncycastle.asn1.x500.X500Name;
|
||||||
|
import org.bouncycastle.asn1.x509.Extension;
|
||||||
|
import org.bouncycastle.asn1.x509.GeneralName;
|
||||||
|
import org.bouncycastle.asn1.x509.GeneralNames;
|
||||||
|
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||||
|
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
|
||||||
|
import org.bouncycastle.operator.ContentSigner;
|
||||||
|
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import javax.net.ssl.SSLEngine;
|
import javax.net.ssl.SSLEngine;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.math.BigInteger;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
|
import java.security.PublicKey;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import java.security.spec.PKCS8EncodedKeySpec;
|
import java.security.spec.PKCS8EncodedKeySpec;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
public class NitroCertificateFactory implements HttpProxyCACertFactory {
|
public class NitroCertificateFactory implements HttpProxyCACertFactory {
|
||||||
|
|
||||||
@ -25,6 +41,7 @@ public class NitroCertificateFactory implements HttpProxyCACertFactory {
|
|||||||
|
|
||||||
private X509Certificate caCert;
|
private X509Certificate caCert;
|
||||||
private PrivateKey caKey;
|
private PrivateKey caKey;
|
||||||
|
private HttpProxyServerConfig config;
|
||||||
|
|
||||||
public NitroCertificateFactory() {
|
public NitroCertificateFactory() {
|
||||||
this.caCertFile = new File(String.format("./%s.crt", NitroAuthority.CERT_ALIAS));
|
this.caCertFile = new File(String.format("./%s.crt", NitroAuthority.CERT_ALIAS));
|
||||||
@ -107,8 +124,57 @@ public class NitroCertificateFactory implements HttpProxyCACertFactory {
|
|||||||
return this.caKey;
|
return this.caKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SSLEngine websocketSslEngine(String host) {
|
public void setServerConfig(HttpProxyServerConfig config) {
|
||||||
throw new UnsupportedOperationException("Not implemented");
|
this.config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public SSLEngine websocketSslEngine(String commonName) {
|
||||||
|
if (this.config == null) {
|
||||||
|
throw new IllegalStateException("Server config not set");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final X509Certificate cert = generateServerCert(commonName,
|
||||||
|
new GeneralName(GeneralName.dNSName, "localhost"),
|
||||||
|
new GeneralName(GeneralName.iPAddress, "127.0.0.1"));
|
||||||
|
|
||||||
|
final SslContext ctx = SslContextBuilder.forServer(this.config.getServerPriKey(), cert).build();
|
||||||
|
|
||||||
|
return ctx.newEngine(ByteBufAllocator.DEFAULT);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to create SSLEngine", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private X509Certificate generateServerCert(String commonName, GeneralName... san) throws Exception {
|
||||||
|
final String issuer = this.config.getIssuer();
|
||||||
|
final PrivateKey caPriKey = this.config.getCaPriKey();
|
||||||
|
final Date caNotBefore = this.config.getCaNotBefore();
|
||||||
|
final Date caNotAfter = this.config.getCaNotAfter();
|
||||||
|
final PublicKey serverPubKey = this.config.getServerPubKey();
|
||||||
|
|
||||||
|
// Replace "CN" in cert authority
|
||||||
|
final String subject = Stream.of(issuer.split(", ")).map(item -> {
|
||||||
|
String[] arr = item.split("=");
|
||||||
|
if ("CN".equals(arr[0])) {
|
||||||
|
return "CN=" + commonName;
|
||||||
|
} else {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}).collect(Collectors.joining(", "));
|
||||||
|
|
||||||
|
final JcaX509v3CertificateBuilder jv3Builder = new JcaX509v3CertificateBuilder(new X500Name(issuer),
|
||||||
|
BigInteger.valueOf(System.currentTimeMillis() + (long) (Math.random() * 10000) + 1000),
|
||||||
|
caNotBefore,
|
||||||
|
caNotAfter,
|
||||||
|
new X500Name(subject),
|
||||||
|
serverPubKey);
|
||||||
|
|
||||||
|
jv3Builder.addExtension(Extension.subjectAlternativeName, false, new GeneralNames(san));
|
||||||
|
|
||||||
|
final ContentSigner signer = new JcaContentSignerBuilder("SHA256WithRSAEncryption").build(caPriKey);
|
||||||
|
|
||||||
|
return new JcaX509CertificateConverter().getCertificate(jv3Builder.build(signer));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -133,6 +133,7 @@ public class NitroHttpProxy {
|
|||||||
proxyServer.startAsync(NitroConstants.HTTP_PORT);
|
proxyServer.startAsync(NitroConstants.HTTP_PORT);
|
||||||
|
|
||||||
// Hack to swap the SSL context.
|
// Hack to swap the SSL context.
|
||||||
|
// Need to set this after proxyServer is started because starting it will override the configured SSL context.
|
||||||
try {
|
try {
|
||||||
Security.addProvider(new BouncyCastleProvider());
|
Security.addProvider(new BouncyCastleProvider());
|
||||||
|
|
||||||
@ -150,6 +151,9 @@ public class NitroHttpProxy {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add config to factory so websocket server can use it as well.
|
||||||
|
this.certificateFactory.setServerConfig(config);
|
||||||
|
|
||||||
if (!registerProxy()) {
|
if (!registerProxy()) {
|
||||||
proxyServer.close();
|
proxyServer.close();
|
||||||
|
|
||||||
|
@ -2,19 +2,59 @@ package gearth.protocol.connection.proxy.nitro.http;
|
|||||||
|
|
||||||
import com.github.monkeywie.proxyee.intercept.HttpProxyInterceptInitializer;
|
import com.github.monkeywie.proxyee.intercept.HttpProxyInterceptInitializer;
|
||||||
import com.github.monkeywie.proxyee.intercept.HttpProxyInterceptPipeline;
|
import com.github.monkeywie.proxyee.intercept.HttpProxyInterceptPipeline;
|
||||||
|
import com.github.monkeywie.proxyee.intercept.common.FullRequestIntercept;
|
||||||
import com.github.monkeywie.proxyee.intercept.common.FullResponseIntercept;
|
import com.github.monkeywie.proxyee.intercept.common.FullResponseIntercept;
|
||||||
import io.netty.handler.codec.http.FullHttpResponse;
|
import com.github.monkeywie.proxyee.util.ByteUtil;
|
||||||
import io.netty.handler.codec.http.HttpRequest;
|
import io.netty.buffer.ByteBuf;
|
||||||
import io.netty.handler.codec.http.HttpResponse;
|
import io.netty.handler.codec.http.*;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
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 NitroHttpProxyIntercept extends HttpProxyInterceptInitializer {
|
public class NitroHttpProxyIntercept extends HttpProxyInterceptInitializer {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(NitroHttpProxyIntercept.class);
|
private static final Logger log = LoggerFactory.getLogger(NitroHttpProxyIntercept.class);
|
||||||
|
|
||||||
public NitroHttpProxyIntercept(NitroHttpProxyServerCallback serverCallback) {
|
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;
|
||||||
|
|
||||||
|
public NitroHttpProxyIntercept(NitroHttpProxyServerCallback callback) {
|
||||||
|
this.callback = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -22,14 +62,144 @@ public class NitroHttpProxyIntercept extends HttpProxyInterceptInitializer {
|
|||||||
pipeline.addLast(new FullResponseIntercept() {
|
pipeline.addLast(new FullResponseIntercept() {
|
||||||
@Override
|
@Override
|
||||||
public boolean match(HttpRequest httpRequest, HttpResponse httpResponse, HttpProxyInterceptPipeline httpProxyInterceptPipeline) {
|
public boolean match(HttpRequest httpRequest, HttpResponse httpResponse, HttpProxyInterceptPipeline httpProxyInterceptPipeline) {
|
||||||
log.debug("Intercepting response for {}", httpRequest.uri());
|
log.debug("Intercepting response for {} {}", httpRequest.headers().get(HttpHeaderNames.HOST), httpRequest.uri());
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleResponse(HttpRequest httpRequest, FullHttpResponse httpResponse, HttpProxyInterceptPipeline pipeline) {
|
public void handleResponse(HttpRequest httpRequest, FullHttpResponse httpResponse, HttpProxyInterceptPipeline pipeline) {
|
||||||
super.handleResponse(httpRequest, httpResponse, pipeline);
|
// Strip cache headers.
|
||||||
|
stripCacheHeaders(httpResponse.headers());
|
||||||
|
|
||||||
|
// Check for response body.
|
||||||
|
final ByteBuf content = httpResponse.content();
|
||||||
|
|
||||||
|
if (content == null || content.readableBytes() == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find nitro configuration.
|
||||||
|
if (ByteUtil.findText(content, NitroConfigSearch) != -1) {
|
||||||
|
final String responseBody = responseRead(httpResponse);
|
||||||
|
final Matcher matcher = NitroConfigPattern.matcher(responseBody);
|
||||||
|
|
||||||
|
// Replace websocket with proxy.
|
||||||
|
if (matcher.find()) {
|
||||||
|
final String originalWebsocket = matcher.group(2).replace("\\/", "/");
|
||||||
|
final String replacementWebsocket = callback.replaceWebsocketServer(originalWebsocket);
|
||||||
|
|
||||||
|
if (replacementWebsocket != null) {
|
||||||
|
final String updatedBody = responseBody.replace(matcher.group(2), replacementWebsocket);
|
||||||
|
|
||||||
|
responseWrite(httpResponse, updatedBody);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve cookies for request to the origin.
|
||||||
|
final String requestCookies = parseCookies(httpRequest);
|
||||||
|
|
||||||
|
if (requestCookies != null && !requestCookies.isEmpty()) {
|
||||||
|
callback.setOriginCookies(requestCookies);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip CSP headers
|
||||||
|
if (ByteUtil.findText(content, NitroClientSearch) != -1) {
|
||||||
|
stripContentSecurityPolicy(httpResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
pipeline.addLast(new FullRequestIntercept() {
|
||||||
|
@Override
|
||||||
|
public boolean match(HttpRequest httpRequest, HttpProxyInterceptPipeline pipeline) {
|
||||||
|
log.debug("Intercepting request for {} {}", httpRequest.headers().get(HttpHeaderNames.HOST), httpRequest.uri());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleRequest(FullHttpRequest httpRequest, HttpProxyInterceptPipeline pipeline) {
|
||||||
|
// Disable caching.
|
||||||
|
stripCacheHeaders(httpRequest.headers());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if cookies from the request need to be recorded for the websocket connection to the origin server.
|
||||||
|
*/
|
||||||
|
private static 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.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return String.join("; ", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modify Content-Security-Policy header, which could prevent Nitro from connecting with G-Earth.
|
||||||
|
*/
|
||||||
|
private static 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip cache headers from the response.
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String responseRead(FullHttpResponse response) {
|
||||||
|
final ByteBuf contentBuf = response.content();
|
||||||
|
return contentBuf.toString(StandardCharsets.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.
|
||||||
|
HttpUtil.setContentLength(response, body.length);
|
||||||
|
|
||||||
|
// Ensure modified response is not cached.
|
||||||
|
stripCacheHeaders(response.headers());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,11 +5,10 @@ public interface NitroHttpProxyServerCallback {
|
|||||||
/**
|
/**
|
||||||
* Specify a replacement for the given websocket url.
|
* 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.
|
* @param websocketUrl The hotel websocket url.
|
||||||
* @return Return null to not replace anything, otherwise specify an alternative websocket url.
|
* @return Return null to not replace anything, otherwise specify an alternative websocket url.
|
||||||
*/
|
*/
|
||||||
String replaceWebsocketServer(String configUrl, String websocketUrl);
|
String replaceWebsocketServer(String websocketUrl);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the parsed cookies for the origin WebSocket connection.
|
* Sets the parsed cookies for the origin WebSocket connection.
|
||||||
|
Loading…
Reference in New Issue
Block a user