Merge pull request #107 from UnfamiliarLegacy/feature/nitro

Add nitro support
This commit is contained in:
sirjonasxx 2021-11-26 21:33:28 +01:00 committed by GitHub
commit 5bd797c4b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1105 additions and 19 deletions

4
.gitignore vendored
View File

@ -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

View File

@ -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>

View File

@ -0,0 +1,31 @@
package gearth.misc;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public final class RuntimeUtil {
public static String getCommandOutput(String[] command) throws IOException {
try {
final Runtime rt = Runtime.getRuntime();
final Process proc = rt.exec(command);
final BufferedReader stdInput = new BufferedReader(new InputStreamReader(proc.getInputStream()));
final StringBuilder result = new StringBuilder();
String line;
while ((line = stdInput.readLine()) != null) {
result.append(line);
result.append("\n");
}
return result.toString();
} catch (IOException e) {
e.printStackTrace();
throw e;
}
}
}

View File

@ -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) {

View File

@ -2,5 +2,6 @@ package gearth.protocol.connection;
public enum HClient {
UNITY,
FLASH
FLASH,
NITRO
}

View File

@ -0,0 +1,13 @@
package gearth.protocol.connection.proxy.nitro;
public final class NitroConstants {
public static final int HTTP_PORT = 9090;
public static final int HTTP_BUFFER_SIZE = 1024 * 1024 * 10;
public static final int WEBSOCKET_PORT = 2096;
public static final int WEBSOCKET_BUFFER_SIZE = 1024 * 1024 * 10;
public static final String WEBSOCKET_REVISION = "PRODUCTION-201611291003-338511768";
public static final String WEBSOCKET_CLIENT_IDENTIFIER = "HTML5";
}

View File

@ -0,0 +1,93 @@
package gearth.protocol.connection.proxy.nitro;
import gearth.protocol.HConnection;
import gearth.protocol.StateChangeListener;
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 gearth.protocol.connection.proxy.nitro.http.NitroHttpProxyServerCallback;
import gearth.protocol.connection.proxy.nitro.websocket.NitroWebsocketProxy;
import java.io.IOException;
public class NitroProxyProvider implements ProxyProvider, NitroHttpProxyServerCallback, StateChangeListener {
private final HProxySetter proxySetter;
private final HStateSetter stateSetter;
private final HConnection connection;
private final NitroHttpProxy nitroHttpProxy;
private final NitroWebsocketProxy nitroWebsocketProxy;
private String originalWebsocketUrl;
public NitroProxyProvider(HProxySetter proxySetter, HStateSetter stateSetter, HConnection connection) {
this.proxySetter = proxySetter;
this.stateSetter = stateSetter;
this.connection = connection;
this.nitroHttpProxy = new NitroHttpProxy(this);
this.nitroWebsocketProxy = new NitroWebsocketProxy(proxySetter, stateSetter, connection, this);
}
public String getOriginalWebsocketUrl() {
return originalWebsocketUrl;
}
@Override
public void start() throws IOException {
connection.getStateObservable().addListener(this);
if (!nitroHttpProxy.start()) {
System.out.println("Failed to start nitro proxy");
abort();
return;
}
if (!nitroWebsocketProxy.start()) {
System.out.println("Failed to start nitro websocket proxy");
abort();
return;
}
stateSetter.setState(HState.WAITING_FOR_CLIENT);
}
@Override
public void abort() {
stateSetter.setState(HState.ABORTING);
new Thread(() -> {
try {
nitroHttpProxy.stop();
} catch (Exception e) {
e.printStackTrace();
}
try {
nitroWebsocketProxy.stop();
} catch (Exception e) {
e.printStackTrace();
}
stateSetter.setState(HState.NOT_CONNECTED);
connection.getStateObservable().removeListener(this);
}).start();
}
@Override
public String replaceWebsocketServer(String configUrl, String websocketUrl) {
originalWebsocketUrl = websocketUrl;
return String.format("ws://127.0.0.1:%d", NitroConstants.WEBSOCKET_PORT);
}
@Override
public void stateChanged(HState oldState, HState newState) {
if (oldState == HState.WAITING_FOR_CLIENT && newState == HState.CONNECTED) {
// Unregister but do not stop http proxy.
// We are not stopping the http proxy because some requests might still require it to be running.
nitroHttpProxy.pause();
}
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,153 @@
package gearth.protocol.connection.proxy.nitro.http;
import gearth.misc.ConfirmationDialog;
import gearth.protocol.connection.proxy.nitro.NitroConstants;
import gearth.protocol.connection.proxy.nitro.os.NitroOsFunctions;
import gearth.protocol.connection.proxy.nitro.os.NitroOsFunctionsFactory;
import javafx.application.Platform;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
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;
import java.io.File;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicBoolean;
public class NitroHttpProxy {
private static final String ADMIN_WARNING_KEY = "admin_warning_dialog";
private static final AtomicBoolean SHUTDOWN_HOOK = new AtomicBoolean();
private final Authority authority;
private final NitroOsFunctions osFunctions;
private final NitroHttpProxyServerCallback serverCallback;
private HttpProxyServer proxyServer = null;
public NitroHttpProxy(NitroHttpProxyServerCallback serverCallback) {
this.serverCallback = serverCallback;
this.authority = new NitroAuthority();
this.osFunctions = NitroOsFunctionsFactory.create();
}
private boolean initializeCertificate() {
final File certificate = this.authority.aliasFile(".pem");
// All good if certificate is already trusted.
if (this.osFunctions.isRootCertificateTrusted(certificate)) {
return true;
}
// Let the user know about admin permissions.
final Semaphore waitForDialog = new Semaphore(0);
final AtomicBoolean shouldInstall = new AtomicBoolean();
Platform.runLater(() -> {
Alert alert = ConfirmationDialog.createAlertWithOptOut(Alert.AlertType.WARNING, ADMIN_WARNING_KEY,
"Root certificate installation", null,
"G-Earth detected that you do not have the root certificate authority installed. " +
"This is required for Nitro to work, do you want to continue? " +
"G-Earth will ask you for Administrator permission if you do so.", "Remember my choice",
ButtonType.YES, ButtonType.NO
);
shouldInstall.set(!(alert.showAndWait().filter(t -> t == ButtonType.YES).isPresent()));
waitForDialog.release();
});
// Wait for dialog choice.
try {
waitForDialog.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
return false;
}
// User opted out.
if (!shouldInstall.get()) {
return false;
}
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", NitroConstants.HTTP_PORT);
}
/**
* Unregister HTTP(s) proxy from system.
*/
private boolean unregisterProxy() {
return this.osFunctions.unregisterSystemProxy();
}
public boolean start() {
setupShutdownHook();
try {
proxyServer = DefaultHttpProxyServer.bootstrap()
.withPort(NitroConstants.HTTP_PORT)
.withManInTheMiddle(new CertificateSniffingMitmManager(authority))
.withFiltersSource(new NitroHttpProxyFilterSource(serverCallback))
.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 pause() {
if (!unregisterProxy()) {
System.out.println("Failed to unregister system proxy, please check manually");
}
}
public void stop() {
pause();
if (proxyServer == null) {
return;
}
proxyServer.stop();
proxyServer = null;
}
/**
* Ensure the system proxy is removed when G-Earth exits.
* Otherwise, users might complain that their browsers / discord stop working when closing G-Earth incorrectly.
*/
private static void setupShutdownHook() {
if (SHUTDOWN_HOOK.get()) {
return;
}
if (SHUTDOWN_HOOK.compareAndSet(false, true)) {
Runtime.getRuntime().addShutdownHook(new Thread(() -> NitroOsFunctionsFactory.create().unregisterSystemProxy()));
}
}
}

View File

@ -0,0 +1,152 @@
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 String NitroClientSearch = "configurationUrls:";
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 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;
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);
}
// CSP.
if (responseBody.contains(NitroClientSearch)) {
stripContentSecurityPolicy(response);
}
}
return httpObject;
}
/**
* Modify Content-Security-Policy header, which could prevent Nitro from connecting with G-Earth.
*/
private 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);
}
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);
}
}

