mirror of
https://github.com/sirjonasxx/G-Earth.git
synced 2024-11-22 08:20:51 +01:00
Add HTTP(S) MITM and replace websocket in config
This commit is contained in:
parent
dbf692cb3d
commit
1c6d086bff
4
.gitignore
vendored
4
.gitignore
vendored
@ -21,3 +21,7 @@ Extensions/BlockReplacePackets/.classpath
|
||||
*.settings/org.eclipse.jdt.apt.core.prefs
|
||||
*.settings/org.eclipse.jdt.core.prefs
|
||||
*.settings/org.eclipse.m2e.core.prefs
|
||||
|
||||
# Certificates
|
||||
*.p12
|
||||
*.pem
|
@ -224,7 +224,11 @@
|
||||
<artifactId>bytes</artifactId>
|
||||
<version>1.5.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.ganskef</groupId>
|
||||
<artifactId>littleproxy-mitm</artifactId>
|
||||
<version>1.1.0</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package gearth.protocol;
|
||||
|
||||
import gearth.misc.listenerpattern.Observable;
|
||||
import gearth.protocol.connection.proxy.nitro.NitroProxyProvider;
|
||||
import gearth.services.packet_info.PacketInfoManager;
|
||||
import gearth.protocol.connection.HClient;
|
||||
import gearth.protocol.connection.HProxy;
|
||||
@ -68,6 +69,12 @@ public class HConnection {
|
||||
startMITM();
|
||||
}
|
||||
|
||||
public void startNitro() {
|
||||
HConnection selff = this;
|
||||
proxyProvider = new NitroProxyProvider(proxy -> selff.proxy = proxy, selff::setState, this);
|
||||
startMITM();
|
||||
}
|
||||
|
||||
private void startMITM() {
|
||||
try {
|
||||
if (proxyProvider != null) {
|
||||
|
@ -0,0 +1,53 @@
|
||||
package gearth.protocol.connection.proxy.nitro;
|
||||
|
||||
import gearth.protocol.HConnection;
|
||||
import gearth.protocol.connection.HProxySetter;
|
||||
import gearth.protocol.connection.HState;
|
||||
import gearth.protocol.connection.HStateSetter;
|
||||
import gearth.protocol.connection.proxy.ProxyProvider;
|
||||
import gearth.protocol.connection.proxy.nitro.http.NitroHttpProxy;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class NitroProxyProvider implements ProxyProvider {
|
||||
|
||||
private final HProxySetter proxySetter;
|
||||
private final HStateSetter stateSetter;
|
||||
private final HConnection hConnection;
|
||||
private final NitroHttpProxy nitroProxy;
|
||||
|
||||
public NitroProxyProvider(HProxySetter proxySetter, HStateSetter stateSetter, HConnection hConnection) {
|
||||
this.proxySetter = proxySetter;
|
||||
this.stateSetter = stateSetter;
|
||||
this.hConnection = hConnection;
|
||||
this.nitroProxy = new NitroHttpProxy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() throws IOException {
|
||||
if (!nitroProxy.start()) {
|
||||
System.out.println("Failed to start nitro proxy");
|
||||
|
||||
stateSetter.setState(HState.NOT_CONNECTED);
|
||||
return;
|
||||
}
|
||||
|
||||
stateSetter.setState(HState.WAITING_FOR_CLIENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void abort() {
|
||||
stateSetter.setState(HState.ABORTING);
|
||||
|
||||
new Thread(() -> {
|
||||
try {
|
||||
nitroProxy.stop();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
stateSetter.setState(HState.NOT_CONNECTED);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package gearth.protocol.connection.proxy.nitro.http;
|
||||
|
||||
import org.littleshoot.proxy.mitm.Authority;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class NitroAuthority extends Authority {
|
||||
|
||||
private static final String CERT_ALIAS = "gearth-nitro";
|
||||
private static final String CERT_ORGANIZATION = "G-Earth Nitro";
|
||||
private static final String CERT_DESCRIPTION = "G-Earth nitro support";
|
||||
|
||||
public NitroAuthority() {
|
||||
super(new File("."),
|
||||
CERT_ALIAS,
|
||||
"verysecure".toCharArray(),
|
||||
CERT_DESCRIPTION,
|
||||
CERT_ORGANIZATION,
|
||||
"Certificate Authority",
|
||||
CERT_ORGANIZATION,
|
||||
CERT_DESCRIPTION);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
package gearth.protocol.connection.proxy.nitro.http;
|
||||
|
||||
import gearth.protocol.connection.proxy.nitro.os.NitroOsFunctions;
|
||||
import gearth.protocol.connection.proxy.nitro.os.NitroOsFunctionsFactory;
|
||||
import org.littleshoot.proxy.HttpProxyServer;
|
||||
import org.littleshoot.proxy.impl.DefaultHttpProxyServer;
|
||||
import org.littleshoot.proxy.mitm.Authority;
|
||||
import org.littleshoot.proxy.mitm.CertificateSniffingMitmManager;
|
||||
import org.littleshoot.proxy.mitm.RootCertificateException;
|
||||
|
||||
public class NitroHttpProxy {
|
||||
|
||||
private final Authority authority;
|
||||
private final NitroOsFunctions osFunctions;
|
||||
|
||||
private HttpProxyServer proxyServer = null;
|
||||
|
||||
public NitroHttpProxy() {
|
||||
this.authority = new NitroAuthority();
|
||||
this.osFunctions = NitroOsFunctionsFactory.create();
|
||||
}
|
||||
|
||||
private boolean initializeCertificate() {
|
||||
return this.osFunctions.installRootCertificate(this.authority.aliasFile(".pem"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register HTTP(s) proxy on the system.
|
||||
*/
|
||||
private boolean registerProxy() {
|
||||
return this.osFunctions.registerSystemProxy("127.0.0.1", 9090);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister HTTP(s) proxy from system.
|
||||
*/
|
||||
private boolean unregisterProxy() {
|
||||
return this.osFunctions.unregisterSystemProxy();
|
||||
}
|
||||
|
||||
public boolean start() {
|
||||
|
||||
|
||||
try {
|
||||
proxyServer = DefaultHttpProxyServer.bootstrap()
|
||||
.withPort(9090)
|
||||
.withManInTheMiddle(new CertificateSniffingMitmManager(authority))
|
||||
// TODO: Replace lambda with some class
|
||||
.withFiltersSource(new NitroHttpProxyFilterSource((configUrl, websocketUrl) -> {
|
||||
System.out.printf("Found %s at %s%n", websocketUrl, configUrl);
|
||||
|
||||
return "wss://127.0.0.1:2096";
|
||||
}))
|
||||
.start();
|
||||
|
||||
if (!initializeCertificate()) {
|
||||
proxyServer.stop();
|
||||
|
||||
System.out.println("Failed to initialize certificate");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!registerProxy()) {
|
||||
proxyServer.stop();
|
||||
|
||||
System.out.println("Failed to register certificate");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (RootCertificateException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (!unregisterProxy()) {
|
||||
System.out.println("Failed to unregister system proxy, please check manually");
|
||||
}
|
||||
|
||||
if (proxyServer == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
proxyServer.stop();
|
||||
proxyServer = null;
|
||||
}
|
||||
}
|
@ -0,0 +1,124 @@
|
||||
package gearth.protocol.connection.proxy.nitro.http;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.http.*;
|
||||
import io.netty.util.CharsetUtil;
|
||||
import org.littleshoot.proxy.HttpFiltersAdapter;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class NitroHttpProxyFilter extends HttpFiltersAdapter {
|
||||
|
||||
private static final String NitroConfigSearch = "\"socket.url\"";
|
||||
private static final Pattern NitroConfigPattern = Pattern.compile("\"socket\\.url\":.?\"(wss?://.*?)\"", Pattern.MULTILINE);
|
||||
|
||||
private static final String HeaderAcceptEncoding = "Accept-Encoding";
|
||||
private static final String HeaderAge = "Age";
|
||||
private static final String HeaderCacheControl = "Cache-Control";
|
||||
private static final String HeaderETag = "ETag";
|
||||
private static final String HeaderIfNoneMatch = "If-None-Match";
|
||||
private static final String HeaderIfModifiedSince = "If-Modified-Since";
|
||||
private static final String HeaderLastModified = "Last-Modified";
|
||||
|
||||
private final NitroHttpProxyServerCallback callback;
|
||||
private final String url;
|
||||
|
||||
public NitroHttpProxyFilter(HttpRequest originalRequest, ChannelHandlerContext ctx, NitroHttpProxyServerCallback callback, String url) {
|
||||
super(originalRequest, ctx);
|
||||
this.callback = callback;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpResponse clientToProxyRequest(HttpObject httpObject) {
|
||||
if (httpObject instanceof HttpRequest) {
|
||||
HttpRequest request = (HttpRequest) httpObject;
|
||||
HttpHeaders headers = request.headers();
|
||||
|
||||
// Only support gzip or deflate.
|
||||
// The LittleProxy library does not support brotli.
|
||||
if (headers.contains(HeaderAcceptEncoding)) {
|
||||
String encoding = headers.get(HeaderAcceptEncoding);
|
||||
|
||||
if (encoding.contains("br")) {
|
||||
if (encoding.contains("gzip") && encoding.contains("deflate")) {
|
||||
headers.set(HeaderAcceptEncoding, "gzip, deflate");
|
||||
} else if (encoding.contains("gzip")) {
|
||||
headers.set(HeaderAcceptEncoding, "gzip, deflate");
|
||||
} else {
|
||||
headers.remove(HeaderAcceptEncoding);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Disable caching.
|
||||
stripCacheHeaders(headers);
|
||||
}
|
||||
|
||||
return super.clientToProxyRequest(httpObject);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpObject serverToProxyResponse(HttpObject httpObject) {
|
||||
if (httpObject instanceof FullHttpResponse) {
|
||||
final FullHttpResponse response = (FullHttpResponse) httpObject;
|
||||
|
||||
// Find nitro configuration file.
|
||||
boolean responseModified = false;
|
||||
String responseBody = responseRead(response);
|
||||
|
||||
if (responseBody.contains(NitroConfigSearch)) {
|
||||
final Matcher matcher = NitroConfigPattern.matcher(responseBody);
|
||||
|
||||
if (matcher.find()) {
|
||||
final String originalWebsocket = matcher.group(1);
|
||||
final String replacementWebsocket = callback.replaceWebsocketServer(url, originalWebsocket);
|
||||
|
||||
if (replacementWebsocket != null) {
|
||||
responseBody = responseBody.replace(originalWebsocket, replacementWebsocket);
|
||||
responseModified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply changes.
|
||||
if (responseModified) {
|
||||
responseWrite(response, responseBody);
|
||||
}
|
||||
}
|
||||
|
||||
return httpObject;
|
||||
}
|
||||
|
||||
private static String responseRead(FullHttpResponse response) {
|
||||
final ByteBuf contentBuf = response.content();
|
||||
return contentBuf.toString(CharsetUtil.UTF_8);
|
||||
}
|
||||
|
||||
private static void responseWrite(FullHttpResponse response, String content) {
|
||||
final byte[] body = content.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
// Update content.
|
||||
response.content().clear().writeBytes(body);
|
||||
|
||||
// Update content-length.
|
||||
HttpHeaders.setContentLength(response, body.length);
|
||||
|
||||
// Ensure modified response is not cached.
|
||||
stripCacheHeaders(response.headers());
|
||||
}
|
||||
|
||||
private static void stripCacheHeaders(HttpHeaders headers) {
|
||||
headers.remove(HeaderAcceptEncoding);
|
||||
headers.remove(HeaderAge);
|
||||
headers.remove(HeaderCacheControl);
|
||||
headers.remove(HeaderETag);
|
||||
headers.remove(HeaderIfNoneMatch);
|
||||
headers.remove(HeaderIfModifiedSince);
|
||||
headers.remove(HeaderLastModified);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package gearth.protocol.connection.proxy.nitro.http;
|
||||
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.http.HttpMethod;
|
||||
import io.netty.handler.codec.http.HttpRequest;
|
||||
import io.netty.util.AttributeKey;
|
||||
import org.littleshoot.proxy.HttpFilters;
|
||||
import org.littleshoot.proxy.HttpFiltersAdapter;
|
||||
import org.littleshoot.proxy.HttpFiltersSourceAdapter;
|
||||
|
||||
public class NitroHttpProxyFilterSource extends HttpFiltersSourceAdapter {
|
||||
|
||||
private static final AttributeKey<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 1024 * 1024 * 1024;
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package gearth.protocol.connection.proxy.nitro.http;
|
||||
|
||||
public interface NitroHttpProxyServerCallback {
|
||||
|
||||
/**
|
||||
* Specify a replacement for the given websocket url.
|
||||
*
|
||||
* @param configUrl The url at which the websocket url was found.
|
||||
* @param websocketUrl The hotel websocket url.
|
||||
* @return Return null to not replace anything, otherwise specify an alternative websocket url.
|
||||
*/
|
||||
String replaceWebsocketServer(String configUrl, String websocketUrl);
|
||||
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package gearth.protocol.connection.proxy.nitro.os;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public interface NitroOsFunctions {
|
||||
|
||||
boolean installRootCertificate(File certificate);
|
||||
|
||||
boolean registerSystemProxy(String host, int port);
|
||||
|
||||
boolean unregisterSystemProxy();
|
||||
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package gearth.protocol.connection.proxy.nitro.os;
|
||||
|
||||
import gearth.misc.OSValidator;
|
||||
import gearth.protocol.connection.proxy.nitro.os.windows.NitroWindows;
|
||||
import org.apache.commons.lang3.NotImplementedException;
|
||||
|
||||
public final class NitroOsFunctionsFactory {
|
||||
|
||||
public static NitroOsFunctions create() {
|
||||
if (OSValidator.isWindows()) {
|
||||
return new NitroWindows();
|
||||
}
|
||||
|
||||
if (OSValidator.isUnix()) {
|
||||
throw new NotImplementedException("unix nitro is not implemented yet");
|
||||
}
|
||||
|
||||
throw new NotImplementedException("macOS nitro is not implemented yet");
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package gearth.protocol.connection.proxy.nitro.os.windows;
|
||||
|
||||
import gearth.protocol.connection.proxy.nitro.os.NitroOsFunctions;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class NitroWindows implements NitroOsFunctions {
|
||||
|
||||
@Override
|
||||
public boolean installRootCertificate(File certificate) {
|
||||
// TODO: Prompt registration
|
||||
System.out.println(certificate.toString());
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean registerSystemProxy(String host, int port) {
|
||||
try {
|
||||
final String proxy = String.format("%s:%d", host, port);
|
||||
Runtime.getRuntime().exec("reg add \"HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\" /v ProxyServer /t REG_SZ /d \"" + proxy + "\" /f");
|
||||
Runtime.getRuntime().exec("reg add \"HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\" /v ProxyEnable /t REG_DWORD /d 1 /f");
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean unregisterSystemProxy() {
|
||||
try {
|
||||
Runtime.getRuntime().exec("reg add \"HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\" /v ProxyEnable /t REG_DWORD /d 0 /f");
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
@ -246,6 +246,10 @@ public class ConnectionController extends SubForm {
|
||||
Platform.runLater(() -> rd_unity.setSelected(true));
|
||||
getHConnection().startUnity();
|
||||
}
|
||||
else if (connectMode.equals("nitro")) {
|
||||
Platform.runLater(() -> rd_nitro.setSelected(true));
|
||||
getHConnection().startNitro();
|
||||
}
|
||||
Platform.runLater(this::updateInputUI);
|
||||
}
|
||||
}
|
||||
@ -255,16 +259,16 @@ public class ConnectionController extends SubForm {
|
||||
|
||||
btnConnect.setDisable(true);
|
||||
new Thread(() -> {
|
||||
if (useFlash()) {
|
||||
if (isClientMode(HClient.FLASH)) {
|
||||
if (cbx_autodetect.isSelected()) {
|
||||
getHConnection().start();
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
getHConnection().start(inpHost.getEditor().getText(), Integer.parseInt(inpPort.getEditor().getText()));
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else if (isClientMode(HClient.UNITY)) {
|
||||
getHConnection().startUnity();
|
||||
} else if (isClientMode(HClient.NITRO)) {
|
||||
getHConnection().startNitro();
|
||||
}
|
||||
|
||||
|
||||
@ -297,4 +301,17 @@ public class ConnectionController extends SubForm {
|
||||
private boolean useFlash() {
|
||||
return rd_flash.isSelected();
|
||||
}
|
||||
|
||||
private boolean isClientMode(HClient client) {
|
||||
switch (client) {
|
||||
case FLASH:
|
||||
return rd_flash.isSelected();
|
||||
case UNITY:
|
||||
return rd_unity.isSelected();
|
||||
case NITRO:
|
||||
return rd_nitro.isSelected();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user