mirror of
https://github.com/sirjonasxx/G-Earth.git
synced 2024-11-23 00:40:51 +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>
|
||||
<jettyVersion>9.4.53.v20231009</jettyVersion>
|
||||
<logback.version>1.3.12</logback.version>
|
||||
<bouncycastle.version>1.78.1</bouncycastle.version>
|
||||
</properties>
|
||||
|
||||
<parent>
|
||||
@ -331,19 +332,19 @@
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk18on</artifactId>
|
||||
<version>1.78.1</version>
|
||||
<version>${bouncycastle.version}</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/org.bouncycastle/bcpkix-jdk18on -->
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcpkix-jdk18on</artifactId>
|
||||
<version>1.78.1</version>
|
||||
<version>${bouncycastle.version}</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/org.bouncycastle/bctls-jdk18on -->
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bctls-jdk18on</artifactId>
|
||||
<version>1.78.1</version>
|
||||
<version>${bouncycastle.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>G-Earth</groupId>
|
||||
|
@ -121,7 +121,7 @@ public class NitroProxyProvider implements ProxyProvider, NitroHttpProxyServerCa
|
||||
}
|
||||
|
||||
@Override
|
||||
public String replaceWebsocketServer(String configUrl, String websocketUrl) {
|
||||
public String replaceWebsocketServer(String websocketUrl) {
|
||||
originalWebsocketUrl = websocketUrl;
|
||||
|
||||
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.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.LoggerFactory;
|
||||
|
||||
import javax.net.ssl.SSLEngine;
|
||||
import java.io.File;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class NitroCertificateFactory implements HttpProxyCACertFactory {
|
||||
|
||||
@ -25,6 +41,7 @@ public class NitroCertificateFactory implements HttpProxyCACertFactory {
|
||||
|
||||
private X509Certificate caCert;
|
||||
private PrivateKey caKey;
|
||||
private HttpProxyServerConfig config;
|
||||
|
||||
public NitroCertificateFactory() {
|
||||
this.caCertFile = new File(String.format("./%s.crt", NitroAuthority.CERT_ALIAS));
|
||||
@ -107,8 +124,57 @@ public class NitroCertificateFactory implements HttpProxyCACertFactory {
|
||||
return this.caKey;
|
||||
}
|
||||
|
||||
public SSLEngine websocketSslEngine(String host) {
|
||||
throw new UnsupportedOperationException("Not implemented");
|
||||
public void setServerConfig(HttpProxyServerConfig config) {
|
||||
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);
|
||||
|
||||
// Hack to swap the SSL context.
|
||||
// Need to set this after proxyServer is started because starting it will override the configured SSL context.
|
||||
try {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
|
||||
@ -150,6 +151,9 @@ public class NitroHttpProxy {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add config to factory so websocket server can use it as well.
|
||||
this.certificateFactory.setServerConfig(config);
|
||||
|
||||
if (!registerProxy()) {
|
||||
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.HttpProxyInterceptPipeline;
|
||||
import com.github.monkeywie.proxyee.intercept.common.FullRequestIntercept;
|
||||
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 com.github.monkeywie.proxyee.util.ByteUtil;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.handler.codec.http.*;
|
||||
import org.slf4j.Logger;
|
||||
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 {
|
||||
|
||||
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
|
||||
@ -22,14 +62,144 @@ public class NitroHttpProxyIntercept extends HttpProxyInterceptInitializer {
|
||||
pipeline.addLast(new FullResponseIntercept() {
|
||||
@Override
|
||||
public boolean match(HttpRequest httpRequest, HttpResponse httpResponse, HttpProxyInterceptPipeline httpProxyInterceptPipeline) {
|
||||
log.debug("Intercepting response for {}", httpRequest.uri());
|
||||
return false;
|
||||
log.debug("Intercepting response for {} {}", httpRequest.headers().get(HttpHeaderNames.HOST), httpRequest.uri());
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
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.
|
||||
*
|
||||
* @param configUrl The url at which the websocket url was found.
|
||||
* @param websocketUrl The hotel websocket url.
|
||||
* @return Return null to not replace anything, otherwise specify an alternative websocket url.
|
||||
*/
|
||||
String replaceWebsocketServer(String configUrl, String websocketUrl);
|
||||
String replaceWebsocketServer(String websocketUrl);
|
||||
|
||||
/**
|
||||
* Sets the parsed cookies for the origin WebSocket connection.
|
||||
|
Loading…
Reference in New Issue
Block a user