Add cloudflare cookies support for Nitro WebSocket connections

This commit is contained in:
UnfamiliarLegacy 2022-12-05 02:25:14 +01:00
parent 0cdb5e6465
commit c4c2d2bfe1
6 changed files with 114 additions and 37 deletions

View File

@ -25,7 +25,7 @@ public class NitroProxyProvider implements ProxyProvider, NitroHttpProxyServerCa
private final AtomicBoolean abortLock; private final AtomicBoolean abortLock;
private String originalWebsocketUrl; private String originalWebsocketUrl;
private String originalOriginUrl; private String originalCookies;
public NitroProxyProvider(HProxySetter proxySetter, HStateSetter stateSetter, HConnection connection) { public NitroProxyProvider(HProxySetter proxySetter, HStateSetter stateSetter, HConnection connection) {
this.proxySetter = proxySetter; this.proxySetter = proxySetter;
@ -40,12 +40,15 @@ public class NitroProxyProvider implements ProxyProvider, NitroHttpProxyServerCa
return originalWebsocketUrl; return originalWebsocketUrl;
} }
public String getOriginalOriginUrl() { public String getOriginalCookies() {
return originalOriginUrl; return originalCookies;
} }
@Override @Override
public void start() throws IOException { public void start() throws IOException {
originalWebsocketUrl = null;
originalCookies = null;
connection.getStateObservable().addListener(this); connection.getStateObservable().addListener(this);
if (!nitroHttpProxy.start()) { if (!nitroHttpProxy.start()) {
@ -97,11 +100,15 @@ public class NitroProxyProvider implements ProxyProvider, NitroHttpProxyServerCa
@Override @Override
public String replaceWebsocketServer(String configUrl, String websocketUrl) { public String replaceWebsocketServer(String configUrl, String websocketUrl) {
originalWebsocketUrl = websocketUrl; originalWebsocketUrl = websocketUrl;
originalOriginUrl = extractOriginUrl(configUrl);
return String.format("ws://127.0.0.1:%d", NitroConstants.WEBSOCKET_PORT); return String.format("ws://127.0.0.1:%d", NitroConstants.WEBSOCKET_PORT);
} }
@Override
public void setOriginCookies(String cookieHeaderValue) {
originalCookies = cookieHeaderValue;
}
@Override @Override
public void stateChanged(HState oldState, HState newState) { public void stateChanged(HState oldState, HState newState) {
if (oldState == HState.WAITING_FOR_CLIENT && newState == HState.CONNECTED) { if (oldState == HState.WAITING_FOR_CLIENT && newState == HState.CONNECTED) {
@ -115,15 +122,4 @@ public class NitroProxyProvider implements ProxyProvider, NitroHttpProxyServerCa
abort(); abort();
} }
} }
private static String extractOriginUrl(String url) {
try {
final URI uri = new URI(url);
return String.format("%s://%s/", uri.getScheme(), uri.getHost());
} catch (URISyntaxException e) {
e.printStackTrace();
}
return null;
}
} }

View File

@ -1,6 +1,5 @@
package gearth.protocol.connection.proxy.nitro.http; package gearth.protocol.connection.proxy.nitro.http;
import gearth.GEarth;
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;
@ -10,8 +9,6 @@ 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 javafx.scene.image.Image;
import javafx.stage.Stage;
import org.littleshoot.proxy.HttpProxyServer; import org.littleshoot.proxy.HttpProxyServer;
import org.littleshoot.proxy.impl.DefaultHttpProxyServer; import org.littleshoot.proxy.impl.DefaultHttpProxyServer;
import org.littleshoot.proxy.mitm.Authority; import org.littleshoot.proxy.mitm.Authority;

View File

@ -7,6 +7,10 @@ import io.netty.util.CharsetUtil;
import org.littleshoot.proxy.HttpFiltersAdapter; import org.littleshoot.proxy.HttpFiltersAdapter;
import java.nio.charset.StandardCharsets; 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.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -16,6 +20,17 @@ public class NitroHttpProxyFilter extends HttpFiltersAdapter {
private static final String NitroClientSearch = "configurationUrls:"; private static final String NitroClientSearch = "configurationUrls:";
private static final Pattern NitroConfigPattern = Pattern.compile("\"socket\\.url\":.?\"(wss?://.*?)\"", Pattern.MULTILINE); private static final Pattern NitroConfigPattern = Pattern.compile("\"socket\\.url\":.?\"(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",
"cf_ob_info",
"cf_use_ob",
"__cfwaitingroom",
"__cfruid",
"cf_clearance"
));
private static final String HeaderAcceptEncoding = "Accept-Encoding"; private static final String HeaderAcceptEncoding = "Accept-Encoding";
private static final String HeaderAge = "Age"; private static final String HeaderAge = "Age";
private static final String HeaderCacheControl = "Cache-Control"; private static final String HeaderCacheControl = "Cache-Control";
@ -27,6 +42,7 @@ public class NitroHttpProxyFilter extends HttpFiltersAdapter {
private final NitroHttpProxyServerCallback callback; private final NitroHttpProxyServerCallback callback;
private final String url; private final String url;
private String cookies;
public NitroHttpProxyFilter(HttpRequest originalRequest, ChannelHandlerContext ctx, NitroHttpProxyServerCallback callback, String url) { public NitroHttpProxyFilter(HttpRequest originalRequest, ChannelHandlerContext ctx, NitroHttpProxyServerCallback callback, String url) {
super(originalRequest, ctx); super(originalRequest, ctx);
@ -58,6 +74,9 @@ public class NitroHttpProxyFilter extends HttpFiltersAdapter {
// Disable caching. // Disable caching.
stripCacheHeaders(headers); stripCacheHeaders(headers);
// Find relevant cookies for the WebSocket connection.
this.cookies = parseCookies(request);
} }
return super.clientToProxyRequest(httpObject); return super.clientToProxyRequest(httpObject);
@ -77,13 +96,15 @@ public class NitroHttpProxyFilter extends HttpFiltersAdapter {
if (matcher.find()) { if (matcher.find()) {
final String originalWebsocket = matcher.group(1); final String originalWebsocket = matcher.group(1);
final String replacementWebsocket = callback.replaceWebsocketServer(url, originalWebsocket); final String replacementWebsocket = callback.replaceWebsocketServer(this.url, originalWebsocket);
if (replacementWebsocket != null) { if (replacementWebsocket != null) {
responseBody = responseBody.replace(originalWebsocket, replacementWebsocket); responseBody = responseBody.replace(originalWebsocket, replacementWebsocket);
responseModified = true; responseModified = true;
} }
} }
callback.setOriginCookies(this.cookies);
} }
// Apply changes. // Apply changes.
@ -100,6 +121,32 @@ public class NitroHttpProxyFilter extends HttpFiltersAdapter {
return httpObject; 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. * Modify Content-Security-Policy header, which could prevent Nitro from connecting with G-Earth.
*/ */
@ -148,5 +195,4 @@ public class NitroHttpProxyFilter extends HttpFiltersAdapter {
headers.remove(HeaderIfModifiedSince); headers.remove(HeaderIfModifiedSince);
headers.remove(HeaderLastModified); headers.remove(HeaderLastModified);
} }
} }

View File

@ -11,4 +11,9 @@ public interface NitroHttpProxyServerCallback {
*/ */
String replaceWebsocketServer(String configUrl, String websocketUrl); String replaceWebsocketServer(String configUrl, String websocketUrl);
/**
* Sets the parsed cookies for the origin WebSocket connection.
*/
void setOriginCookies(String cookieHeaderValue);
} }