View File

@ -0,0 +1,47 @@
package gearth.protocol.connection.proxy.nitro.http;
import gearth.protocol.connection.proxy.nitro.NitroConstants;
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 NitroConstants.HTTP_BUFFER_SIZE;
}
}

View File

@ -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);
}

View File

@ -0,0 +1,15 @@
package gearth.protocol.connection.proxy.nitro.os;
import java.io.File;
public interface NitroOsFunctions {
boolean isRootCertificateTrusted(File certificate);
boolean installRootCertificate(File certificate);
boolean registerSystemProxy(String host, int port);
boolean unregisterSystemProxy();
}

View File

@ -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");
}
}

View File

@ -0,0 +1,89 @@
package gearth.protocol.connection.proxy.nitro.os.windows;
import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.WinBase;
import com.sun.jna.platform.win32.WinDef;
import com.sun.jna.ptr.IntByReference;
import gearth.misc.RuntimeUtil;
import gearth.protocol.connection.proxy.nitro.os.NitroOsFunctions;
import java.io.File;
import java.io.IOException;
public class NitroWindows implements NitroOsFunctions {
/**
* Semicolon separated hosts to ignore for proxying.
*/
private static final String PROXY_IGNORE = "discord.com;github.com;";
/**
* Checks if the certificate is trusted by the local machine.
* @param certificate Absolute path to the certificate.
* @return true if trusted
*/
@Override
public boolean isRootCertificateTrusted(File certificate) {
try {
final String output = RuntimeUtil.getCommandOutput(new String[] {"cmd", "/c", " certutil.exe -f -verify " + certificate.getAbsolutePath()});
return !output.contains("CERT_TRUST_IS_UNTRUSTED_ROOT") &&
output.contains("dwInfoStatus=10c dwErrorStatus=0");
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
@Override
public boolean installRootCertificate(File certificate) {
final String certificatePath = certificate.getAbsolutePath();
// Prompt UAC elevation.
WinDef.HINSTANCE result = NitroWindowsShell32.INSTANCE.ShellExecuteA(null, "runas", "cmd.exe", "/S /C \"certutil -addstore root " + certificatePath + "\"", null, 1);
// Wait for exit.
Kernel32.INSTANCE.WaitForSingleObject(result, WinBase.INFINITE);
// Exit code for certutil.
final IntByReference statusRef = new IntByReference(-1);
Kernel32.INSTANCE.GetExitCodeProcess(result, statusRef);
// Check if process exited without errors
if (statusRef.getValue() != -1) {
System.out.printf("Certutil command exited with exit code %s%n", statusRef.getValue());
return false;
}
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 ProxyOverride /t REG_SZ /d \"" + PROXY_IGNORE + "\" /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;
}
}

View File

@ -0,0 +1,17 @@
package gearth.protocol.connection.proxy.nitro.os.windows;
import com.sun.jna.Native;
import com.sun.jna.platform.win32.ShellAPI;
import com.sun.jna.platform.win32.WinDef;
import com.sun.jna.win32.StdCallLibrary;
public interface NitroWindowsShell32 extends ShellAPI, StdCallLibrary {
NitroWindowsShell32 INSTANCE = Native.loadLibrary("shell32", NitroWindowsShell32.class);
WinDef.HINSTANCE ShellExecuteA(WinDef.HWND hwnd,
String lpOperation,
String lpFile,
String lpParameters,
String lpDirectory,
int nShowCmd);
}

View File

@ -0,0 +1,59 @@
package gearth.protocol.connection.proxy.nitro.websocket;
import gearth.protocol.HMessage;
import gearth.protocol.HPacket;
import gearth.protocol.packethandler.PacketHandler;
import gearth.services.extension_handler.ExtensionHandler;
import gearth.services.extension_handler.OnHMessageHandled;
import javax.websocket.Session;
import java.io.IOException;
import java.nio.ByteBuffer;
public class NitroPacketHandler extends PacketHandler {
private final HMessage.Direction direction;
private final NitroSession session;
protected NitroPacketHandler(HMessage.Direction direction, NitroSession session, ExtensionHandler extensionHandler, Object[] trafficObservables) {
super(extensionHandler, trafficObservables);
this.direction = direction;
this.session = session;
}
@Override
public boolean sendToStream(byte[] buffer) {
final Session localSession = session.getSession();
if (localSession == null) {
return false;
}
// Required to prevent garbage buffer within the UI logger.
if (direction == HMessage.Direction.TOSERVER) {
buffer = buffer.clone();
}
localSession.getAsyncRemote().sendBinary(ByteBuffer.wrap(buffer));
return true;
}
@Override
public void act(byte[] buffer) throws IOException {
HMessage hMessage = new HMessage(new HPacket(buffer), direction, currentIndex);
OnHMessageHandled afterExtensionIntercept = hMessage1 -> {
notifyListeners(2, hMessage1);
if (!hMessage1.isBlocked()) {
sendToStream(hMessage1.getPacket().toBytes());
}
};
notifyListeners(0, hMessage);
notifyListeners(1, hMessage);
extensionHandler.handle(hMessage, afterExtensionIntercept);
currentIndex++;
}
}

View File

@ -0,0 +1,9 @@
package gearth.protocol.connection.proxy.nitro.websocket;
import javax.websocket.Session;
public interface NitroSession {
Session getSession();
}

View File

@ -0,0 +1,118 @@
package gearth.protocol.connection.proxy.nitro.websocket;
import gearth.protocol.HConnection;
import gearth.protocol.HMessage;
import gearth.protocol.connection.*;
import gearth.protocol.connection.proxy.nitro.NitroConstants;
import gearth.protocol.connection.proxy.nitro.NitroProxyProvider;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
@ServerEndpoint(value = "/")
public class NitroWebsocketClient implements NitroSession {
private final HProxySetter proxySetter;
private final HStateSetter stateSetter;
private final HConnection connection;
private final NitroProxyProvider proxyProvider;
private final NitroWebsocketServer server;
private final NitroPacketHandler packetHandler;
private final AtomicBoolean shutdownLock;
private Session activeSession = null;
public NitroWebsocketClient(HProxySetter proxySetter, HStateSetter stateSetter, HConnection connection, NitroProxyProvider proxyProvider) {
this.proxySetter = proxySetter;
this.stateSetter = stateSetter;
this.connection = connection;
this.proxyProvider = proxyProvider;
this.server = new NitroWebsocketServer(connection, this);
this.packetHandler = new NitroPacketHandler(HMessage.Direction.TOSERVER, server, connection.getExtensionHandler(), connection.getTrafficObservables());
this.shutdownLock = new AtomicBoolean();
}
@OnOpen
public void onOpen(Session session) throws IOException {
activeSession = session;
activeSession.setMaxBinaryMessageBufferSize(NitroConstants.WEBSOCKET_BUFFER_SIZE);
server.connect(proxyProvider.getOriginalWebsocketUrl());
final HProxy proxy = new HProxy(HClient.NITRO, "", "", -1, -1, "");
proxy.verifyProxy(
this.server.getPacketHandler(),
this.packetHandler,
NitroConstants.WEBSOCKET_REVISION,
NitroConstants.WEBSOCKET_CLIENT_IDENTIFIER
);
proxySetter.setProxy(proxy);
stateSetter.setState(HState.CONNECTED);
}
@OnMessage
public void onMessage(byte[] b, Session session) throws IOException {
packetHandler.act(b);
}
@OnClose
public void onClose(Session session) throws IOException {
activeSession = null;
shutdownProxy();
}
@OnError
public void onError(Session session, Throwable throwable) {
throwable.printStackTrace();
// Shutdown.
shutdownProxy();
}
@Override
public Session getSession() {
return activeSession;
}
/**
* Shutdown and clean up the client connection.
*/
private void shutdown() {
if (activeSession == null) {
return;
}
try {
activeSession.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
activeSession = null;
}
}
/**
* Shutdown all connections and reset program state.
*/
public void shutdownProxy() {
if (shutdownLock.get()) {
return;
}
if (shutdownLock.compareAndSet(false, true)) {
// Close client connection.
shutdown();
// Close server connection.
server.shutdown();
// Reset program state.
proxySetter.setProxy(null);
stateSetter.setState(HState.NOT_CONNECTED);
}
}
}

View File

@ -0,0 +1,66 @@
package gearth.protocol.connection.proxy.nitro.websocket;
import gearth.protocol.HConnection;
import gearth.protocol.connection.HProxySetter;
import gearth.protocol.connection.HStateSetter;
import gearth.protocol.connection.proxy.nitro.NitroConstants;
import gearth.protocol.connection.proxy.nitro.NitroProxyProvider;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer;
import javax.websocket.server.ServerContainer;
import javax.websocket.server.ServerEndpointConfig;
public class NitroWebsocketProxy {
private final HProxySetter proxySetter;
private final HStateSetter stateSetter;
private final HConnection connection;
private final NitroProxyProvider proxyProvider;
private final Server server;
public NitroWebsocketProxy(HProxySetter proxySetter, HStateSetter stateSetter, HConnection connection, NitroProxyProvider proxyProvider) {
this.proxySetter = proxySetter;
this.stateSetter = stateSetter;
this.connection = connection;
this.proxyProvider = proxyProvider;
this.server = new Server(NitroConstants.WEBSOCKET_PORT);
}
public boolean start() {
try {
final ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.setContextPath("/");
final HandlerList handlers = new HandlerList();
handlers.setHandlers(new Handler[] { context });
final ServerContainer wscontainer = WebSocketServerContainerInitializer.configureContext(context);
wscontainer.addEndpoint(ServerEndpointConfig.Builder
.create(NitroWebsocketClient.class, "/")
.configurator(new NitroWebsocketServerConfigurator(proxySetter, stateSetter, connection, proxyProvider))
.build());
server.setHandler(handlers);
server.start();
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
public void stop() {
try {
server.stop();
} catch (Exception e) {
e.printStackTrace();
}
}
}

View File

@ -0,0 +1,82 @@
package gearth.protocol.connection.proxy.nitro.websocket;
import gearth.protocol.HConnection;
import gearth.protocol.HMessage;
import gearth.protocol.connection.proxy.nitro.NitroConstants;
import gearth.protocol.packethandler.PacketHandler;
import javax.websocket.*;
import java.io.IOException;
import java.net.URI;
@ClientEndpoint
public class NitroWebsocketServer implements NitroSession {
private final PacketHandler packetHandler;
private final NitroWebsocketClient client;
private Session activeSession = null;
public NitroWebsocketServer(HConnection connection, NitroWebsocketClient client) {
this.client = client;
this.packetHandler = new NitroPacketHandler(HMessage.Direction.TOCLIENT, client, connection.getExtensionHandler(), connection.getTrafficObservables());
}
public void connect(String websocketUrl) throws IOException {
try {
ContainerProvider.getWebSocketContainer().connectToServer(this, URI.create(websocketUrl));
} catch (DeploymentException e) {
throw new IOException("Failed to deploy websocket client", e);
}
}
@OnOpen
public void onOpen(Session Session) {
this.activeSession = Session;
this.activeSession.setMaxBinaryMessageBufferSize(NitroConstants.WEBSOCKET_BUFFER_SIZE);
}
@OnMessage
public void onMessage(byte[] b, Session session) throws IOException {
packetHandler.act(b);
}
@OnClose
public void onClose(Session userSession, CloseReason reason) {
// Hotel closed connection.
client.shutdownProxy();
}
@OnError
public void onError(Session session, Throwable throwable) {
throwable.printStackTrace();
// Shutdown.
client.shutdownProxy();
}
@Override
public Session getSession() {
return activeSession;
}
public PacketHandler getPacketHandler() {
return packetHandler;
}
/**
* Shutdown and clean up the server connection.
*/
public void shutdown() {
if (activeSession == null) {
return;
}
try {
activeSession.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
activeSession = null;
}
}
}

View File

@ -0,0 +1,28 @@
package gearth.protocol.connection.proxy.nitro.websocket;
import gearth.protocol.HConnection;
import gearth.protocol.connection.HProxySetter;
import gearth.protocol.connection.HStateSetter;
import gearth.protocol.connection.proxy.nitro.NitroProxyProvider;
import javax.websocket.server.ServerEndpointConfig;
public class NitroWebsocketServerConfigurator extends ServerEndpointConfig.Configurator {
private final HProxySetter proxySetter;
private final HStateSetter stateSetter;
private final HConnection connection;
private final NitroProxyProvider proxyProvider;
public NitroWebsocketServerConfigurator(HProxySetter proxySetter, HStateSetter stateSetter, HConnection connection, NitroProxyProvider proxyProvider) {
this.proxySetter = proxySetter;
this.stateSetter = stateSetter;
this.connection = connection;
this.proxyProvider = proxyProvider;
}
@Override
public <T> T getEndpointInstance(Class<T> endpointClass) {
return (T) new NitroWebsocketClient(proxySetter, stateSetter, connection, proxyProvider);
}
}

View File

@ -114,8 +114,7 @@ public class PacketInfoManager {
if (clientType == HClient.UNITY) {
result.addAll(new GEarthUnityPacketInfoProvider(hotelversion).provide());
}
else if (clientType == HClient.FLASH) {
} else if (clientType == HClient.FLASH || clientType == HClient.NITRO) {
try {
List<RemotePacketInfoProvider> providers = new ArrayList<>();
providers.add(new HarblePacketInfoProvider(hotelversion));

View File

@ -2,6 +2,7 @@ package gearth.ui.connection;
import gearth.Main;
import gearth.misc.Cacher;
import gearth.protocol.connection.HClient;
import gearth.protocol.connection.HState;
import gearth.protocol.connection.proxy.ProxyProviderFactory;
import gearth.services.Constants;
@ -38,10 +39,11 @@ public class ConnectionController extends SubForm {
private volatile int fullyInitialized = 0;
public static final String USE_UNITY_CLIENT_CACHE_KEY = "use_unity";
public static final String CLIENT_CACHE_KEY = "last_client_mode";
public ToggleGroup tgl_clientMode;
public RadioButton rd_unity;
public RadioButton rd_flash;
public RadioButton rd_nitro;
public GridPane grd_clientSelection;
private volatile int initcount = 0;
@ -54,9 +56,18 @@ public class ConnectionController extends SubForm {
Constants.UNITY_PACKETS = rd_unity.isSelected();
});
if (Cacher.getCacheContents().has(USE_UNITY_CLIENT_CACHE_KEY)) {
rd_unity.setSelected(Cacher.getCacheContents().getBoolean(USE_UNITY_CLIENT_CACHE_KEY));
rd_flash.setSelected(!Cacher.getCacheContents().getBoolean(USE_UNITY_CLIENT_CACHE_KEY));
if (Cacher.getCacheContents().has(CLIENT_CACHE_KEY)) {
switch (Cacher.getCacheContents().getEnum(HClient.class, CLIENT_CACHE_KEY)) {
case FLASH:
rd_flash.setSelected(true);
break;
case UNITY:
rd_unity.setSelected(true);
break;
case NITRO:
rd_nitro.setSelected(true);
break;
}
}
@ -235,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);
}
}
@ -244,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();
}
@ -269,7 +284,13 @@ public class ConnectionController extends SubForm {
@Override
protected void onExit() {
Cacher.put(USE_UNITY_CLIENT_CACHE_KEY, rd_unity.isSelected());
if (rd_flash.isSelected()) {
Cacher.put(CLIENT_CACHE_KEY, HClient.FLASH);
} else if (rd_unity.isSelected()) {
Cacher.put(CLIENT_CACHE_KEY, HClient.UNITY);
} else if (rd_nitro.isSelected()) {
Cacher.put(CLIENT_CACHE_KEY, HClient.NITRO);
}
getHConnection().abort();
}
@ -280,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;
}
}

View File

@ -5,7 +5,7 @@
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<GridPane alignment="CENTER" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" prefHeight="258.0" prefWidth="650.0" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="gearth.ui.connection.ConnectionController">
<GridPane alignment="CENTER" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" prefHeight="258.0" prefWidth="650.0" xmlns="http://javafx.com/javafx/11.0.2" xmlns:fx="http://javafx.com/fxml/1" fx:controller="gearth.ui.connection.ConnectionController">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
</columnConstraints>
@ -132,9 +132,9 @@
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" maxWidth="149.0" minWidth="10.0" prefWidth="76.0" />
<ColumnConstraints hgrow="SOMETIMES" maxWidth="149.0" minWidth="10.0" prefWidth="25.0" />
<ColumnConstraints hgrow="SOMETIMES" maxWidth="177.0" minWidth="66.0" prefWidth="93.0" />
<ColumnConstraints hgrow="SOMETIMES" maxWidth="133.0" minWidth="8.0" prefWidth="98.0" />
<ColumnConstraints hgrow="SOMETIMES" maxWidth="242.0" minWidth="10.0" prefWidth="32.0" />
<ColumnConstraints hgrow="SOMETIMES" maxWidth="177.0" minWidth="66.0" prefWidth="90.0" />
<ColumnConstraints hgrow="SOMETIMES" maxWidth="133.0" minWidth="8.0" prefWidth="70.0" />
<ColumnConstraints hgrow="SOMETIMES" maxWidth="242.0" minWidth="10.0" prefWidth="70.0" />
</columnConstraints>
<rowConstraints>
<RowConstraints minHeight="20.0" prefHeight="34.0" vgrow="SOMETIMES" />
@ -156,7 +156,13 @@
<RadioButton fx:id="rd_flash" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" mnemonicParsing="false" prefHeight="35.0" prefWidth="111.0" selected="true" text="Flash / Air" toggleGroup="$tgl_clientMode" GridPane.columnIndex="2">
<GridPane.margin>
<Insets />
</GridPane.margin></RadioButton>
</GridPane.margin>
</RadioButton>
<RadioButton fx:id="rd_nitro" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" mnemonicParsing="false" prefHeight="22.0" prefWidth="54.0" text="Nitro" toggleGroup="$tgl_clientMode" GridPane.columnIndex="4">
<GridPane.margin>
<Insets />
</GridPane.margin>
</RadioButton>
</children>
<padding>
<Insets bottom="8.0" top="8.0" />