mirror of
https://github.com/sirjonasxx/G-Earth.git
synced 2024-11-26 18:30:52 +01:00
Merge branch 'master' into development
This commit is contained in:
commit
eff6876546
88
.github/workflows/build.yml
vendored
Normal file
88
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
name: Build G-Earth
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- '.github/workflows/**'
|
||||||
|
- 'G-Earth/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout G-Earth
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Checkout G-Wasm
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
repository: sirjonasxx/G-Wasm
|
||||||
|
path: gwasm
|
||||||
|
ref: minimal
|
||||||
|
|
||||||
|
- name: Set up JDK 8
|
||||||
|
uses: actions/setup-java@v2
|
||||||
|
with:
|
||||||
|
java-version: '8'
|
||||||
|
java-package: jdk+fx
|
||||||
|
distribution: 'liberica'
|
||||||
|
|
||||||
|
- name: Install G-Wasm
|
||||||
|
working-directory: gwasm
|
||||||
|
run: mvn -B install
|
||||||
|
|
||||||
|
- name: Build G-Earth
|
||||||
|
run: mvn -B package
|
||||||
|
|
||||||
|
- name: Zip Build/Mac
|
||||||
|
run: |
|
||||||
|
cd ${{ github.workspace }}/Build/Mac/
|
||||||
|
zip -r ../../build-mac.zip *
|
||||||
|
|
||||||
|
- name: Zip Build/Linux
|
||||||
|
run: |
|
||||||
|
cd ${{ github.workspace }}/Build/Linux/
|
||||||
|
zip -r ../../build-linux.zip *
|
||||||
|
|
||||||
|
- name: Zip Build/Windows_32bit
|
||||||
|
run: |
|
||||||
|
cd ${{ github.workspace }}/Build/Windows_32bit/
|
||||||
|
zip -r ../../build-win32.zip *
|
||||||
|
|
||||||
|
- name: Zip Build/Windows_64bit
|
||||||
|
run: |
|
||||||
|
cd ${{ github.workspace }}/Build/Windows_64bit/
|
||||||
|
zip -r ../../build-win64.zip *
|
||||||
|
|
||||||
|
- name: Upload Mac OSX
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: Mac OSX
|
||||||
|
path: build-mac.zip
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
|
- name: Upload Linux
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: Linux
|
||||||
|
path: build-linux.zip
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
|
- name: Upload Windows x32
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: Windows x32
|
||||||
|
path: build-win32.zip
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
|
- name: Upload Windows x64
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: Windows x64
|
||||||
|
path: build-win64.zip
|
||||||
|
retention-days: 7
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -21,3 +21,7 @@ Extensions/BlockReplacePackets/.classpath
|
|||||||
*.settings/org.eclipse.jdt.apt.core.prefs
|
*.settings/org.eclipse.jdt.apt.core.prefs
|
||||||
*.settings/org.eclipse.jdt.core.prefs
|
*.settings/org.eclipse.jdt.core.prefs
|
||||||
*.settings/org.eclipse.m2e.core.prefs
|
*.settings/org.eclipse.m2e.core.prefs
|
||||||
|
|
||||||
|
# Certificates
|
||||||
|
*.p12
|
||||||
|
*.pem
|
@ -224,7 +224,11 @@
|
|||||||
<artifactId>bytes</artifactId>
|
<artifactId>bytes</artifactId>
|
||||||
<version>1.5.0</version>
|
<version>1.5.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.ganskef</groupId>
|
||||||
|
<artifactId>littleproxy-mitm</artifactId>
|
||||||
|
<version>1.1.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -232,7 +236,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>G-Earth</groupId>
|
<groupId>G-Earth</groupId>
|
||||||
<artifactId>G-Wasm</artifactId>
|
<artifactId>G-Wasm</artifactId>
|
||||||
<version>1.0</version>
|
<version>1.0.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
31
G-Earth/src/main/java/gearth/misc/RuntimeUtil.java
Normal file
31
G-Earth/src/main/java/gearth/misc/RuntimeUtil.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
package gearth.protocol;
|
package gearth.protocol;
|
||||||
|
|
||||||
import gearth.misc.listenerpattern.Observable;
|
import gearth.misc.listenerpattern.Observable;
|
||||||
|
import gearth.protocol.connection.proxy.nitro.NitroProxyProvider;
|
||||||
import gearth.services.packet_info.PacketInfoManager;
|
import gearth.services.packet_info.PacketInfoManager;
|
||||||
import gearth.protocol.connection.HClient;
|
import gearth.protocol.connection.HClient;
|
||||||
import gearth.protocol.connection.HProxy;
|
import gearth.protocol.connection.HProxy;
|
||||||
@ -68,6 +69,12 @@ public class HConnection {
|
|||||||
startMITM();
|
startMITM();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void startNitro() {
|
||||||
|
HConnection selff = this;
|
||||||
|
proxyProvider = new NitroProxyProvider(proxy -> selff.proxy = proxy, selff::setState, this);
|
||||||
|
startMITM();
|
||||||
|
}
|
||||||
|
|
||||||
private void startMITM() {
|
private void startMITM() {
|
||||||
try {
|
try {
|
||||||
if (proxyProvider != null) {
|
if (proxyProvider != null) {
|
||||||
@ -140,6 +147,7 @@ public class HConnection {
|
|||||||
|
|
||||||
|
|
||||||
public boolean sendToClient(HPacket packet) {
|
public boolean sendToClient(HPacket packet) {
|
||||||
|
HProxy proxy = this.proxy;
|
||||||
if (proxy == null) return false;
|
if (proxy == null) return false;
|
||||||
|
|
||||||
if (!packet.isPacketComplete()) {
|
if (!packet.isPacketComplete()) {
|
||||||
@ -149,10 +157,10 @@ public class HConnection {
|
|||||||
if (!packet.isPacketComplete() || !packet.canSendToClient()) return false;
|
if (!packet.isPacketComplete() || !packet.canSendToClient()) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
proxy.getPacketSenderQueue().queueToClient(packet);
|
return proxy.sendToClient(packet);
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
public boolean sendToServer(HPacket packet) {
|
public boolean sendToServer(HPacket packet) {
|
||||||
|
HProxy proxy = this.proxy;
|
||||||
if (proxy == null) return false;
|
if (proxy == null) return false;
|
||||||
|
|
||||||
if (!packet.isPacketComplete()) {
|
if (!packet.isPacketComplete()) {
|
||||||
@ -162,8 +170,7 @@ public class HConnection {
|
|||||||
if (!packet.isPacketComplete() || !packet.canSendToServer()) return false;
|
if (!packet.isPacketComplete() || !packet.canSendToServer()) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
proxy.getPacketSenderQueue().queueToServer(packet);
|
return proxy.sendToServer(packet);
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getClientHost() {
|
public String getClientHost() {
|
||||||
|
@ -2,5 +2,6 @@ package gearth.protocol.connection;
|
|||||||
|
|
||||||
public enum HClient {
|
public enum HClient {
|
||||||
UNITY,
|
UNITY,
|
||||||
FLASH
|
FLASH,
|
||||||
|
NITRO
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package gearth.protocol.connection;
|
package gearth.protocol.connection;
|
||||||
|
|
||||||
|
import gearth.protocol.HPacket;
|
||||||
import gearth.services.packet_info.PacketInfoManager;
|
import gearth.services.packet_info.PacketInfoManager;
|
||||||
import gearth.protocol.packethandler.PacketHandler;
|
import gearth.protocol.packethandler.PacketHandler;
|
||||||
|
|
||||||
@ -25,8 +26,6 @@ public class HProxy {
|
|||||||
private volatile String clientIdentifier = "";
|
private volatile String clientIdentifier = "";
|
||||||
private volatile PacketInfoManager packetInfoManager = null;
|
private volatile PacketInfoManager packetInfoManager = null;
|
||||||
|
|
||||||
private volatile PacketSenderQueue packetSenderQueue = null;
|
|
||||||
|
|
||||||
public HProxy(HClient hClient, String input_domain, String actual_domain, int actual_port, int intercept_port, String intercept_host) {
|
public HProxy(HClient hClient, String input_domain, String actual_domain, int actual_port, int intercept_port, String intercept_host) {
|
||||||
this.hClient = hClient;
|
this.hClient = hClient;
|
||||||
this.input_domain = input_domain;
|
this.input_domain = input_domain;
|
||||||
@ -46,7 +45,20 @@ public class HProxy {
|
|||||||
this.hotelVersion = hotelVersion;
|
this.hotelVersion = hotelVersion;
|
||||||
this.clientIdentifier = clientIdentifier;
|
this.clientIdentifier = clientIdentifier;
|
||||||
this.packetInfoManager = PacketInfoManager.fromHotelVersion(hotelVersion, hClient);
|
this.packetInfoManager = PacketInfoManager.fromHotelVersion(hotelVersion, hClient);
|
||||||
this.packetSenderQueue = new PacketSenderQueue(this);
|
}
|
||||||
|
|
||||||
|
public boolean sendToServer(HPacket packet) {
|
||||||
|
if (outHandler != null) {
|
||||||
|
return outHandler.sendToStream(packet.toBytes());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean sendToClient(HPacket packet) {
|
||||||
|
if (inHandler != null) {
|
||||||
|
return inHandler.sendToStream(packet.toBytes());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getClientIdentifier() {
|
public String getClientIdentifier() {
|
||||||
@ -89,10 +101,6 @@ public class HProxy {
|
|||||||
return hotelVersion;
|
return hotelVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PacketSenderQueue getPacketSenderQueue() {
|
|
||||||
return packetSenderQueue;
|
|
||||||
}
|
|
||||||
|
|
||||||
public HClient gethClient() {
|
public HClient gethClient() {
|
||||||
return hClient;
|
return hClient;
|
||||||
}
|
}
|
||||||
|
@ -1,78 +0,0 @@
|
|||||||
package gearth.protocol.connection;
|
|
||||||
|
|
||||||
import gearth.protocol.HPacket;
|
|
||||||
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.Queue;
|
|
||||||
|
|
||||||
public class PacketSenderQueue {
|
|
||||||
|
|
||||||
private final HProxy proxy;
|
|
||||||
private final Queue<HPacket> sendToClientQueue = new LinkedList<>();
|
|
||||||
private final Queue<HPacket> sendToServerQueue = new LinkedList<>();
|
|
||||||
|
|
||||||
PacketSenderQueue(HProxy proxy) {
|
|
||||||
this.proxy = proxy;
|
|
||||||
new Thread(() -> {
|
|
||||||
while (true) {
|
|
||||||
HPacket packet;
|
|
||||||
synchronized (sendToClientQueue) {
|
|
||||||
while ((packet = sendToClientQueue.poll()) != null) {
|
|
||||||
sendToClient(packet);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Thread.sleep(1);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
new Thread(() -> {
|
|
||||||
while (true) {
|
|
||||||
HPacket packet;
|
|
||||||
synchronized (sendToServerQueue) {
|
|
||||||
while ((packet = sendToServerQueue.poll()) != null) {
|
|
||||||
sendToServer(packet);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Thread.sleep(1);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void sendToClient(HPacket message) {
|
|
||||||
proxy.getInHandler().sendToStream(message.toBytes());
|
|
||||||
}
|
|
||||||
private void sendToServer(HPacket message) {
|
|
||||||
proxy.getOutHandler().sendToStream(message.toBytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void queueToClient(HPacket message) {
|
|
||||||
synchronized (sendToClientQueue) {
|
|
||||||
sendToClientQueue.add(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
public void queueToServer(HPacket message) {
|
|
||||||
synchronized (sendToServerQueue) {
|
|
||||||
sendToServerQueue.add(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clear() {
|
|
||||||
synchronized (sendToClientQueue) {
|
|
||||||
sendToClientQueue.clear();
|
|
||||||
}
|
|
||||||
synchronized (sendToServerQueue) {
|
|
||||||
sendToServerQueue.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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";
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,129 @@
|
|||||||
|
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;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
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 final AtomicBoolean abortLock;
|
||||||
|
|
||||||
|
private String originalWebsocketUrl;
|
||||||
|
private String originalOriginUrl;
|
||||||
|
|
||||||
|
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);
|
||||||
|
this.abortLock = new AtomicBoolean();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOriginalWebsocketUrl() {
|
||||||
|
return originalWebsocketUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOriginalOriginUrl() {
|
||||||
|
return originalOriginUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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() {
|
||||||
|
if (abortLock.get()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (abortLock.compareAndSet(true, true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
originalOriginUrl = extractOriginUrl(configUrl);
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Catch setState ABORTING inside NitroWebsocketClient.
|
||||||
|
if (newState == HState.ABORTING) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -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,99 @@
|
|||||||
|
package gearth.protocol.connection.proxy.nitro.http;
|
||||||
|
|
||||||
|
import io.netty.handler.codec.http.HttpRequest;
|
||||||
|
import org.littleshoot.proxy.MitmManager;
|
||||||
|
import org.littleshoot.proxy.mitm.*;
|
||||||
|
|
||||||
|
import javax.net.ssl.SSLEngine;
|
||||||
|
import javax.net.ssl.SSLPeerUnverifiedException;
|
||||||
|
import javax.net.ssl.SSLSession;
|
||||||
|
import java.security.cert.Certificate;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link MitmManager} that uses the common name and subject alternative names
|
||||||
|
* from the upstream certificate to create a dynamic certificate with it.
|
||||||
|
*/
|
||||||
|
public class NitroCertificateSniffingManager implements MitmManager {
|
||||||
|
|
||||||
|
private static final boolean DEBUG = false;
|
||||||
|
|
||||||
|
private final BouncyCastleSslEngineSource sslEngineSource;
|
||||||
|
|
||||||
|
public NitroCertificateSniffingManager(Authority authority) throws RootCertificateException {
|
||||||
|
try {
|
||||||
|
sslEngineSource = new BouncyCastleSslEngineSource(authority, true, true, null);
|
||||||
|
} catch (final Exception e) {
|
||||||
|
throw new RootCertificateException("Errors during assembling root CA.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public SSLEngine serverSslEngine(String peerHost, int peerPort) {
|
||||||
|
return sslEngineSource.newSslEngine(peerHost, peerPort);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SSLEngine serverSslEngine() {
|
||||||
|
return sslEngineSource.newSslEngine();
|
||||||
|
}
|
||||||
|
|
||||||
|
public SSLEngine clientSslEngineFor(HttpRequest httpRequest, SSLSession serverSslSession) {
|
||||||
|
try {
|
||||||
|
X509Certificate upstreamCert = getCertificateFromSession(serverSslSession);
|
||||||
|
// TODO store the upstream cert by commonName to review it later
|
||||||
|
|
||||||
|
// A reasons to not use the common name and the alternative names
|
||||||
|
// from upstream certificate from serverSslSession to create the
|
||||||
|
// dynamic certificate:
|
||||||
|
//
|
||||||
|
// It's not necessary. The host name is accepted by the browser.
|
||||||
|
//
|
||||||
|
String commonName = getCommonName(upstreamCert);
|
||||||
|
|
||||||
|
SubjectAlternativeNameHolder san = new SubjectAlternativeNameHolder();
|
||||||
|
|
||||||
|
san.addAll(upstreamCert.getSubjectAlternativeNames());
|
||||||
|
|
||||||
|
if (DEBUG) {
|
||||||
|
System.out.println("[NitroCertificateSniffingManager] Subject Alternative Names");
|
||||||
|
|
||||||
|
for (List<?> name : upstreamCert.getSubjectAlternativeNames()) {
|
||||||
|
System.out.printf("[NitroCertificateSniffingManager] - %s%n", name.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sslEngineSource.createCertForHost(commonName, san);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new FakeCertificateException("Creation dynamic certificate failed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private X509Certificate getCertificateFromSession(SSLSession sslSession) throws SSLPeerUnverifiedException {
|
||||||
|
Certificate[] peerCerts = sslSession.getPeerCertificates();
|
||||||
|
Certificate peerCert = peerCerts[0];
|
||||||
|
if (peerCert instanceof java.security.cert.X509Certificate) {
|
||||||
|
return (java.security.cert.X509Certificate) peerCert;
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Required java.security.cert.X509Certificate, found: " + peerCert);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getCommonName(X509Certificate c) {
|
||||||
|
if (DEBUG) {
|
||||||
|
System.out.printf("[NitroCertificateSniffingManager] Subject DN principal name: %s%n", c.getSubjectDN().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String each : c.getSubjectDN().getName().split(",\\s*")) {
|
||||||
|
if (each.startsWith("CN=")) {
|
||||||
|
String result = each.substring(3);
|
||||||
|
|
||||||
|
if (DEBUG) {
|
||||||
|
System.out.printf("[NitroCertificateSniffingManager] Common Name: %s%n", c.getSubjectDN().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalStateException("Missed CN in Subject DN: " + c.getSubjectDN());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,152 @@
|
|||||||
|
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.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 NitroCertificateSniffingManager(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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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,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();
|
||||||
|
|
||||||
|
}
|
@ -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,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;discordapp.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
package gearth.protocol.connection.proxy.nitro.websocket;
|
||||||
|
|
||||||
|
import gearth.protocol.HMessage;
|
||||||
|
import gearth.protocol.HPacket;
|
||||||
|
import gearth.protocol.packethandler.PacketHandler;
|
||||||
|
import gearth.protocol.packethandler.PayloadBuffer;
|
||||||
|
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;
|
||||||
|
private final PayloadBuffer payloadBuffer;
|
||||||
|
private final Object payloadLock;
|
||||||
|
|
||||||
|
protected NitroPacketHandler(HMessage.Direction direction, NitroSession session, ExtensionHandler extensionHandler, Object[] trafficObservables) {
|
||||||
|
super(extensionHandler, trafficObservables);
|
||||||
|
this.direction = direction;
|
||||||
|
this.session = session;
|
||||||
|
this.payloadBuffer = new PayloadBuffer();
|
||||||
|
this.payloadLock = new Object();
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 {
|
||||||
|
payloadBuffer.push(buffer);
|
||||||
|
|
||||||
|
synchronized (payloadLock) {
|
||||||
|
for (HPacket packet : payloadBuffer.receive()) {
|
||||||
|
HMessage hMessage = new HMessage(packet, 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++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package gearth.protocol.connection.proxy.nitro.websocket;
|
||||||
|
|
||||||
|
import javax.websocket.Session;
|
||||||
|
|
||||||
|
public interface NitroSession {
|
||||||
|
|
||||||
|
Session getSession();
|
||||||
|
|
||||||
|
}
|
@ -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(), proxyProvider.getOriginalOriginUrl());
|
||||||
|
|
||||||
|
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.ABORTING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,102 @@
|
|||||||
|
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;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class NitroWebsocketServer extends Endpoint 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, String originUrl) throws IOException {
|
||||||
|
try {
|
||||||
|
ClientEndpointConfig.Builder builder = ClientEndpointConfig.Builder.create();
|
||||||
|
|
||||||
|
if (originUrl != null) {
|
||||||
|
builder.configurator(new ClientEndpointConfig.Configurator() {
|
||||||
|
@Override
|
||||||
|
public void beforeRequest(Map<String, List<String>> headers) {
|
||||||
|
headers.put("Origin", Collections.singletonList(originUrl));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientEndpointConfig config = builder.build();
|
||||||
|
|
||||||
|
ContainerProvider.getWebSocketContainer().connectToServer(this, config, URI.create(websocketUrl));
|
||||||
|
} catch (DeploymentException e) {
|
||||||
|
throw new IOException("Failed to deploy websocket client", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onOpen(Session session, EndpointConfig config) {
|
||||||
|
this.activeSession = session;
|
||||||
|
this.activeSession.setMaxBinaryMessageBufferSize(NitroConstants.WEBSOCKET_BUFFER_SIZE);
|
||||||
|
this.activeSession.addMessageHandler(new MessageHandler.Whole<byte[]>() {
|
||||||
|
@Override
|
||||||
|
public void onMessage(byte[] message) {
|
||||||
|
try {
|
||||||
|
packetHandler.act(message);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClose(Session session, CloseReason closeReason) {
|
||||||
|
// Hotel closed connection.
|
||||||
|
client.shutdownProxy();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -20,7 +20,7 @@ public abstract class PacketHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public abstract void sendToStream(byte[] buffer);
|
public abstract boolean sendToStream(byte[] buffer);
|
||||||
|
|
||||||
public abstract void act(byte[] buffer) throws IOException;
|
public abstract void act(byte[] buffer) throws IOException;
|
||||||
|
|
||||||
|
@ -51,7 +51,9 @@ public abstract class FlashPacketHandler extends PacketHandler {
|
|||||||
|
|
||||||
public void act(byte[] buffer) throws IOException {
|
public void act(byte[] buffer) throws IOException {
|
||||||
if (!isDataStream) {
|
if (!isDataStream) {
|
||||||
out.write(buffer);
|
synchronized (sendLock) {
|
||||||
|
out.write(buffer);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,7 +115,7 @@ public abstract class FlashPacketHandler extends PacketHandler {
|
|||||||
isTempBlocked = false;
|
isTempBlocked = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendToStream(byte[] buffer) {
|
public boolean sendToStream(byte[] buffer) {
|
||||||
synchronized (sendLock) {
|
synchronized (sendLock) {
|
||||||
try {
|
try {
|
||||||
out.write(
|
out.write(
|
||||||
@ -121,8 +123,10 @@ public abstract class FlashPacketHandler extends PacketHandler {
|
|||||||
? buffer
|
? buffer
|
||||||
: encryptcipher.rc4(buffer)
|
: encryptcipher.rc4(buffer)
|
||||||
);
|
);
|
||||||
|
return true;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,11 +23,12 @@ public class UnityPacketHandler extends PacketHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void sendToStream(byte[] buffer) {
|
public boolean sendToStream(byte[] buffer) {
|
||||||
byte[] prefix = new byte[]{(direction == HMessage.Direction.TOCLIENT ? ((byte)0) : ((byte)1))};
|
byte[] prefix = new byte[]{(direction == HMessage.Direction.TOCLIENT ? ((byte)0) : ((byte)1))};
|
||||||
byte[] combined = ByteArrayUtils.combineByteArrays(prefix, buffer);
|
byte[] combined = ByteArrayUtils.combineByteArrays(prefix, buffer);
|
||||||
|
|
||||||
session.getAsyncRemote().sendBinary(ByteBuffer.wrap(combined));
|
session.getAsyncRemote().sendBinary(ByteBuffer.wrap(combined));
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -114,8 +114,7 @@ public class PacketInfoManager {
|
|||||||
|
|
||||||
if (clientType == HClient.UNITY) {
|
if (clientType == HClient.UNITY) {
|
||||||
result.addAll(new GEarthUnityPacketInfoProvider(hotelversion).provide());
|
result.addAll(new GEarthUnityPacketInfoProvider(hotelversion).provide());
|
||||||
}
|
} else if (clientType == HClient.FLASH || clientType == HClient.NITRO) {
|
||||||
else if (clientType == HClient.FLASH) {
|
|
||||||
try {
|
try {
|
||||||
List<RemotePacketInfoProvider> providers = new ArrayList<>();
|
List<RemotePacketInfoProvider> providers = new ArrayList<>();
|
||||||
providers.add(new HarblePacketInfoProvider(hotelversion));
|
providers.add(new HarblePacketInfoProvider(hotelversion));
|
||||||
|
@ -7,7 +7,6 @@ import wasm.disassembly.modules.sections.code.Locals;
|
|||||||
import wasm.disassembly.types.FuncType;
|
import wasm.disassembly.types.FuncType;
|
||||||
import wasm.disassembly.types.ResultType;
|
import wasm.disassembly.types.ResultType;
|
||||||
import wasm.disassembly.types.ValType;
|
import wasm.disassembly.types.ValType;
|
||||||
import wasm.misc.CodeCompare;
|
|
||||||
import wasm.misc.StreamReplacement;
|
import wasm.misc.StreamReplacement;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@ -38,23 +37,23 @@ public class IncomingPacketPatcher implements StreamReplacement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CodeCompare getCodeCompare() {
|
public boolean codeMatches(Func code) {
|
||||||
return code -> {
|
if (!(code.getLocalss().equals(Collections.singletonList(new Locals(1, ValType.I32)))))
|
||||||
if (!(code.getLocalss().equals(Collections.singletonList(new Locals(1, ValType.I32)))))
|
return false;
|
||||||
return false;
|
|
||||||
|
|
||||||
List<InstrType> expectedExpr = Arrays.asList(InstrType.I32_CONST, InstrType.I32_LOAD8_S,
|
List<InstrType> expectedExpr = Arrays.asList(InstrType.I32_CONST, InstrType.I32_LOAD8_S,
|
||||||
InstrType.I32_EQZ, InstrType.IF, InstrType.LOCAL_GET, InstrType.I32_LOAD, InstrType.LOCAL_TEE,
|
InstrType.I32_EQZ, InstrType.IF, InstrType.LOCAL_GET, InstrType.I32_LOAD, InstrType.LOCAL_TEE,
|
||||||
InstrType.IF);
|
InstrType.IF);
|
||||||
|
|
||||||
if (code.getExpression().getInstructions().size() != expectedExpr.size()) return false;
|
if (code.getExpression().getInstructions().size() != expectedExpr.size()) return false;
|
||||||
|
|
||||||
for (int j = 0; j < code.getExpression().getInstructions().size(); j++) {
|
for (int j = 0; j < code.getExpression().getInstructions().size(); j++) {
|
||||||
Instr instr = code.getExpression().getInstructions().get(j);
|
Instr instr = code.getExpression().getInstructions().get(j);
|
||||||
if (instr.getInstrType() != expectedExpr.get(j)) return false;
|
if (instr.getInstrType() != expectedExpr.get(j)) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ import wasm.disassembly.modules.sections.code.Func;
|
|||||||
import wasm.disassembly.types.FuncType;
|
import wasm.disassembly.types.FuncType;
|
||||||
import wasm.disassembly.types.ResultType;
|
import wasm.disassembly.types.ResultType;
|
||||||
import wasm.disassembly.types.ValType;
|
import wasm.disassembly.types.ValType;
|
||||||
import wasm.misc.CodeCompare;
|
|
||||||
import wasm.misc.StreamReplacement;
|
import wasm.misc.StreamReplacement;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@ -36,18 +35,16 @@ public class OutgoingPacketPatcher implements StreamReplacement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CodeCompare getCodeCompare() {
|
public boolean codeMatches(Func code) {
|
||||||
return code -> {
|
if (code.getLocalss().size() != 0) return false;
|
||||||
if (code.getLocalss().size() != 0) return false;
|
List<Instr> expression = code.getExpression().getInstructions();
|
||||||
List<Instr> expression = code.getExpression().getInstructions();
|
if (expression.get(0).getInstrType() != InstrType.LOCAL_GET) return false;
|
||||||
if (expression.get(0).getInstrType() != InstrType.LOCAL_GET) return false;
|
if (expression.get(1).getInstrType() != InstrType.LOCAL_GET) return false;
|
||||||
if (expression.get(1).getInstrType() != InstrType.LOCAL_GET) return false;
|
if (expression.get(2).getInstrType() != InstrType.LOCAL_GET) return false;
|
||||||
if (expression.get(2).getInstrType() != InstrType.LOCAL_GET) return false;
|
if (expression.get(3).getInstrType() != InstrType.I32_LOAD) return false;
|
||||||
if (expression.get(3).getInstrType() != InstrType.I32_LOAD) return false;
|
if (expression.get(4).getInstrType() != InstrType.I32_CONST) return false;
|
||||||
if (expression.get(4).getInstrType() != InstrType.I32_CONST) return false;
|
if (expression.get(5).getInstrType() != InstrType.CALL) return false;
|
||||||
if (expression.get(5).getInstrType() != InstrType.CALL) return false;
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ import wasm.disassembly.modules.sections.code.Func;
|
|||||||
import wasm.disassembly.types.FuncType;
|
import wasm.disassembly.types.FuncType;
|
||||||
import wasm.disassembly.types.ResultType;
|
import wasm.disassembly.types.ResultType;
|
||||||
import wasm.disassembly.types.ValType;
|
import wasm.disassembly.types.ValType;
|
||||||
import wasm.misc.CodeCompare;
|
|
||||||
import wasm.misc.StreamReplacement;
|
import wasm.misc.StreamReplacement;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@ -36,13 +35,11 @@ public class ReturnBytePatcher implements StreamReplacement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CodeCompare getCodeCompare() {
|
public boolean codeMatches(Func code) {
|
||||||
return code -> {
|
if (code.getLocalss().size() != 0) return false;
|
||||||
if (code.getLocalss().size() != 0) return false;
|
if (code.getExpression().getInstructions().size() != 30) return false;
|
||||||
if (code.getExpression().getInstructions().size() != 30) return false;
|
List<Instr> expr = code.getExpression().getInstructions();
|
||||||
List<Instr> expr = code.getExpression().getInstructions();
|
if (expr.get(expr.size() - 1).getInstrType() != InstrType.I32_XOR) return false;
|
||||||
if (expr.get(expr.size() - 1).getInstrType() != InstrType.I32_XOR) return false;
|
return true;
|
||||||
return true;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,6 @@ import wasm.disassembly.modules.sections.code.Locals;
|
|||||||
import wasm.disassembly.types.FuncType;
|
import wasm.disassembly.types.FuncType;
|
||||||
import wasm.disassembly.types.ResultType;
|
import wasm.disassembly.types.ResultType;
|
||||||
import wasm.disassembly.types.ValType;
|
import wasm.disassembly.types.ValType;
|
||||||
import wasm.misc.CodeCompare;
|
|
||||||
import wasm.misc.StreamReplacement;
|
import wasm.misc.StreamReplacement;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@ -38,23 +37,21 @@ public class SetKeyPatcher implements StreamReplacement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CodeCompare getCodeCompare() {
|
public boolean codeMatches(Func code) {
|
||||||
return code -> {
|
if (!(code.getLocalss().equals(Collections.singletonList(new Locals(1, ValType.I32)))))
|
||||||
if (!(code.getLocalss().equals(Collections.singletonList(new Locals(1, ValType.I32)))))
|
return false;
|
||||||
return false;
|
List<InstrType> expectedExpr = Arrays.asList(InstrType.I32_CONST, InstrType.I32_LOAD8_S,
|
||||||
List<InstrType> expectedExpr = Arrays.asList(InstrType.I32_CONST, InstrType.I32_LOAD8_S,
|
InstrType.I32_EQZ, InstrType.IF, InstrType.BLOCK, InstrType.LOCAL_GET, InstrType.I32_CONST,
|
||||||
InstrType.I32_EQZ, InstrType.IF, InstrType.BLOCK, InstrType.LOCAL_GET, InstrType.I32_CONST,
|
InstrType.LOCAL_GET, InstrType.I32_LOAD, InstrType.I32_CONST, InstrType.I32_CONST, InstrType.I32_CONST,
|
||||||
InstrType.LOCAL_GET, InstrType.I32_LOAD, InstrType.I32_CONST, InstrType.I32_CONST, InstrType.I32_CONST,
|
InstrType.CALL);
|
||||||
InstrType.CALL);
|
|
||||||
|
|
||||||
if (code.getExpression().getInstructions().size() != expectedExpr.size()) return false;
|
if (code.getExpression().getInstructions().size() != expectedExpr.size()) return false;
|
||||||
|
|
||||||
for (int j = 0; j < code.getExpression().getInstructions().size(); j++) {
|
for (int j = 0; j < code.getExpression().getInstructions().size(); j++) {
|
||||||
Instr instr = code.getExpression().getInstructions().get(j);
|
Instr instr = code.getExpression().getInstructions().get(j);
|
||||||
if (instr.getInstrType() != expectedExpr.get(j)) return false;
|
if (instr.getInstrType() != expectedExpr.get(j)) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package gearth.ui.connection;
|
|||||||
|
|
||||||
import gearth.Main;
|
import gearth.Main;
|
||||||
import gearth.misc.Cacher;
|
import gearth.misc.Cacher;
|
||||||
|
import gearth.protocol.connection.HClient;
|
||||||
import gearth.protocol.connection.HState;
|
import gearth.protocol.connection.HState;
|
||||||
import gearth.protocol.connection.proxy.ProxyProviderFactory;
|
import gearth.protocol.connection.proxy.ProxyProviderFactory;
|
||||||
import gearth.services.Constants;
|
import gearth.services.Constants;
|
||||||
@ -38,10 +39,11 @@ public class ConnectionController extends SubForm {
|
|||||||
private volatile int fullyInitialized = 0;
|
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 ToggleGroup tgl_clientMode;
|
||||||
public RadioButton rd_unity;
|
public RadioButton rd_unity;
|
||||||
public RadioButton rd_flash;
|
public RadioButton rd_flash;
|
||||||
|
public RadioButton rd_nitro;
|
||||||
public GridPane grd_clientSelection;
|
public GridPane grd_clientSelection;
|
||||||
|
|
||||||
private volatile int initcount = 0;
|
private volatile int initcount = 0;
|
||||||
@ -54,9 +56,18 @@ public class ConnectionController extends SubForm {
|
|||||||
Constants.UNITY_PACKETS = rd_unity.isSelected();
|
Constants.UNITY_PACKETS = rd_unity.isSelected();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (Cacher.getCacheContents().has(USE_UNITY_CLIENT_CACHE_KEY)) {
|
if (Cacher.getCacheContents().has(CLIENT_CACHE_KEY)) {
|
||||||
rd_unity.setSelected(Cacher.getCacheContents().getBoolean(USE_UNITY_CLIENT_CACHE_KEY));
|
switch (Cacher.getCacheContents().getEnum(HClient.class, CLIENT_CACHE_KEY)) {
|
||||||
rd_flash.setSelected(!Cacher.getCacheContents().getBoolean(USE_UNITY_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));
|
Platform.runLater(() -> rd_unity.setSelected(true));
|
||||||
getHConnection().startUnity();
|
getHConnection().startUnity();
|
||||||
}
|
}
|
||||||
|
else if (connectMode.equals("nitro")) {
|
||||||
|
Platform.runLater(() -> rd_nitro.setSelected(true));
|
||||||
|
getHConnection().startNitro();
|
||||||
|
}
|
||||||
Platform.runLater(this::updateInputUI);
|
Platform.runLater(this::updateInputUI);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -244,16 +259,16 @@ public class ConnectionController extends SubForm {
|
|||||||
|
|
||||||
btnConnect.setDisable(true);
|
btnConnect.setDisable(true);
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
if (useFlash()) {
|
if (isClientMode(HClient.FLASH)) {
|
||||||
if (cbx_autodetect.isSelected()) {
|
if (cbx_autodetect.isSelected()) {
|
||||||
getHConnection().start();
|
getHConnection().start();
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
getHConnection().start(inpHost.getEditor().getText(), Integer.parseInt(inpPort.getEditor().getText()));
|
getHConnection().start(inpHost.getEditor().getText(), Integer.parseInt(inpPort.getEditor().getText()));
|
||||||
}
|
}
|
||||||
}
|
} else if (isClientMode(HClient.UNITY)) {
|
||||||
else {
|
|
||||||
getHConnection().startUnity();
|
getHConnection().startUnity();
|
||||||
|
} else if (isClientMode(HClient.NITRO)) {
|
||||||
|
getHConnection().startNitro();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -269,7 +284,13 @@ public class ConnectionController extends SubForm {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onExit() {
|
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();
|
getHConnection().abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -280,4 +301,17 @@ public class ConnectionController extends SubForm {
|
|||||||
private boolean useFlash() {
|
private boolean useFlash() {
|
||||||
return rd_flash.isSelected();
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<?import javafx.scene.layout.*?>
|
<?import javafx.scene.layout.*?>
|
||||||
<?import javafx.scene.text.*?>
|
<?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>
|
||||||
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
|
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
|
||||||
</columnConstraints>
|
</columnConstraints>
|
||||||
@ -132,9 +132,9 @@
|
|||||||
<columnConstraints>
|
<columnConstraints>
|
||||||
<ColumnConstraints hgrow="SOMETIMES" maxWidth="149.0" minWidth="10.0" prefWidth="76.0" />
|
<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="149.0" minWidth="10.0" prefWidth="25.0" />
|
||||||
<ColumnConstraints hgrow="SOMETIMES" maxWidth="177.0" minWidth="66.0" prefWidth="93.0" />
|
<ColumnConstraints hgrow="SOMETIMES" maxWidth="177.0" minWidth="66.0" prefWidth="90.0" />
|
||||||
<ColumnConstraints hgrow="SOMETIMES" maxWidth="133.0" minWidth="8.0" prefWidth="98.0" />
|
<ColumnConstraints hgrow="SOMETIMES" maxWidth="133.0" minWidth="8.0" prefWidth="70.0" />
|
||||||
<ColumnConstraints hgrow="SOMETIMES" maxWidth="242.0" minWidth="10.0" prefWidth="32.0" />
|
<ColumnConstraints hgrow="SOMETIMES" maxWidth="242.0" minWidth="10.0" prefWidth="70.0" />
|
||||||
</columnConstraints>
|
</columnConstraints>
|
||||||
<rowConstraints>
|
<rowConstraints>
|
||||||
<RowConstraints minHeight="20.0" prefHeight="34.0" vgrow="SOMETIMES" />
|
<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">
|
<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>
|
<GridPane.margin>
|
||||||
<Insets />
|
<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>
|
</children>
|
||||||
<padding>
|
<padding>
|
||||||
<Insets bottom="8.0" top="8.0" />
|
<Insets bottom="8.0" top="8.0" />
|
||||||
|
Loading…
Reference in New Issue
Block a user