View File

@ -6,10 +6,18 @@ import gearth.protocol.connection.*;
import gearth.protocol.connection.proxy.nitro.NitroConstants; import gearth.protocol.connection.proxy.nitro.NitroConstants;
import gearth.protocol.connection.proxy.nitro.NitroProxyProvider; import gearth.protocol.connection.proxy.nitro.NitroProxyProvider;
import gearth.protocol.packethandler.nitro.NitroPacketHandler; import gearth.protocol.packethandler.nitro.NitroPacketHandler;
import org.eclipse.jetty.websocket.common.WebSocketSession;
import org.eclipse.jetty.websocket.jsr356.JsrSession;
import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
import javax.websocket.*; import javax.websocket.*;
import javax.websocket.server.ServerEndpoint; import javax.websocket.server.ServerEndpoint;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@ServerEndpoint(value = "/") @ServerEndpoint(value = "/")
@ -23,7 +31,7 @@ public class NitroWebsocketClient implements NitroSession {
private final NitroPacketHandler packetHandler; private final NitroPacketHandler packetHandler;
private final AtomicBoolean shutdownLock; private final AtomicBoolean shutdownLock;
private Session activeSession = null; private JsrSession activeSession = null;
public NitroWebsocketClient(HProxySetter proxySetter, HStateSetter stateSetter, HConnection connection, NitroProxyProvider proxyProvider) { public NitroWebsocketClient(HProxySetter proxySetter, HStateSetter stateSetter, HConnection connection, NitroProxyProvider proxyProvider) {
this.proxySetter = proxySetter; this.proxySetter = proxySetter;
@ -36,11 +44,19 @@ public class NitroWebsocketClient implements NitroSession {
} }
@OnOpen @OnOpen
public void onOpen(Session session) throws IOException { public void onOpen(Session session) throws Exception {
activeSession = session; activeSession = (JsrSession) session;
activeSession.setMaxBinaryMessageBufferSize(NitroConstants.WEBSOCKET_BUFFER_SIZE); activeSession.setMaxBinaryMessageBufferSize(NitroConstants.WEBSOCKET_BUFFER_SIZE);
server.connect(proxyProvider.getOriginalWebsocketUrl(), proxyProvider.getOriginalOriginUrl()); // Set proper headers to spoof being a real client.
final Map<String, List<String>> headers = new HashMap<>(activeSession.getUpgradeRequest().getHeaders());
if (proxyProvider.getOriginalCookies() != null) {
headers.put("Cookie", Collections.singletonList(proxyProvider.getOriginalCookies()));
}
// Connect to origin server.
server.connect(proxyProvider.getOriginalWebsocketUrl(), headers);
final HProxy proxy = new HProxy(HClient.NITRO, "", "", -1, -1, ""); final HProxy proxy = new HProxy(HClient.NITRO, "", "", -1, -1, "");
@ -89,7 +105,7 @@ public class NitroWebsocketClient implements NitroSession {
try { try {
activeSession.close(); activeSession.close();
} catch (IOException e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} finally { } finally {
activeSession = null; activeSession = null;

View File

@ -5,16 +5,26 @@ import gearth.protocol.HMessage;
import gearth.protocol.connection.proxy.nitro.NitroConstants; import gearth.protocol.connection.proxy.nitro.NitroConstants;
import gearth.protocol.packethandler.PacketHandler; import gearth.protocol.packethandler.PacketHandler;
import gearth.protocol.packethandler.nitro.NitroPacketHandler; import gearth.protocol.packethandler.nitro.NitroPacketHandler;
import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig;
import org.eclipse.jetty.websocket.common.extensions.compress.PerMessageDeflateExtension;
import org.eclipse.jetty.websocket.jsr356.JsrExtension;
import javax.websocket.*; import javax.websocket.*;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.util.Collections; import java.util.*;
import java.util.List;
import java.util.Map;
public class NitroWebsocketServer extends Endpoint implements NitroSession { public class NitroWebsocketServer extends Endpoint implements NitroSession {
private static final HashSet<String> SKIP_HEADERS = new HashSet<>(Arrays.asList(
"Sec-WebSocket-Extensions",
"Sec-WebSocket-Key",
"Sec-WebSocket-Version",
"Host",
"Connection",
"Upgrade"
));
private final PacketHandler packetHandler; private final PacketHandler packetHandler;
private final NitroWebsocketClient client; private final NitroWebsocketClient client;
private Session activeSession = null; private Session activeSession = null;
@ -24,18 +34,25 @@ public class NitroWebsocketServer extends Endpoint implements NitroSession {
this.packetHandler = new NitroPacketHandler(HMessage.Direction.TOCLIENT, client, connection.getExtensionHandler(), connection.getTrafficObservables()); this.packetHandler = new NitroPacketHandler(HMessage.Direction.TOCLIENT, client, connection.getExtensionHandler(), connection.getTrafficObservables());
} }
public void connect(String websocketUrl, String originUrl) throws IOException { public void connect(String websocketUrl, Map<String, List<String>> clientHeaders) throws IOException {
try { try {
ClientEndpointConfig.Builder builder = ClientEndpointConfig.Builder.create(); ClientEndpointConfig.Builder builder = ClientEndpointConfig.Builder.create();
if (originUrl != null) { builder.extensions(Collections.singletonList(new JsrExtension(new ExtensionConfig("permessage-deflate;client_max_window_bits"))));
builder.configurator(new ClientEndpointConfig.Configurator() { builder.configurator(new ClientEndpointConfig.Configurator() {
@Override @Override
public void beforeRequest(Map<String, List<String>> headers) { public void beforeRequest(Map<String, List<String>> headers) {
headers.put("Origin", Collections.singletonList(originUrl)); clientHeaders.forEach((key, value) -> {
if (SKIP_HEADERS.contains(key)) {
return;
} }
headers.remove(key);
headers.put(key, value);
}); });
} }
});
ClientEndpointConfig config = builder.build(); ClientEndpointConfig config = builder.build();