diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/NitroProxyProvider.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/NitroProxyProvider.java index 5b90922..a756d6a 100644 --- a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/NitroProxyProvider.java +++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/NitroProxyProvider.java @@ -25,7 +25,7 @@ public class NitroProxyProvider implements ProxyProvider, NitroHttpProxyServerCa private final AtomicBoolean abortLock; private String originalWebsocketUrl; - private String originalOriginUrl; + private String originalCookies; public NitroProxyProvider(HProxySetter proxySetter, HStateSetter stateSetter, HConnection connection) { this.proxySetter = proxySetter; @@ -40,12 +40,15 @@ public class NitroProxyProvider implements ProxyProvider, NitroHttpProxyServerCa return originalWebsocketUrl; } - public String getOriginalOriginUrl() { - return originalOriginUrl; + public String getOriginalCookies() { + return originalCookies; } @Override public void start() throws IOException { + originalWebsocketUrl = null; + originalCookies = null; + connection.getStateObservable().addListener(this); if (!nitroHttpProxy.start()) { @@ -97,11 +100,15 @@ public class NitroProxyProvider implements ProxyProvider, NitroHttpProxyServerCa @Override public String replaceWebsocketServer(String configUrl, String websocketUrl) { originalWebsocketUrl = websocketUrl; - originalOriginUrl = extractOriginUrl(configUrl); return String.format("ws://127.0.0.1:%d", NitroConstants.WEBSOCKET_PORT); } + @Override + public void setOriginCookies(String cookieHeaderValue) { + originalCookies = cookieHeaderValue; + } + @Override public void stateChanged(HState oldState, HState newState) { if (oldState == HState.WAITING_FOR_CLIENT && newState == HState.CONNECTED) { @@ -115,15 +122,4 @@ public class NitroProxyProvider implements ProxyProvider, NitroHttpProxyServerCa 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; - } } diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxy.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxy.java index f056564..670640b 100644 --- a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxy.java +++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxy.java @@ -1,6 +1,5 @@ package gearth.protocol.connection.proxy.nitro.http; -import gearth.GEarth; import gearth.misc.ConfirmationDialog; import gearth.protocol.connection.proxy.nitro.NitroConstants; 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.ButtonType; import javafx.scene.control.Label; -import javafx.scene.image.Image; -import javafx.stage.Stage; import org.littleshoot.proxy.HttpProxyServer; import org.littleshoot.proxy.impl.DefaultHttpProxyServer; import org.littleshoot.proxy.mitm.Authority; diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxyFilter.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxyFilter.java index ea0cddc..b9795ec 100644 --- a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxyFilter.java +++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxyFilter.java @@ -7,6 +7,10 @@ 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; @@ -16,6 +20,17 @@ public class NitroHttpProxyFilter extends HttpFiltersAdapter { private static final String NitroClientSearch = "configurationUrls:"; 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 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 HeaderAge = "Age"; private static final String HeaderCacheControl = "Cache-Control"; @@ -27,6 +42,7 @@ public class NitroHttpProxyFilter extends HttpFiltersAdapter { private final NitroHttpProxyServerCallback callback; private final String url; + private String cookies; public NitroHttpProxyFilter(HttpRequest originalRequest, ChannelHandlerContext ctx, NitroHttpProxyServerCallback callback, String url) { super(originalRequest, ctx); @@ -58,6 +74,9 @@ public class NitroHttpProxyFilter extends HttpFiltersAdapter { // Disable caching. stripCacheHeaders(headers); + + // Find relevant cookies for the WebSocket connection. + this.cookies = parseCookies(request); } return super.clientToProxyRequest(httpObject); @@ -77,13 +96,15 @@ public class NitroHttpProxyFilter extends HttpFiltersAdapter { if (matcher.find()) { final String originalWebsocket = matcher.group(1); - final String replacementWebsocket = callback.replaceWebsocketServer(url, originalWebsocket); + final String replacementWebsocket = callback.replaceWebsocketServer(this.url, originalWebsocket); if (replacementWebsocket != null) { responseBody = responseBody.replace(originalWebsocket, replacementWebsocket); responseModified = true; } } + + callback.setOriginCookies(this.cookies); } // Apply changes. @@ -100,6 +121,32 @@ public class NitroHttpProxyFilter extends HttpFiltersAdapter { 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 result = new ArrayList<>(); + final List 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. */ @@ -148,5 +195,4 @@ public class NitroHttpProxyFilter extends HttpFiltersAdapter { headers.remove(HeaderIfModifiedSince); headers.remove(HeaderLastModified); } - } diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxyServerCallback.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxyServerCallback.java index 3f04f11..c90b5dc 100644 --- a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxyServerCallback.java +++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxyServerCallback.java @@ -11,4 +11,9 @@ public interface NitroHttpProxyServerCallback { */ String replaceWebsocketServer(String configUrl, String websocketUrl); + /** + * Sets the parsed cookies for the origin WebSocket connection. + */ + void setOriginCookies(String cookieHeaderValue); + } diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroWebsocketClient.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroWebsocketClient.java index c940962..0c06f62 100644 --- a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroWebsocketClient.java +++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroWebsocketClient.java @@ -6,10 +6,18 @@ import gearth.protocol.connection.*; import gearth.protocol.connection.proxy.nitro.NitroConstants; import gearth.protocol.connection.proxy.nitro.NitroProxyProvider; 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.server.ServerEndpoint; 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; @ServerEndpoint(value = "/") @@ -23,7 +31,7 @@ public class NitroWebsocketClient implements NitroSession { private final NitroPacketHandler packetHandler; private final AtomicBoolean shutdownLock; - private Session activeSession = null; + private JsrSession activeSession = null; public NitroWebsocketClient(HProxySetter proxySetter, HStateSetter stateSetter, HConnection connection, NitroProxyProvider proxyProvider) { this.proxySetter = proxySetter; @@ -36,11 +44,19 @@ public class NitroWebsocketClient implements NitroSession { } @OnOpen - public void onOpen(Session session) throws IOException { - activeSession = session; + public void onOpen(Session session) throws Exception { + activeSession = (JsrSession) session; activeSession.setMaxBinaryMessageBufferSize(NitroConstants.WEBSOCKET_BUFFER_SIZE); - server.connect(proxyProvider.getOriginalWebsocketUrl(), proxyProvider.getOriginalOriginUrl()); + // Set proper headers to spoof being a real client. + final Map> 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, ""); @@ -89,7 +105,7 @@ public class NitroWebsocketClient implements NitroSession { try { activeSession.close(); - } catch (IOException e) { + } catch (Exception e) { e.printStackTrace(); } finally { activeSession = null; diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroWebsocketServer.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroWebsocketServer.java index 86c5ad0..d1017d7 100644 --- a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroWebsocketServer.java +++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroWebsocketServer.java @@ -5,16 +5,26 @@ import gearth.protocol.HMessage; import gearth.protocol.connection.proxy.nitro.NitroConstants; import gearth.protocol.packethandler.PacketHandler; 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 java.io.IOException; import java.net.URI; -import java.util.Collections; -import java.util.List; -import java.util.Map; +import java.util.*; public class NitroWebsocketServer extends Endpoint implements NitroSession { + private static final HashSet SKIP_HEADERS = new HashSet<>(Arrays.asList( + "Sec-WebSocket-Extensions", + "Sec-WebSocket-Key", + "Sec-WebSocket-Version", + "Host", + "Connection", + "Upgrade" + )); + private final PacketHandler packetHandler; private final NitroWebsocketClient client; 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()); } - public void connect(String websocketUrl, String originUrl) throws IOException { + public void connect(String websocketUrl, Map> clientHeaders) throws IOException { try { ClientEndpointConfig.Builder builder = ClientEndpointConfig.Builder.create(); - if (originUrl != null) { - builder.configurator(new ClientEndpointConfig.Configurator() { - @Override - public void beforeRequest(Map> headers) { - headers.put("Origin", Collections.singletonList(originUrl)); - } - }); - } + builder.extensions(Collections.singletonList(new JsrExtension(new ExtensionConfig("permessage-deflate;client_max_window_bits")))); + + builder.configurator(new ClientEndpointConfig.Configurator() { + @Override + public void beforeRequest(Map> headers) { + clientHeaders.forEach((key, value) -> { + if (SKIP_HEADERS.contains(key)) { + return; + } + + headers.remove(key); + headers.put(key, value); + }); + } + }); ClientEndpointConfig config = builder.build();