diff --git a/G-Earth/pom.xml b/G-Earth/pom.xml index c30a5ce..3c4a4ff 100644 --- a/G-Earth/pom.xml +++ b/G-Earth/pom.xml @@ -10,13 +10,14 @@ 1.8 - 9.4.48.v20220622 + 9.4.50.v20221201 + 1.3.5 G-Earth G-Earth-Parent - 1.5.2 + 1.5.3 @@ -218,17 +219,18 @@ org.jsoup jsoup - 1.14.2 + 1.15.3 com.github.tulskiy jkeymaster 1.3 - - - org.slf4j - slf4j-jdk14 - 2.0.0-alpha0 + + + org.slf4j + slf4j-api + + @@ -236,14 +238,11 @@ maven-artifact 3.6.3 - - javax.websocket javax.websocket-api 1.1 - org.eclipse.jetty jetty-server @@ -274,17 +273,32 @@ com.github.ganskef littleproxy-mitm 1.1.0 + + + org.slf4j + slf4j-api + + + org.slf4j + slf4j-log4j12 + + - - - - G-Earth G-Wasm-Minimal 1.0.3 - + + ch.qos.logback + logback-core + ${logback.version} + + + ch.qos.logback + logback-classic + ${logback.version} + diff --git a/G-Earth/src/main/java/gearth/GEarth.java b/G-Earth/src/main/java/gearth/GEarth.java index 361c69c..cc5fb52 100644 --- a/G-Earth/src/main/java/gearth/GEarth.java +++ b/G-Earth/src/main/java/gearth/GEarth.java @@ -22,7 +22,7 @@ import javafx.stage.StageStyle; public class GEarth extends Application { public static GEarth main; - public static String version = "1.5.2"; + public static String version = "1.5.3"; public static String gitApi = "https://api.github.com/repos/sirjonasxx/G-Earth/releases/latest"; public static ObservableObject observableTheme; @@ -43,7 +43,13 @@ public class GEarth extends Application { stage = primaryStage; FXMLLoader loader = new FXMLLoader(getClass().getResource("/gearth/ui/G-Earth.fxml")); - Parent root = loader.load(); + Parent root; + try { + root = loader.load(); + } catch (Exception e) { + e.printStackTrace(); + return; + } controller = loader.getController(); controller.setStage(primaryStage); stage.initStyle(StageStyle.TRANSPARENT); diff --git a/G-Earth/src/main/java/gearth/extensions/Extension.java b/G-Earth/src/main/java/gearth/extensions/Extension.java index 9409af0..55c80db 100644 --- a/G-Earth/src/main/java/gearth/extensions/Extension.java +++ b/G-Earth/src/main/java/gearth/extensions/Extension.java @@ -1,12 +1,14 @@ package gearth.extensions; import gearth.misc.HostInfo; +import gearth.services.extension_handler.extensions.implementations.network.NetworkExtensionMessage; +import gearth.services.extension_handler.extensions.implementations.network.NetworkExtensionMessage.Incoming; +import gearth.services.extension_handler.extensions.implementations.network.NetworkExtensionMessage.Outgoing; import gearth.services.packet_info.PacketInfoManager; import gearth.protocol.HMessage; import gearth.protocol.HPacket; import gearth.protocol.connection.HClient; import gearth.services.Constants; -import gearth.services.extension_handler.extensions.implementations.network.NetworkExtensionInfo; import java.io.*; import java.net.Socket; @@ -108,10 +110,10 @@ public abstract class Extension extends ExtensionBase { packet.fixLength(); - if (packet.headerId() == NetworkExtensionInfo.OUTGOING_MESSAGES_IDS.INFOREQUEST) { + if (packet.headerId() == Outgoing.InfoRequest.HEADER_ID) { ExtensionInfo info = getInfoAnnotations(); - HPacket response = new HPacket(NetworkExtensionInfo.INCOMING_MESSAGES_IDS.EXTENSIONINFO); + HPacket response = new HPacket(Incoming.ExtensionInfo.HEADER_ID); response.appendString(info.Title()) .appendString(info.Author()) .appendString(info.Version()) @@ -124,7 +126,7 @@ public abstract class Extension extends ExtensionBase { .appendBoolean(canDelete()); writeToStream(response.toBytes()); } - else if (packet.headerId() == NetworkExtensionInfo.OUTGOING_MESSAGES_IDS.CONNECTIONSTART) { + else if (packet.headerId() == Outgoing.ConnectionStart.HEADER_ID) { String host = packet.readString(); int connectionPort = packet.readInteger(); String hotelVersion = packet.readString(); @@ -143,10 +145,10 @@ public abstract class Extension extends ExtensionBase { ); onStartConnection(); } - else if (packet.headerId() == NetworkExtensionInfo.OUTGOING_MESSAGES_IDS.CONNECTIONEND) { + else if (packet.headerId() == Outgoing.ConnectionEnd.HEADER_ID) { onEndConnection(); } - else if (packet.headerId() == NetworkExtensionInfo.OUTGOING_MESSAGES_IDS.FLAGSCHECK) { + else if (packet.headerId() == Outgoing.FlagsCheck.HEADER_ID) { // body = an array of G-Earths gearth flags if (flagRequestCallback != null) { int arraysize = packet.readInteger(); @@ -158,7 +160,7 @@ public abstract class Extension extends ExtensionBase { } flagRequestCallback = null; } - else if (packet.headerId() == NetworkExtensionInfo.OUTGOING_MESSAGES_IDS.INIT) { + else if (packet.headerId() == Outgoing.Init.HEADER_ID) { delayed_init = packet.readBoolean(); HostInfo hostInfo = HostInfo.fromPacket(packet); updateHostInfo(hostInfo); @@ -167,21 +169,21 @@ public abstract class Extension extends ExtensionBase { } writeToConsole("green","Extension \"" + getInfoAnnotations().Title() + "\" successfully initialized", false); } - else if (packet.headerId() == NetworkExtensionInfo.OUTGOING_MESSAGES_IDS.ONDOUBLECLICK) { + else if (packet.headerId() == Outgoing.OnDoubleClick.HEADER_ID) { onClick(); } - else if (packet.headerId() == NetworkExtensionInfo.OUTGOING_MESSAGES_IDS.PACKETINTERCEPT) { + else if (packet.headerId() == Outgoing.PacketIntercept.HEADER_ID) { String stringifiedMessage = packet.readLongString(); HMessage habboMessage = new HMessage(stringifiedMessage); modifyMessage(habboMessage); - HPacket response = new HPacket(NetworkExtensionInfo.INCOMING_MESSAGES_IDS.MANIPULATEDPACKET); + HPacket response = new HPacket(Incoming.ManipulatedPacket.MANIPULATED_PACKET); response.appendLongString(habboMessage.stringify()); writeToStream(response.toBytes()); } - else if (packet.headerId() == NetworkExtensionInfo.OUTGOING_MESSAGES_IDS.UPDATEHOSTINFO) { + else if (packet.headerId() == Outgoing.UpdateHostInfo.HEADER_ID) { HostInfo hostInfo = HostInfo.fromPacket(packet); updateHostInfo(hostInfo); } @@ -231,7 +233,7 @@ public abstract class Extension extends ExtensionBase { if (!packet.isPacketComplete()) packet.completePacket(packetInfoManager); if (!packet.isPacketComplete()) return false; - HPacket packet1 = new HPacket(NetworkExtensionInfo.INCOMING_MESSAGES_IDS.SENDMESSAGE); + HPacket packet1 = new HPacket(Incoming.SendMessage.HEADER_ID); packet1.appendByte(direction == HMessage.Direction.TOCLIENT ? (byte)0 : (byte)1); packet1.appendInt(packet.getBytesLength()); packet1.appendBytes(packet.toBytes()); @@ -253,7 +255,7 @@ public abstract class Extension extends ExtensionBase { if (this.flagRequestCallback != null) return false; this.flagRequestCallback = flagRequestCallback; try { - writeToStream(new HPacket(NetworkExtensionInfo.INCOMING_MESSAGES_IDS.REQUESTFLAGS).toBytes()); + writeToStream(new HPacket(Incoming.RequestFlags.HEADER_ID).toBytes()); return true; } catch (IOException e) { e.printStackTrace(); @@ -279,7 +281,7 @@ public abstract class Extension extends ExtensionBase { private void writeToConsole(String colorClass, String s, boolean mentionTitle) { String text = "[" + colorClass + "]" + (mentionTitle ? (getInfoAnnotations().Title() + " --> ") : "") + s; - HPacket packet = new HPacket(NetworkExtensionInfo.INCOMING_MESSAGES_IDS.EXTENSIONCONSOLELOG); + HPacket packet = new HPacket(Incoming.ExtensionConsoleLog.HEADER_ID); packet.appendString(text); try { writeToStream(packet.toBytes()); diff --git a/G-Earth/src/main/java/gearth/misc/AdminValidator.java b/G-Earth/src/main/java/gearth/misc/AdminValidator.java index 7a96735..8b1c81e 100644 --- a/G-Earth/src/main/java/gearth/misc/AdminValidator.java +++ b/G-Earth/src/main/java/gearth/misc/AdminValidator.java @@ -1,11 +1,11 @@ package gearth.misc; import gearth.ui.titlebar.TitleBarController; +import gearth.ui.translations.LanguageBundle; import javafx.application.Platform; import javafx.scene.control.Alert; import javafx.scene.control.ButtonType; import javafx.scene.control.Label; -import javafx.scene.layout.Region; import java.io.IOException; import java.io.PrintStream; @@ -47,7 +47,7 @@ public class AdminValidator { if (!AdminValidator.isAdmin()) { Platform.runLater(() -> { Alert alert = new Alert(Alert.AlertType.WARNING, "", ButtonType.OK); - alert.getDialogPane().setContent(new Label("G-Earth needs admin privileges in order to work on Flash,\nplease restart G-Earth with admin permissions unless\nyou're using Unity")); + alert.getDialogPane().setContent(new Label(LanguageBundle.get("alert.adminvalidator.content").replaceAll("\\\\n", System.lineSeparator()))); try { TitleBarController.create(alert).showAlert(); } catch (IOException e) { diff --git a/G-Earth/src/main/java/gearth/misc/Cacher.java b/G-Earth/src/main/java/gearth/misc/Cacher.java index c5a8000..882c4ab 100644 --- a/G-Earth/src/main/java/gearth/misc/Cacher.java +++ b/G-Earth/src/main/java/gearth/misc/Cacher.java @@ -85,6 +85,11 @@ public class Cacher { object.put(key, val); updateCache(object, cache_filename); } + public static void remove(String key, String cache_filename) { + JSONObject object = getCacheContents(cache_filename); + if (object.has(key)) object.remove(key); + updateCache(object, cache_filename); + } public static Object get(String key, String cache_filename) { JSONObject object = getCacheContents(cache_filename); if (object.has(key)) return object.get(key); @@ -113,6 +118,9 @@ public class Cacher { public static void put(String key, Object val) { put(key, val, DEFAULT_CACHE_FILENAME); } + public static void remove(String key) { + remove(key, DEFAULT_CACHE_FILENAME); + } public static Object get(String key) { return get(key, DEFAULT_CACHE_FILENAME); } diff --git a/G-Earth/src/main/java/gearth/misc/StringUtils.java b/G-Earth/src/main/java/gearth/misc/StringUtils.java new file mode 100644 index 0000000..660447f --- /dev/null +++ b/G-Earth/src/main/java/gearth/misc/StringUtils.java @@ -0,0 +1,22 @@ +package gearth.misc; + +/** + * Contains utility methods for {@link String} conversions. + */ +public final class StringUtils { + + /** + * Interprets the argued {@link String} as a hex-string and converts it to a byte array. + * @param hexString the {@link String} to be converted. + * @return a byte array containing the converted hex string values. + */ + public static byte[] hexStringToByteArray(String hexString) { + int len = hexString.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) + + Character.digit(hexString.charAt(i+1), 16)); + } + return data; + } +} diff --git a/G-Earth/src/main/java/gearth/misc/UpdateChecker.java b/G-Earth/src/main/java/gearth/misc/UpdateChecker.java index 93a0827..ec001b8 100644 --- a/G-Earth/src/main/java/gearth/misc/UpdateChecker.java +++ b/G-Earth/src/main/java/gearth/misc/UpdateChecker.java @@ -2,6 +2,7 @@ package gearth.misc; import gearth.GEarth; import gearth.ui.titlebar.TitleBarController; +import gearth.ui.translations.LanguageBundle; import javafx.application.Platform; import javafx.scene.control.Alert; import javafx.scene.control.ButtonType; @@ -37,10 +38,10 @@ public class UpdateChecker { String body = (String)object.get("body"); boolean isForcedUpdate = body.contains("(!)"); - Alert alert = new Alert(isForcedUpdate ? Alert.AlertType.ERROR : Alert.AlertType.INFORMATION, "G-Earth is outdated!", ButtonType.OK); + Alert alert = new Alert(isForcedUpdate ? Alert.AlertType.ERROR : Alert.AlertType.INFORMATION, LanguageBundle.get("alert.outdated.title"), ButtonType.OK); FlowPane fp = new FlowPane(); - Label lbl = new Label("A new version of G-Earth has been found ("+gitv+")" + System.lineSeparator()+ System.lineSeparator() + "Update to the latest version:"); + Label lbl = new Label(LanguageBundle.get("alert.outdated.content.newversion") + " ("+gitv+")" + System.lineSeparator()+ System.lineSeparator() + LanguageBundle.get("alert.outdated.content.update") + ":"); Hyperlink link = new Hyperlink("https://github.com/sirjonasxx/G-Earth/releases"); fp.getChildren().addAll( lbl, link); link.setOnAction(event -> { @@ -51,7 +52,7 @@ public class UpdateChecker { WebView webView = new WebView(); - webView.getEngine().loadContent("A new version of G-Earth has been found ("+gitv+")

Update to the latest version:
https://github.com/sirjonasxx/G-Earth/releases"); + webView.getEngine().loadContent(String.format("%s (%s)

%s:
https://github.com/sirjonasxx/G-Earth/releases", LanguageBundle.get("alert.outdated.content.newversion"), gitv, LanguageBundle.get("alert.outdated.content.update"))); webView.setPrefSize(500, 200); alert.setResizable(false); diff --git a/G-Earth/src/main/java/gearth/protocol/HConnection.java b/G-Earth/src/main/java/gearth/protocol/HConnection.java index 70bd0c4..9263687 100644 --- a/G-Earth/src/main/java/gearth/protocol/HConnection.java +++ b/G-Earth/src/main/java/gearth/protocol/HConnection.java @@ -1,6 +1,8 @@ package gearth.protocol; import gearth.misc.listenerpattern.Observable; +import gearth.protocol.connection.packetsafety.PacketSafetyManager; +import gearth.protocol.connection.packetsafety.SafePacketsContainer; import gearth.protocol.connection.proxy.nitro.NitroProxyProvider; import gearth.services.packet_info.PacketInfoManager; import gearth.protocol.connection.HClient; @@ -13,16 +15,18 @@ import gearth.protocol.connection.proxy.unity.UnityProxyProvider; import gearth.services.extension_handler.ExtensionHandler; import java.io.IOException; +import java.util.function.Consumer; public class HConnection { public static volatile boolean DECRYPTPACKETS = true; - public static volatile boolean DEBUG = false; + public static volatile boolean DEBUG = true; private volatile ExtensionHandler extensionHandler = null; private volatile Object[] trafficObservables = {new Observable(), new Observable(), new Observable()}; private volatile Observable stateObservable = new Observable<>(); + private volatile Observable> developerModeChangeObservable = new Observable<>(); private volatile HState state = HState.NOT_CONNECTED; private volatile HProxy proxy = null; @@ -30,6 +34,8 @@ public class HConnection { private ProxyProviderFactory proxyProviderFactory; private ProxyProvider proxyProvider = null; + private volatile boolean developerMode = false; + public HConnection() { HConnection selff = this; proxyProviderFactory = new ProxyProviderFactory( @@ -37,6 +43,8 @@ public class HConnection { selff::setState, this ); + + PacketSafetyManager.PACKET_SAFETY_MANAGER.initialize(this); } public HState getState() { @@ -147,30 +155,54 @@ public class HConnection { public boolean sendToClient(HPacket packet) { - HProxy proxy = this.proxy; - if (proxy == null) return false; - - if (!packet.isPacketComplete()) { - PacketInfoManager packetInfoManager = getPacketInfoManager(); - packet.completePacket(packetInfoManager); - - if (!packet.isPacketComplete() || !packet.canSendToClient()) return false; - } - + if (!canSendPacket(HMessage.Direction.TOCLIENT, packet)) return false; return proxy.sendToClient(packet); } + public boolean sendToServer(HPacket packet) { + if (!canSendPacket(HMessage.Direction.TOSERVER, packet)) return false; + return proxy.sendToServer(packet); + } + + public boolean canSendPacket(HMessage.Direction direction, HPacket packet) { + return isPacketSendingAllowed(direction, packet) && (developerMode || isPacketSendingSafe(direction, packet)); + } + + public boolean isPacketSendingAllowed(HMessage.Direction direction, HPacket packet) { + if (state != HState.CONNECTED) return false; + HProxy proxy = this.proxy; if (proxy == null) return false; + if (packet.isCorrupted()) return false; if (!packet.isPacketComplete()) { PacketInfoManager packetInfoManager = getPacketInfoManager(); packet.completePacket(packetInfoManager); - if (!packet.isPacketComplete() || !packet.canSendToServer()) return false; + return packet.isPacketComplete() && + (packet.canSendToClient() || direction != HMessage.Direction.TOCLIENT) && + (packet.canSendToServer() || direction != HMessage.Direction.TOSERVER); } - return proxy.sendToServer(packet); + return true; + } + + public boolean isPacketSendingSafe(HMessage.Direction direction, HPacket packet) { + if (proxy == null) return true; // do not mark unsafe, but won't pass "isPacketSendingAllowed()" check + String hotelVersion = proxy.getHotelVersion(); + if (hotelVersion == null) return true; + + SafePacketsContainer packetsContainer = PacketSafetyManager.PACKET_SAFETY_MANAGER.getPacketContainer(hotelVersion); + return packetsContainer.isPacketSafe(packet.headerId(), direction); + } + + public void setDeveloperMode(boolean developerMode) { + this.developerMode = developerMode; + developerModeChangeObservable.fireEvent(listener -> listener.accept(developerMode)); + } + + public void onDeveloperModeChange(Consumer onChange) { + developerModeChangeObservable.addListener(onChange); } public String getClientHost() { @@ -205,7 +237,7 @@ public class HConnection { if (proxy == null) { return null; } - return proxy.gethClient(); + return proxy.getHClient(); } public PacketInfoManager getPacketInfoManager() { @@ -223,4 +255,12 @@ public class HConnection { public ProxyProvider getProxyProvider() { return proxyProvider; } + + @Override + public String toString() { + return "HConnection{" + + "state=" + state + + ", proxy=" + proxy + + '}'; + } } diff --git a/G-Earth/src/main/java/gearth/protocol/HPacket.java b/G-Earth/src/main/java/gearth/protocol/HPacket.java index f8fdd62..bceb962 100644 --- a/G-Earth/src/main/java/gearth/protocol/HPacket.java +++ b/G-Earth/src/main/java/gearth/protocol/HPacket.java @@ -121,7 +121,7 @@ public class HPacket implements StringifyAble { } public void completePacket(PacketInfoManager packetInfoManager) { - if (isCorrupted() || identifier == null) return; + if (isCorrupted() || identifier == null || packetInfoManager == null) return; PacketInfo packetInfo = packetInfoManager.getPacketInfoFromName(identifierDirection, identifier); if (packetInfo == null) { @@ -718,7 +718,15 @@ public class HPacket implements StringifyAble { public String toExpression() { if (isCorrupted()) return ""; - return PacketStringUtils.predictedExpression(this, null); + return PacketStringUtils.predictedExpression(this, dummyPacketInfo()); + } + + /** + * Provides dummy packet information for a packet that hasn't been completed with headerId yet + */ + private PacketInfo dummyPacketInfo() { + if (isPacketComplete()) return null; + return new PacketInfo(identifierDirection, -1, "", identifier, null, ""); } public void setBytes(byte[] bytes) { diff --git a/G-Earth/src/main/java/gearth/protocol/connection/HProxy.java b/G-Earth/src/main/java/gearth/protocol/connection/HProxy.java index e3bb228..e565b5d 100644 --- a/G-Earth/src/main/java/gearth/protocol/connection/HProxy.java +++ b/G-Earth/src/main/java/gearth/protocol/connection/HProxy.java @@ -1,6 +1,9 @@ package gearth.protocol.connection; import gearth.protocol.HPacket; +import gearth.protocol.connection.packetsafety.PacketSafetyManager; +import gearth.protocol.connection.packetsafety.SafePacketsContainer; +import gearth.services.packet_info.PacketInfo; import gearth.services.packet_info.PacketInfoManager; import gearth.protocol.packethandler.PacketHandler; @@ -45,6 +48,11 @@ public class HProxy { this.hotelVersion = hotelVersion; this.clientIdentifier = clientIdentifier; this.packetInfoManager = PacketInfoManager.fromHotelVersion(hotelVersion, hClient); + + SafePacketsContainer packetsContainer = PacketSafetyManager.PACKET_SAFETY_MANAGER.getPacketContainer(hotelVersion); + for (PacketInfo packetInfo : packetInfoManager.getPacketInfoList()) { + packetsContainer.validateSafePacket(packetInfo.getHeaderId(), packetInfo.getDestination()); + } } public boolean sendToServer(HPacket packet) { @@ -101,11 +109,28 @@ public class HProxy { return hotelVersion; } - public HClient gethClient() { + public HClient getHClient() { return hClient; } public PacketInfoManager getPacketInfoManager() { return packetInfoManager; } + + @Override + public String toString() { + return "HProxy{" + + "hClient=" + hClient + + ", input_domain='" + input_domain + '\'' + + ", actual_domain='" + actual_domain + '\'' + + ", actual_port=" + actual_port + + ", intercept_port=" + intercept_port + + ", intercept_host='" + intercept_host + '\'' + + ", proxy_server=" + proxy_server + + ", inHandler=" + inHandler + + ", outHandler=" + outHandler + + ", hotelVersion='" + hotelVersion + '\'' + + ", clientIdentifier='" + clientIdentifier + '\'' + + '}'; + } } diff --git a/G-Earth/src/main/java/gearth/protocol/connection/packetsafety/PacketSafetyManager.java b/G-Earth/src/main/java/gearth/protocol/connection/packetsafety/PacketSafetyManager.java new file mode 100644 index 0000000..76b5c42 --- /dev/null +++ b/G-Earth/src/main/java/gearth/protocol/connection/packetsafety/PacketSafetyManager.java @@ -0,0 +1,32 @@ +package gearth.protocol.connection.packetsafety; + +import gearth.protocol.HConnection; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class PacketSafetyManager { + + public static final PacketSafetyManager PACKET_SAFETY_MANAGER = new PacketSafetyManager(); + + private final Map safePacketContainers = new ConcurrentHashMap<>(); + + private PacketSafetyManager() { + + } + + public void initialize(HConnection connection) { + connection.addTrafficListener(0, message -> { + String hotelVersion = connection.getHotelVersion(); + if (hotelVersion.equals("")) return; + + SafePacketsContainer safePacketsContainer = getPacketContainer(hotelVersion); + safePacketsContainer.validateSafePacket(message.getPacket().headerId(), message.getDestination()); + }); + } + + public SafePacketsContainer getPacketContainer(String hotelVersion) { + return safePacketContainers.computeIfAbsent(hotelVersion, v -> new SafePacketsContainer()); + } + +} diff --git a/G-Earth/src/main/java/gearth/protocol/connection/packetsafety/SafePacketsContainer.java b/G-Earth/src/main/java/gearth/protocol/connection/packetsafety/SafePacketsContainer.java new file mode 100644 index 0000000..ef47977 --- /dev/null +++ b/G-Earth/src/main/java/gearth/protocol/connection/packetsafety/SafePacketsContainer.java @@ -0,0 +1,24 @@ +package gearth.protocol.connection.packetsafety; + +import gearth.protocol.HMessage; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +public class SafePacketsContainer { + + private final Set safeIncomingIds = Collections.synchronizedSet(new HashSet<>()); + private final Set safeOutgoingIds = Collections.synchronizedSet(new HashSet<>()); + + public void validateSafePacket(int headerId, HMessage.Direction direction) { + Set headerIds = direction == HMessage.Direction.TOCLIENT ? safeIncomingIds : safeOutgoingIds; + headerIds.add(headerId); + } + + public boolean isPacketSafe(int headerId, HMessage.Direction direction) { + Set headerIds = direction == HMessage.Direction.TOCLIENT ? safeIncomingIds : safeOutgoingIds; + return headerIds.contains(headerId); + } + +} diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/ProxyProviderFactory.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/ProxyProviderFactory.java index 783f4d0..9070132 100644 --- a/G-Earth/src/main/java/gearth/protocol/connection/proxy/ProxyProviderFactory.java +++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/ProxyProviderFactory.java @@ -1,6 +1,5 @@ package gearth.protocol.connection.proxy; -import gearth.GEarth; import gearth.misc.Cacher; import gearth.misc.OSValidator; import gearth.protocol.HConnection; @@ -10,6 +9,7 @@ import gearth.protocol.connection.proxy.flash.NormalFlashProxyProvider; import gearth.protocol.connection.proxy.flash.unix.LinuxRawIpFlashProxyProvider; import gearth.protocol.connection.proxy.flash.windows.WindowsRawIpFlashProxyProvider; import gearth.ui.titlebar.TitleBarController; +import gearth.ui.translations.LanguageBundle; import javafx.application.Platform; import javafx.scene.control.Alert; import javafx.scene.control.ButtonType; @@ -108,9 +108,7 @@ public class ProxyProviderFactory { Platform.runLater(() -> { Alert alert = new Alert(Alert.AlertType.ERROR, "", ButtonType.OK); - alert.getDialogPane().getChildren().add(new Label("G-Earth is already connected to this hotel.\n" + - "Due to current limitations you can only connect one session per hotel to G-Earth in Raw IP mode on Windows.\n\n" + - "You can bypass this by using a SOCKS proxy [Extra -> Advanced -> SOCKS]")); + alert.getDialogPane().getChildren().add(new Label(LanguageBundle.get("alert.alreadyconnected.content").replaceAll("\\\\n", System.lineSeparator()))); alert.getDialogPane().setMinHeight(Region.USE_PREF_SIZE); alert.setResizable(false); try { diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/flash/FlashProxyProvider.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/flash/FlashProxyProvider.java index dbbc7af..dedddde 100644 --- a/G-Earth/src/main/java/gearth/protocol/connection/proxy/flash/FlashProxyProvider.java +++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/flash/FlashProxyProvider.java @@ -1,6 +1,5 @@ package gearth.protocol.connection.proxy.flash; -import gearth.GEarth; import gearth.protocol.HConnection; import gearth.protocol.connection.HProxy; import gearth.protocol.connection.HProxySetter; @@ -8,17 +7,18 @@ import gearth.protocol.connection.HState; import gearth.protocol.connection.HStateSetter; import gearth.protocol.connection.proxy.ProxyProvider; import gearth.protocol.memory.Rc4Obtainer; +import gearth.protocol.packethandler.flash.FlashPacketHandler; import gearth.protocol.packethandler.flash.IncomingFlashPacketHandler; import gearth.protocol.packethandler.flash.OutgoingFlashPacketHandler; -import gearth.protocol.packethandler.flash.FlashPacketHandler; import gearth.ui.titlebar.TitleBarController; +import gearth.ui.translations.LanguageBundle; import javafx.application.Platform; import javafx.scene.control.Alert; import javafx.scene.control.ButtonType; import javafx.scene.control.Label; -import javafx.scene.image.Image; import javafx.scene.layout.Region; -import javafx.stage.Stage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.Socket; @@ -27,6 +27,7 @@ import java.util.concurrent.Semaphore; public abstract class FlashProxyProvider implements ProxyProvider { + protected final Logger logger = LoggerFactory.getLogger(getClass()); protected final HProxySetter proxySetter; protected final HStateSetter stateSetter; protected final HConnection hConnection; @@ -47,7 +48,8 @@ public abstract class FlashProxyProvider implements ProxyProvider { client.setSoTimeout(0); server.setSoTimeout(0); - if (HConnection.DEBUG) System.out.println(server.getLocalAddress().getHostAddress() + ": " + server.getLocalPort()); + logger.debug("Starting proxy thread at {}:{}", server.getLocalAddress().getHostAddress(), server.getLocalPort()); + Rc4Obtainer rc4Obtainer = new Rc4Obtainer(hConnection); OutgoingFlashPacketHandler outgoingHandler = new OutgoingFlashPacketHandler(server.getOutputStream(), hConnection.getTrafficObservables(), hConnection.getExtensionHandler()); @@ -73,13 +75,13 @@ public abstract class FlashProxyProvider implements ProxyProvider { try { if (!server.isClosed()) server.close(); if (!client.isClosed()) client.close(); - if (HConnection.DEBUG) System.out.println("STOP"); + logger.debug("Closed server {} and client {}, dataStreams {}", server, client, datastream); if (datastream[0]) { onConnectEnd(); }; } catch (IOException e) { - e.printStackTrace(); + logger.error("Failed to gracefully stop", e); } } @@ -95,8 +97,7 @@ public abstract class FlashProxyProvider implements ProxyProvider { } } catch (IOException ignore) { -// System.err.println(packetHandler instanceof IncomingPacketHandler ? "incoming" : "outgoing"); -// ignore.printStackTrace(); +// logger.error("Failed to read input stream from socket {}", socket, ignore); } finally { abort.release(); } @@ -126,15 +127,14 @@ public abstract class FlashProxyProvider implements ProxyProvider { protected void showInvalidConnectionError() { Platform.runLater(() -> { Alert alert = new Alert(Alert.AlertType.ERROR, "", ButtonType.OK); - alert.getDialogPane().getChildren().add(new Label("You entered invalid connection information, G-Earth could not connect")); + alert.getDialogPane().getChildren().add(new Label(LanguageBundle.get("alert.invalidconnection.content"))); alert.getDialogPane().setMinHeight(Region.USE_PREF_SIZE); alert.setResizable(false); try { TitleBarController.create(alert).showAlert(); } catch (IOException e) { - e.printStackTrace(); + logger.error("Failed to create invalid connection error alert", e); } }); } - } diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/flash/NormalFlashProxyProvider.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/flash/NormalFlashProxyProvider.java index 49b4c6e..4404cf4 100644 --- a/G-Earth/src/main/java/gearth/protocol/connection/proxy/flash/NormalFlashProxyProvider.java +++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/flash/NormalFlashProxyProvider.java @@ -1,6 +1,5 @@ package gearth.protocol.connection.proxy.flash; -import gearth.GEarth; import gearth.misc.Cacher; import gearth.protocol.HConnection; import gearth.protocol.connection.*; @@ -14,8 +13,6 @@ import gearth.ui.titlebar.TitleBarController; import javafx.application.Platform; import javafx.scene.control.Alert; import javafx.scene.control.ButtonType; -import javafx.scene.image.Image; -import javafx.stage.Stage; import java.io.IOException; import java.net.*; @@ -26,7 +23,6 @@ public class NormalFlashProxyProvider extends FlashProxyProvider { private List potentialHosts; - private static final HostReplacer hostsReplacer = HostReplacerFactory.get(); private volatile boolean hostRedirected = false; @@ -113,7 +109,7 @@ public class NormalFlashProxyProvider extends FlashProxyProvider { try { TitleBarController.create(a).showAlertAndWait(); } catch (IOException ex) { - ex.printStackTrace(); + logger.error("Failed to create port in use error alert", ex); } }); throw new IOException(e); @@ -128,8 +124,7 @@ public class NormalFlashProxyProvider extends FlashProxyProvider { Socket client = proxy_server.accept(); proxy = potentialProxy; closeAllProxies(proxy); - if (HConnection.DEBUG) System.out.println("accepted a proxy"); - + logger.debug("Accepted proxy {}, starting proxy thread (useSocks={})",proxy, useSocks); new Thread(() -> { try { Socket server; @@ -152,18 +147,14 @@ public class NormalFlashProxyProvider extends FlashProxyProvider { // should only happen when SOCKS configured badly showInvalidConnectionError(); abort(); - e.printStackTrace(); + logger.error("Failed to configure SOCKS proxy", e); } catch (InterruptedException | IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); + logger.error("An unexpected exception occurred", e); } }).start(); - - } catch (IOException e1) { - // TODO Auto-generated catch block -// e1.printStackTrace(); + logger.error("An unexpected exception occurred", e1); } } } catch (Exception e) { @@ -171,10 +162,7 @@ public class NormalFlashProxyProvider extends FlashProxyProvider { } }).start(); } - - - if (HConnection.DEBUG) System.out.println("done waiting for clients with: " + hConnection.getState() ); - + logger.debug("Done waiting for clients with connection state {}", hConnection.getState()); } @Override @@ -233,12 +221,10 @@ public class NormalFlashProxyProvider extends FlashProxyProvider { try { proxy.getProxy_server().close(); } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); + logger.error("Failed to close all proxies", e); } } } } -// potentialProxies = Collections.singletonList(except); } } diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/flash/unix/LinuxRawIpFlashProxyProvider.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/flash/unix/LinuxRawIpFlashProxyProvider.java index 3b31f78..95337fd 100644 --- a/G-Earth/src/main/java/gearth/protocol/connection/proxy/flash/unix/LinuxRawIpFlashProxyProvider.java +++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/flash/unix/LinuxRawIpFlashProxyProvider.java @@ -54,7 +54,7 @@ public class LinuxRawIpFlashProxyProvider extends FlashProxyProvider { maybeAddMapping(); - if (HConnection.DEBUG) System.out.println("Added mapping for raw IP"); + logger.debug("Added mapping for raw IP"); ServerSocket proxy_server = new ServerSocket(proxy.getIntercept_port(), 10, InetAddress.getByName(proxy.getIntercept_host())); proxy.initProxy(proxy_server); @@ -62,10 +62,10 @@ public class LinuxRawIpFlashProxyProvider extends FlashProxyProvider { stateSetter.setState(HState.WAITING_FOR_CLIENT); while ((hConnection.getState() == HState.WAITING_FOR_CLIENT) && !proxy_server.isClosed()) { try { - if (HConnection.DEBUG) System.out.println("try accept proxy"); + logger.debug("Trying to accept a new proxy from {}", proxy_server); Socket client = proxy_server.accept(); - if (HConnection.DEBUG) System.out.println("accepted a proxy"); + logger.debug("Accepted a proxy {}", client); new Thread(() -> { try { @@ -82,7 +82,7 @@ public class LinuxRawIpFlashProxyProvider extends FlashProxyProvider { } catch (Exception e) { - e.printStackTrace(); + logger.error("An unexpected exception occurred in proxy listener", e); } }).start(); } @@ -114,7 +114,7 @@ public class LinuxRawIpFlashProxyProvider extends FlashProxyProvider { try { proxy.getProxy_server().close(); } catch (IOException e) { - e.printStackTrace(); + logger.error("Failed to close proxy server", e); } } } @@ -136,6 +136,7 @@ public class LinuxRawIpFlashProxyProvider extends FlashProxyProvider { } catch (SocketTimeoutException e) { showInvalidConnectionError(); + logger.error("Connection to proxy {} timed out", proxy, e); return false; } @@ -151,7 +152,7 @@ public class LinuxRawIpFlashProxyProvider extends FlashProxyProvider { createSocksProxyThread(client); } else if (preConnectedServerConnections.isEmpty()) { - if (HConnection.DEBUG) System.out.println("pre-made server connections ran out of stock"); + logger.error("pre-made server connections ran out of stock"); } else { startProxyThread(client, preConnectedServerConnections.poll(), proxy); @@ -165,6 +166,7 @@ public class LinuxRawIpFlashProxyProvider extends FlashProxyProvider { maybeRemoveMapping(); stateSetter.setState(HState.NOT_CONNECTED); showInvalidConnectionError(); + logger.error("Failed to create socks proxy thread because configuration is null"); return; } @@ -177,7 +179,7 @@ public class LinuxRawIpFlashProxyProvider extends FlashProxyProvider { maybeRemoveMapping(); stateSetter.setState(HState.NOT_CONNECTED); showInvalidConnectionError(); - e.printStackTrace(); + logger.error("Failed to create socks proxy thread", e); } } @@ -188,7 +190,5 @@ public class LinuxRawIpFlashProxyProvider extends FlashProxyProvider { protected void maybeRemoveMapping() { ipMapper.deleteMapping(proxy.getActual_domain(), proxy.getActual_port(), proxy.getIntercept_port()); - } - } diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/NitroConstants.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/NitroConstants.java index 2d111f3..a6b8fd7 100644 --- a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/NitroConstants.java +++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/NitroConstants.java @@ -5,7 +5,6 @@ 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"; diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/NitroProxyProvider.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/NitroProxyProvider.java index 5b90922..b86a4fc 100644 --- a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/NitroProxyProvider.java +++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/NitroProxyProvider.java @@ -9,14 +9,17 @@ 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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; +import java.net.ServerSocket; import java.util.concurrent.atomic.AtomicBoolean; public class NitroProxyProvider implements ProxyProvider, NitroHttpProxyServerCallback, StateChangeListener { + private static final Logger logger = LoggerFactory.getLogger(NitroProxyProvider.class); + private final HProxySetter proxySetter; private final HStateSetter stateSetter; private final HConnection connection; @@ -24,8 +27,9 @@ public class NitroProxyProvider implements ProxyProvider, NitroHttpProxyServerCa private final NitroWebsocketProxy nitroWebsocketProxy; private final AtomicBoolean abortLock; + private int websocketPort; private String originalWebsocketUrl; - private String originalOriginUrl; + private String originalCookies; public NitroProxyProvider(HProxySetter proxySetter, HStateSetter stateSetter, HConnection connection) { this.proxySetter = proxySetter; @@ -40,26 +44,38 @@ public class NitroProxyProvider implements ProxyProvider, NitroHttpProxyServerCa return originalWebsocketUrl; } - public String getOriginalOriginUrl() { - return originalOriginUrl; + public String getOriginalCookies() { + return originalCookies; } @Override public void start() throws IOException { + originalWebsocketUrl = null; + originalCookies = null; + connection.getStateObservable().addListener(this); + logger.info("Starting http proxy"); + if (!nitroHttpProxy.start()) { - System.out.println("Failed to start nitro proxy"); + logger.error("Failed to start nitro proxy"); abort(); return; } + logger.info("Starting websocket proxy"); + if (!nitroWebsocketProxy.start()) { - System.out.println("Failed to start nitro websocket proxy"); + logger.error("Failed to start nitro websocket proxy"); abort(); return; } + websocketPort = nitroWebsocketProxy.getPort(); + + logger.info("Websocket proxy is listening on port {}", websocketPort); + logger.info("Nitro proxy started"); + stateSetter.setState(HState.WAITING_FOR_CLIENT); } @@ -69,19 +85,25 @@ public class NitroProxyProvider implements ProxyProvider, NitroHttpProxyServerCa return; } - if (abortLock.compareAndSet(true, true)) { + if (!abortLock.compareAndSet(false, true)) { return; } + logger.info("Aborting nitro proxy"); + stateSetter.setState(HState.ABORTING); new Thread(() -> { + logger.info("Stopping nitro http proxy"); + try { nitroHttpProxy.stop(); } catch (Exception e) { e.printStackTrace(); } + logger.info("Stopping nitro websocket proxy"); + try { nitroWebsocketProxy.stop(); } catch (Exception e) { @@ -91,15 +113,21 @@ public class NitroProxyProvider implements ProxyProvider, NitroHttpProxyServerCa stateSetter.setState(HState.NOT_CONNECTED); connection.getStateObservable().removeListener(this); + + logger.info("Nitro proxy stopped"); }).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); + return String.format("ws://127.0.0.1:%d", websocketPort); + } + + @Override + public void setOriginCookies(String cookieHeaderValue) { + originalCookies = cookieHeaderValue; } @Override @@ -115,15 +143,4 @@ public class NitroProxyProvider implements ProxyProvider, NitroHttpProxyServerCa abort(); } } - - private static String extractOriginUrl(String url) { - try { - final URI uri = new URI(url); - return String.format("%s://%s/", uri.getScheme(), uri.getHost()); - } catch (URISyntaxException e) { - e.printStackTrace(); - } - - return null; - } } diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxy.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxy.java index f056564..c0098f3 100644 --- a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxy.java +++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxy.java @@ -1,17 +1,15 @@ package gearth.protocol.connection.proxy.nitro.http; -import gearth.GEarth; import gearth.misc.ConfirmationDialog; import gearth.protocol.connection.proxy.nitro.NitroConstants; import gearth.protocol.connection.proxy.nitro.os.NitroOsFunctions; import gearth.protocol.connection.proxy.nitro.os.NitroOsFunctionsFactory; import gearth.ui.titlebar.TitleBarController; +import gearth.ui.translations.LanguageBundle; import javafx.application.Platform; import javafx.scene.control.Alert; import javafx.scene.control.ButtonType; import javafx.scene.control.Label; -import javafx.scene.image.Image; -import javafx.stage.Stage; import org.littleshoot.proxy.HttpProxyServer; import org.littleshoot.proxy.impl.DefaultHttpProxyServer; import org.littleshoot.proxy.mitm.Authority; @@ -53,14 +51,12 @@ public class NitroHttpProxy { Platform.runLater(() -> { Alert alert = ConfirmationDialog.createAlertWithOptOut(Alert.AlertType.WARNING, ADMIN_WARNING_KEY, - "Root certificate installation", null, - "", "Remember my choice", + LanguageBundle.get("alert.rootcertificate.title"), null, + "", LanguageBundle.get("alert.rootcertificate.remember"), ButtonType.YES, ButtonType.NO ); - alert.getDialogPane().setContent(new Label("G-Earth detected that you do not have the root certificate authority installed.\n" + - "This is required for Nitro to work, do you want to continue?\n" + - "G-Earth will ask you for Administrator permission if you do so.")); + alert.getDialogPane().setContent(new Label(LanguageBundle.get("alert.rootcertificate.content").replaceAll("\\\\n", System.lineSeparator()))); try { shouldInstall.set(TitleBarController.create(alert).showAlertAndWait() @@ -109,6 +105,7 @@ public class NitroHttpProxy { .withPort(NitroConstants.HTTP_PORT) .withManInTheMiddle(new NitroCertificateSniffingManager(authority)) .withFiltersSource(new NitroHttpProxyFilterSource(serverCallback)) + .withTransparent(true) .start(); if (!initializeCertificate()) { diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxyFilter.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxyFilter.java index ea0cddc..b9795ec 100644 --- a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxyFilter.java +++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxyFilter.java @@ -7,6 +7,10 @@ import io.netty.util.CharsetUtil; import org.littleshoot.proxy.HttpFiltersAdapter; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -16,6 +20,17 @@ public class NitroHttpProxyFilter extends HttpFiltersAdapter { private static final String NitroClientSearch = "configurationUrls:"; private static final Pattern NitroConfigPattern = Pattern.compile("\"socket\\.url\":.?\"(wss?://.*?)\"", Pattern.MULTILINE); + // https://developers.cloudflare.com/fundamentals/get-started/reference/cloudflare-cookies/ + private static final HashSet CloudflareCookies = new HashSet<>(Arrays.asList( + "__cflb", + "__cf_bm", + "cf_ob_info", + "cf_use_ob", + "__cfwaitingroom", + "__cfruid", + "cf_clearance" + )); + private static final String HeaderAcceptEncoding = "Accept-Encoding"; private static final String HeaderAge = "Age"; private static final String HeaderCacheControl = "Cache-Control"; @@ -27,6 +42,7 @@ public class NitroHttpProxyFilter extends HttpFiltersAdapter { private final NitroHttpProxyServerCallback callback; private final String url; + private String cookies; public NitroHttpProxyFilter(HttpRequest originalRequest, ChannelHandlerContext ctx, NitroHttpProxyServerCallback callback, String url) { super(originalRequest, ctx); @@ -58,6 +74,9 @@ public class NitroHttpProxyFilter extends HttpFiltersAdapter { // Disable caching. stripCacheHeaders(headers); + + // Find relevant cookies for the WebSocket connection. + this.cookies = parseCookies(request); } return super.clientToProxyRequest(httpObject); @@ -77,13 +96,15 @@ public class NitroHttpProxyFilter extends HttpFiltersAdapter { if (matcher.find()) { final String originalWebsocket = matcher.group(1); - final String replacementWebsocket = callback.replaceWebsocketServer(url, originalWebsocket); + final String replacementWebsocket = callback.replaceWebsocketServer(this.url, originalWebsocket); if (replacementWebsocket != null) { responseBody = responseBody.replace(originalWebsocket, replacementWebsocket); responseModified = true; } } + + callback.setOriginCookies(this.cookies); } // Apply changes. @@ -100,6 +121,32 @@ public class NitroHttpProxyFilter extends HttpFiltersAdapter { return httpObject; } + /** + * Check if cookies from the request need to be recorded for the websocket connection to the origin server. + */ + private String parseCookies(final HttpRequest request) { + final List result = new ArrayList<>(); + final List cookieHeaders = request.headers().getAll("Cookie"); + + for (final String cookieHeader : cookieHeaders) { + final String[] cookies = cookieHeader.split(";"); + + for (final String cookie : cookies) { + final String[] parts = cookie.trim().split("="); + + if (CloudflareCookies.contains(parts[0])) { + result.add(cookie.trim()); + } + } + } + + if (result.size() == 0) { + return null; + } + + return String.join("; ", result); + } + /** * Modify Content-Security-Policy header, which could prevent Nitro from connecting with G-Earth. */ @@ -148,5 +195,4 @@ public class NitroHttpProxyFilter extends HttpFiltersAdapter { headers.remove(HeaderIfModifiedSince); headers.remove(HeaderLastModified); } - } diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxyServerCallback.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxyServerCallback.java index 3f04f11..c90b5dc 100644 --- a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxyServerCallback.java +++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/http/NitroHttpProxyServerCallback.java @@ -11,4 +11,9 @@ public interface NitroHttpProxyServerCallback { */ String replaceWebsocketServer(String configUrl, String websocketUrl); + /** + * Sets the parsed cookies for the origin WebSocket connection. + */ + void setOriginCookies(String cookieHeaderValue); + } diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroSession.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroSession.java index f85854e..d7f4665 100644 --- a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroSession.java +++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroSession.java @@ -1,6 +1,6 @@ package gearth.protocol.connection.proxy.nitro.websocket; -import javax.websocket.Session; +import org.eclipse.jetty.websocket.api.Session; public interface NitroSession { diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroWebsocketClient.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroWebsocketClient.java index c940962..7ebfc7d 100644 --- a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroWebsocketClient.java +++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroWebsocketClient.java @@ -6,15 +6,24 @@ import gearth.protocol.connection.*; import gearth.protocol.connection.proxy.nitro.NitroConstants; import gearth.protocol.connection.proxy.nitro.NitroProxyProvider; import gearth.protocol.packethandler.nitro.NitroPacketHandler; +import org.eclipse.jetty.websocket.jsr356.JsrSession; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.websocket.*; import javax.websocket.server.ServerEndpoint; import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; @ServerEndpoint(value = "/") public class NitroWebsocketClient implements NitroSession { + private static final Logger logger = LoggerFactory.getLogger(NitroWebsocketClient.class); + private final HProxySetter proxySetter; private final HStateSetter stateSetter; private final HConnection connection; @@ -23,7 +32,7 @@ public class NitroWebsocketClient implements NitroSession { private final NitroPacketHandler packetHandler; private final AtomicBoolean shutdownLock; - private Session activeSession = null; + private JsrSession activeSession = null; public NitroWebsocketClient(HProxySetter proxySetter, HStateSetter stateSetter, HConnection connection, NitroProxyProvider proxyProvider) { this.proxySetter = proxySetter; @@ -36,11 +45,22 @@ public class NitroWebsocketClient implements NitroSession { } @OnOpen - public void onOpen(Session session) throws IOException { - activeSession = session; - activeSession.setMaxBinaryMessageBufferSize(NitroConstants.WEBSOCKET_BUFFER_SIZE); + public void onOpen(Session session) throws Exception { + logger.info("WebSocket connection accepted"); - server.connect(proxyProvider.getOriginalWebsocketUrl(), proxyProvider.getOriginalOriginUrl()); + activeSession = (JsrSession) session; + activeSession.setMaxBinaryMessageBufferSize(NitroConstants.WEBSOCKET_BUFFER_SIZE); + activeSession.setMaxTextMessageBufferSize(NitroConstants.WEBSOCKET_BUFFER_SIZE); + + // Set proper headers to spoof being a real client. + final Map> headers = new HashMap<>(activeSession.getUpgradeRequest().getHeaders()); + + if (proxyProvider.getOriginalCookies() != null) { + headers.put("Cookie", Collections.singletonList(proxyProvider.getOriginalCookies())); + } + + // Connect to origin server. + server.connect(proxyProvider.getOriginalWebsocketUrl(), headers); final HProxy proxy = new HProxy(HClient.NITRO, "", "", -1, -1, ""); @@ -75,7 +95,7 @@ public class NitroWebsocketClient implements NitroSession { } @Override - public Session getSession() { + public org.eclipse.jetty.websocket.api.Session getSession() { return activeSession; } @@ -89,7 +109,7 @@ public class NitroWebsocketClient implements NitroSession { try { activeSession.close(); - } catch (IOException e) { + } catch (Exception e) { e.printStackTrace(); } finally { activeSession = null; diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroWebsocketProxy.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroWebsocketProxy.java index 11122d1..c60f8c2 100644 --- a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroWebsocketProxy.java +++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroWebsocketProxy.java @@ -3,10 +3,11 @@ 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.Connector; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.handler.HandlerList; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer; @@ -28,7 +29,7 @@ public class NitroWebsocketProxy { this.stateSetter = stateSetter; this.connection = connection; this.proxyProvider = proxyProvider; - this.server = new Server(NitroConstants.WEBSOCKET_PORT); + this.server = new Server(0); } public boolean start() { @@ -56,6 +57,12 @@ public class NitroWebsocketProxy { return false; } + public int getPort() { + final ServerConnector serverConnector = (ServerConnector) server.getConnectors()[0]; + + return serverConnector.getLocalPort(); + } + public void stop() { try { server.stop(); diff --git a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroWebsocketServer.java b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroWebsocketServer.java index 86c5ad0..37ca63d 100644 --- a/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroWebsocketServer.java +++ b/G-Earth/src/main/java/gearth/protocol/connection/proxy/nitro/websocket/NitroWebsocketServer.java @@ -5,15 +5,39 @@ import gearth.protocol.HMessage; import gearth.protocol.connection.proxy.nitro.NitroConstants; import gearth.protocol.packethandler.PacketHandler; import gearth.protocol.packethandler.nitro.NitroPacketHandler; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.WebSocketListener; +import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; +import org.eclipse.jetty.websocket.client.WebSocketClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import javax.websocket.*; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; import java.io.IOException; import java.net.URI; -import java.util.Collections; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.HashSet; import java.util.List; import java.util.Map; -public class NitroWebsocketServer extends Endpoint implements NitroSession { +public class NitroWebsocketServer implements WebSocketListener, NitroSession { + + private static final Logger logger = LoggerFactory.getLogger(NitroWebsocketServer.class); + + private static final HashSet SKIP_HEADERS = new HashSet<>(Arrays.asList( + "Sec-WebSocket-Extensions", + "Sec-WebSocket-Key", + "Sec-WebSocket-Version", + "Host", + "Connection", + "Upgrade" + )); private final PacketHandler packetHandler; private final NitroWebsocketClient client; @@ -24,55 +48,31 @@ public class NitroWebsocketServer extends Endpoint implements NitroSession { this.packetHandler = new NitroPacketHandler(HMessage.Direction.TOCLIENT, client, connection.getExtensionHandler(), connection.getTrafficObservables()); } - public void connect(String websocketUrl, String originUrl) throws IOException { + public void connect(String websocketUrl, Map> clientHeaders) throws IOException { try { - ClientEndpointConfig.Builder builder = ClientEndpointConfig.Builder.create(); + logger.info("Building origin websocket connection ({})", websocketUrl); - if (originUrl != null) { - builder.configurator(new ClientEndpointConfig.Configurator() { - @Override - public void beforeRequest(Map> headers) { - headers.put("Origin", Collections.singletonList(originUrl)); - } - }); - } + final WebSocketClient client = createWebSocketClient(); - ClientEndpointConfig config = builder.build(); + final ClientUpgradeRequest request = new ClientUpgradeRequest(); - 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() { - @Override - public void onMessage(byte[] message) { - try { - packetHandler.act(message); - } catch (IOException e) { - e.printStackTrace(); + clientHeaders.forEach((key, value) -> { + if (SKIP_HEADERS.contains(key)) { + return; } - } - }); - } - @Override - public void onClose(Session session, CloseReason closeReason) { - // Hotel closed connection. - client.shutdownProxy(); - } + request.setHeader(key, value); + }); - @Override - public void onError(Session session, Throwable throwable) { - throwable.printStackTrace(); + logger.info("Connecting to origin websocket at {}", websocketUrl); - // Shutdown. - client.shutdownProxy(); + client.start(); + client.connect(this, URI.create(websocketUrl), request); + + logger.info("Connected to origin websocket"); + } catch (Exception e) { + throw new IOException("Failed to start websocket client to origin " + websocketUrl, e); + } } @Override @@ -94,10 +94,86 @@ public class NitroWebsocketServer extends Endpoint implements NitroSession { try { activeSession.close(); - } catch (IOException e) { + } catch (Exception e) { e.printStackTrace(); } finally { activeSession = null; } } + + @Override + public void onWebSocketBinary(byte[] bytes, int i, int i1) { + try { + packetHandler.act(bytes); + } catch (IOException e) { + logger.error("Failed to handle packet", e); + } + } + + @Override + public void onWebSocketText(String s) { + logger.warn("Received text message from hotel"); + } + + @Override + public void onWebSocketClose(int i, String s) { + // Hotel closed connection. + client.shutdownProxy(); + } + + @Override + public void onWebSocketConnect(org.eclipse.jetty.websocket.api.Session session) { + activeSession = session; + } + + @Override + public void onWebSocketError(Throwable throwable) { + throwable.printStackTrace(); + + // Shutdown. + client.shutdownProxy(); + } + + private SSLContext createSSLContext() { + final TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() { + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + @Override + public void checkClientTrusted(X509Certificate[] certs, String authType) { + } + + @Override + public void checkServerTrusted(X509Certificate[] certs, String authType) { + } + }}; + + try { + final SSLContext sslContext = SSLContext.getInstance("TLS"); + + sslContext.init(null, trustAllCerts, new SecureRandom()); + + return sslContext; + } catch (Exception e) { + throw new RuntimeException("Failed to setup ssl context", e); + } + } + + private HttpClient createHttpClient() { + final SslContextFactory.Client factory = new SslContextFactory.Client(); + + factory.setSslContext(createSSLContext()); + + return new HttpClient(factory); + } + + private WebSocketClient createWebSocketClient() { + final WebSocketClient client = new WebSocketClient(createHttpClient()); + + client.setMaxBinaryMessageBufferSize(NitroConstants.WEBSOCKET_BUFFER_SIZE); + client.setMaxTextMessageBufferSize(NitroConstants.WEBSOCKET_BUFFER_SIZE); + + return client; + } } diff --git a/G-Earth/src/main/java/gearth/protocol/hostreplacer/hostsfile/UnixHostReplacer.java b/G-Earth/src/main/java/gearth/protocol/hostreplacer/hostsfile/UnixHostReplacer.java index cc9ea5e..4754cbf 100644 --- a/G-Earth/src/main/java/gearth/protocol/hostreplacer/hostsfile/UnixHostReplacer.java +++ b/G-Earth/src/main/java/gearth/protocol/hostreplacer/hostsfile/UnixHostReplacer.java @@ -1,14 +1,21 @@ package gearth.protocol.hostreplacer.hostsfile; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.*; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.stream.Collectors; /** * Created by Jonas on 04/04/18. */ class UnixHostReplacer implements HostReplacer { + private static final Logger LOGGER = LoggerFactory.getLogger(UnixHostReplacer.class); + protected String hostsFileLocation; UnixHostReplacer() { @@ -17,23 +24,24 @@ class UnixHostReplacer implements HostReplacer { @Override public void addRedirect(String[] lines) { - List adders = new ArrayList<>(); - for (int i = 0; i < lines.length; i++) { - adders.add(lines[i] + "\t# G-Earth replacement"); - } - FileReader fr = null; - BufferedReader br = null; - FileWriter fw = null; - BufferedWriter out = null; + final List adders = appendCommentToEachLine(lines); + FileReader fr; + BufferedReader br; + FileWriter fw; + BufferedWriter out; try { - ArrayList fileLines = new ArrayList<>(); - File f1 = new File(hostsFileLocation); - fr = new FileReader(f1); + final ArrayList fileLines = new ArrayList<>(); + final File hostsFile = new File(hostsFileLocation); + + LOGGER.debug("Replacing hosts at {}", hostsFile.getAbsolutePath()); + + fr = new FileReader(hostsFile); br = new BufferedReader(fr); - String line = null; + + String line; while ((line = br.readLine()) != null) { adders.remove(line); @@ -42,7 +50,7 @@ class UnixHostReplacer implements HostReplacer { fr.close(); br.close(); - fw = new FileWriter(f1); + fw = new FileWriter(hostsFile); out = new BufferedWriter(fw); for (String li : adders) { @@ -58,29 +66,26 @@ class UnixHostReplacer implements HostReplacer { } catch (Exception ex) { - ex.printStackTrace(); + LOGGER.error("Failed to add host redirects", ex); } } @Override public void removeRedirect(String[] lines) { - ArrayList removers = new ArrayList<>(); - for (int i = 0; i < lines.length; i++) { - removers.add(lines[i] + "\t# G-Earth replacement"); - } + final List removers = appendCommentToEachLine(lines); - FileReader fr = null; - BufferedReader br = null; - FileWriter fw = null; - BufferedWriter out = null; + FileReader fr; + BufferedReader br; + FileWriter fw; + BufferedWriter out; try { - ArrayList fileLines = new ArrayList(); - File f1 = new File(hostsFileLocation); - fr = new FileReader(f1); + final ArrayList fileLines = new ArrayList<>(); + final File hostsFile = new File(hostsFileLocation); + fr = new FileReader(hostsFile); br = new BufferedReader(fr); - String line = null; + String line; while ((line = br.readLine()) != null) { if (!removers.contains(line)) @@ -89,12 +94,13 @@ class UnixHostReplacer implements HostReplacer { fr.close(); br.close(); - fw = new FileWriter(f1); + fw = new FileWriter(hostsFile); out = new BufferedWriter(fw); for (int i = 0; i < fileLines.size(); i++) { out.write(fileLines.get(i)); - if (i != fileLines.size() - 1) out.write(System.getProperty("line.separator")); + if (i != fileLines.size() - 1) + out.write(System.getProperty("line.separator")); } out.flush(); fw.close(); @@ -102,7 +108,14 @@ class UnixHostReplacer implements HostReplacer { } catch (Exception ex) { - ex.printStackTrace(); + LOGGER.error("Failed to remove host replace lines", ex); } } + + private static List appendCommentToEachLine(String[] lines) { + return Arrays + .stream(lines) + .map(line -> line + "\t# G-Earth replacement") + .collect(Collectors.toList()); + } } diff --git a/G-Earth/src/main/java/gearth/protocol/memory/Rc4Obtainer.java b/G-Earth/src/main/java/gearth/protocol/memory/Rc4Obtainer.java index 50d37ad..debd252 100644 --- a/G-Earth/src/main/java/gearth/protocol/memory/Rc4Obtainer.java +++ b/G-Earth/src/main/java/gearth/protocol/memory/Rc4Obtainer.java @@ -10,6 +10,7 @@ import gearth.protocol.packethandler.PayloadBuffer; import gearth.protocol.packethandler.flash.BufferChangeListener; import gearth.protocol.packethandler.flash.FlashPacketHandler; import gearth.ui.titlebar.TitleBarController; +import gearth.ui.translations.LanguageBundle; import javafx.application.Platform; import javafx.scene.control.Alert; import javafx.scene.control.ButtonType; @@ -17,6 +18,8 @@ import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; import javafx.scene.layout.FlowPane; import javafx.scene.layout.Region; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.Arrays; @@ -24,7 +27,9 @@ import java.util.List; public class Rc4Obtainer { - public static final boolean DEBUG = false; + private static final Logger LOGGER = LoggerFactory.getLogger(Rc4Obtainer.class); + + public static final boolean DEBUG = true; private final HabboClient client; private List flashPacketHandlers; @@ -57,8 +62,7 @@ public class Rc4Obtainer { new Thread(() -> { long startTime = System.currentTimeMillis(); - if (DEBUG) System.out.println("[+] send encrypted"); - + LOGGER.debug("[+] send encrypted"); boolean worked = false; int i = 0; while (!worked && i < 4) { @@ -69,13 +73,12 @@ public class Rc4Obtainer { } if (!worked) { - System.err.println("COULD NOT FIND RC4 TABLE"); - + LOGGER.error("COULD NOT FIND RC4 TABLE"); Platform.runLater(() -> { - Alert alert = new Alert(Alert.AlertType.WARNING, "Something went wrong!", ButtonType.OK); + Alert alert = new Alert(Alert.AlertType.WARNING, LanguageBundle.get("alert.somethingwentwrong.title"), ButtonType.OK); FlowPane fp = new FlowPane(); - Label lbl = new Label("G-Earth has experienced an issue" + System.lineSeparator() + System.lineSeparator() + "Head over to our Troubleshooting page to solve the problem:"); + Label lbl = new Label(LanguageBundle.get("alert.somethingwentwrong.content").replaceAll("\\\\n", System.lineSeparator())); Hyperlink link = new Hyperlink("https://github.com/sirjonasxx/G-Earth/wiki/Troubleshooting"); fp.getChildren().addAll(lbl, link); link.setOnAction(event -> { @@ -89,14 +92,13 @@ public class Rc4Obtainer { try { TitleBarController.create(alert).showAlert(); } catch (IOException e) { - e.printStackTrace(); + LOGGER.error("Failed to create error alert", e); } }); } final long endTime = System.currentTimeMillis(); - if (DEBUG) - System.out.println("Cracked RC4 in " + (endTime - startTime) + "ms"); + LOGGER.debug("Cracked RC4 in " + (endTime - startTime) + "ms"); flashPacketHandlers.forEach(FlashPacketHandler::unblock); }).start(); diff --git a/G-Earth/src/main/java/gearth/protocol/memory/habboclient/HabboClientFactory.java b/G-Earth/src/main/java/gearth/protocol/memory/habboclient/HabboClientFactory.java index 48cba93..512c21a 100644 --- a/G-Earth/src/main/java/gearth/protocol/memory/habboclient/HabboClientFactory.java +++ b/G-Earth/src/main/java/gearth/protocol/memory/habboclient/HabboClientFactory.java @@ -3,7 +3,7 @@ package gearth.protocol.memory.habboclient; import gearth.misc.OSValidator; import gearth.protocol.HConnection; import gearth.protocol.memory.habboclient.linux.LinuxHabboClient; -import gearth.protocol.memory.habboclient.macOs.MacOsHabboClient; +import gearth.protocol.memory.habboclient.macos.MacOSHabboClient; import gearth.protocol.memory.habboclient.windows.WindowsHabboClient; /** @@ -15,7 +15,7 @@ public class HabboClientFactory { public static HabboClient get(HConnection connection) { if (OSValidator.isUnix()) return new LinuxHabboClient(connection); if (OSValidator.isWindows()) return new WindowsHabboClient(connection); - if (OSValidator.isMac()) return new MacOsHabboClient(connection); + if (OSValidator.isMac()) return new MacOSHabboClient(connection); // todo use rust if beneficial diff --git a/G-Earth/src/main/java/gearth/protocol/memory/habboclient/macOs/MacOsHabboClient.java b/G-Earth/src/main/java/gearth/protocol/memory/habboclient/macOs/MacOsHabboClient.java deleted file mode 100644 index 5cdcd0f..0000000 --- a/G-Earth/src/main/java/gearth/protocol/memory/habboclient/macOs/MacOsHabboClient.java +++ /dev/null @@ -1,144 +0,0 @@ -package gearth.protocol.memory.habboclient.macOs; - -import gearth.misc.Cacher; -import gearth.protocol.HConnection; -import gearth.protocol.HMessage; -import gearth.protocol.memory.habboclient.HabboClient; -import org.json.JSONArray; -import org.json.JSONObject; - - -import java.io.BufferedReader; -import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.List; -import java.util.StringJoiner; - -public class MacOsHabboClient extends HabboClient { - - public MacOsHabboClient(HConnection connection) { - super(connection); - - connection.addTrafficListener(0, message -> { - if (message.getDestination() == HMessage.Direction.TOSERVER && message.getPacket().headerId() == PRODUCTION_ID) { - production = message.getPacket().readString(); - } - }); - } - - - private static final String OFFSETS_CACHE_KEY = "RC4Offsets"; - private static final int PRODUCTION_ID = 4000; - private String production = ""; - - @Override - public List getRC4cached() { - List result = new ArrayList<>(); - try { - List possibleResults = readPossibleBytes(true); - - if (possibleResults == null) - return new ArrayList<>(); - - for (String s : possibleResults) - result.add(hexStringToByteArray(s)); - } catch (IOException | URISyntaxException e) { - e.printStackTrace(); - } - return result; - } - - private ArrayList readPossibleBytes(boolean useCache) throws IOException, URISyntaxException { - ProcessBuilder pb; - - JSONObject revisionList = (JSONObject) Cacher.get(OFFSETS_CACHE_KEY); - if (revisionList == null) { - Cacher.put(OFFSETS_CACHE_KEY, new JSONObject()); - revisionList = (JSONObject) Cacher.get(OFFSETS_CACHE_KEY); - } - - assert revisionList != null; - JSONArray cachedOffsets; - if (revisionList.has(production)) - cachedOffsets = (JSONArray) revisionList.get(production); - else - cachedOffsets = null; - - StringJoiner joiner = new StringJoiner(" "); - - if (useCache) { - if (cachedOffsets == null) { - return null; - } - - for (Object s : cachedOffsets) { - joiner.add((String)s); - } - } - - String g_mem = new File(this.getClass().getProtectionDomain().getCodeSource().getLocation().toURI()).getParent() + "/G-Mem"; - if (!useCache) - pb = new ProcessBuilder(g_mem, hConnection.getClientHost() , Integer.toString(hConnection.getClientPort())); - else - pb = new ProcessBuilder(g_mem, hConnection.getClientHost() , Integer.toString(hConnection.getClientPort()), "-c" + joiner.toString()); - - - Process p = pb.start(); - BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream())); - - String line; - ArrayList possibleData = new ArrayList<>(); - - if (cachedOffsets == null) { - cachedOffsets = new JSONArray(); - } - - - int count = 0; - while((line = reader.readLine()) != null) { - if (line.length() > 1) { - if (!useCache && (count++ % 2 == 0)) { - if (!cachedOffsets.toList().contains(line)) { - cachedOffsets.put(line); - System.out.println("[+] " + line); - } - } - else - possibleData.add(line); - } - } - - revisionList.put(production, cachedOffsets); - Cacher.put(OFFSETS_CACHE_KEY, revisionList); - p.destroy(); - return possibleData; - } - - @Override - public List getRC4possibilities() { - List result = new ArrayList<>(); - try { - ArrayList possibleData = readPossibleBytes(false); - - for (String possibleHexStr : possibleData) { - result.add(hexStringToByteArray(possibleHexStr)); - } - } catch (IOException | URISyntaxException e) { - e.printStackTrace(); - } - return result; - } - - private static byte[] hexStringToByteArray(String s) { - int len = s.length(); - byte[] data = new byte[len / 2]; - for (int i = 0; i < len; i += 2) { - data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) - + Character.digit(s.charAt(i+1), 16)); - } - return data; - } -} diff --git a/G-Earth/src/main/java/gearth/protocol/memory/habboclient/macos/MacOSHabboClient.java b/G-Earth/src/main/java/gearth/protocol/memory/habboclient/macos/MacOSHabboClient.java new file mode 100644 index 0000000..95420e9 --- /dev/null +++ b/G-Earth/src/main/java/gearth/protocol/memory/habboclient/macos/MacOSHabboClient.java @@ -0,0 +1,104 @@ +package gearth.protocol.memory.habboclient.macos; + +import gearth.misc.StringUtils; +import gearth.protocol.HConnection; +import gearth.protocol.HMessage; +import gearth.protocol.HPacket; +import gearth.protocol.memory.habboclient.HabboClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a {@link HabboClient} implementation for the MacOS operating system. + * + * @author sirjonasxx / dorving (revised) + */ +public class MacOSHabboClient extends HabboClient { + + private static final Logger LOGGER = LoggerFactory.getLogger(MacOSHabboClient.class); + + private static final String G_MEM_EXECUTABLE_FILE_NAME = "/g_mem_mac"; + + /** + * The header id (opcode) of the packet that contains the value for the {@link #production} field. + */ + private static final int PRODUCTION_ID = 4000; + + private String production = ""; + + /** + * Create a new {@link MacOSHabboClient} instance. + * + * @param connection the {@link HConnection connection} with the Habbo server. + */ + public MacOSHabboClient(HConnection connection) { + super(connection); + listenForProductionPacket(connection); + } + + private void listenForProductionPacket(HConnection connection) { + connection.addTrafficListener(0, message -> { + if (message.getDestination() == HMessage.Direction.TOSERVER) { + final HPacket packet = message.getPacket(); + if (packet.headerId() == PRODUCTION_ID) { + production = packet.readString(); + LOGGER.debug("Read production packet from connection {}, set `production` to {}", connection, production); + } + } + }); + } + + @Override + public List getRC4cached() { + return new ArrayList<>(); + } + + + @Override + public List getRC4possibilities() { + final List result = new ArrayList<>(); + try { + for (String possibleHexStr : readPossibleBytes()) + result.add(StringUtils.hexStringToByteArray(possibleHexStr)); + } catch (IOException | URISyntaxException e) { + LOGGER.error("Failed to parse line as hex string", e); + } + return result; + } + + private ArrayList readPossibleBytes() throws IOException, URISyntaxException { + final String pathToGMemExecutable = getPathToGMemExecutable(); + final String clientHost = hConnection.getClientHost(); + final String clientPort = Integer.toString(hConnection.getClientPort()); + LOGGER.debug("Attempting to execute G-Mem executable {} with host {} at port {}", pathToGMemExecutable, clientHost, clientPort); + final Process process = new ProcessBuilder(pathToGMemExecutable, clientHost, clientPort) + .start(); + final ArrayList possibleData = new ArrayList<>(); + try(BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + int count = 0; + String line; + while((line = reader.readLine()) != null) { + if (line.length() > 1 && (count++ % 2 != 0)) + possibleData.add(line); + } + } catch (Exception e) { + LOGGER.error("Failed to execute G-Mem", e); + } finally { + process.destroy(); + } + LOGGER.debug("Read {} from G-Mem output stream", possibleData); + return possibleData; + } + + private String getPathToGMemExecutable() throws URISyntaxException { + return new File(getClass().getProtectionDomain().getCodeSource().getLocation().toURI()).getParent() + G_MEM_EXECUTABLE_FILE_NAME; + } +} diff --git a/G-Earth/src/main/java/gearth/protocol/memory/habboclient/rust/RustHabboClient.java b/G-Earth/src/main/java/gearth/protocol/memory/habboclient/rust/RustHabboClient.java index b5c88a8..79ba40b 100644 --- a/G-Earth/src/main/java/gearth/protocol/memory/habboclient/rust/RustHabboClient.java +++ b/G-Earth/src/main/java/gearth/protocol/memory/habboclient/rust/RustHabboClient.java @@ -1,5 +1,6 @@ package gearth.protocol.memory.habboclient.rust; +import gearth.misc.StringUtils; import gearth.protocol.HConnection; import gearth.protocol.memory.habboclient.HabboClient; @@ -48,18 +49,9 @@ public class RustHabboClient extends HabboClient { List ret = new ArrayList<>(); for (String possibleHexStr : possibleData) - ret.add(hexStringToByteArray(possibleHexStr)); + ret.add(StringUtils.hexStringToByteArray(possibleHexStr)); return ret; } - private static byte[] hexStringToByteArray(String s) { - int len = s.length(); - byte[] data = new byte[len / 2]; - for (int i = 0; i < len; i += 2) { - data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) - + Character.digit(s.charAt(i+1), 16)); - } - return data; - } } diff --git a/G-Earth/src/main/java/gearth/protocol/memory/habboclient/windows/WindowsHabboClient.java b/G-Earth/src/main/java/gearth/protocol/memory/habboclient/windows/WindowsHabboClient.java index 7178b06..32bb362 100644 --- a/G-Earth/src/main/java/gearth/protocol/memory/habboclient/windows/WindowsHabboClient.java +++ b/G-Earth/src/main/java/gearth/protocol/memory/habboclient/windows/WindowsHabboClient.java @@ -1,6 +1,7 @@ package gearth.protocol.memory.habboclient.windows; import gearth.misc.Cacher; +import gearth.misc.StringUtils; import gearth.protocol.HConnection; import gearth.protocol.HMessage; import gearth.protocol.memory.habboclient.HabboClient; @@ -44,7 +45,7 @@ public class WindowsHabboClient extends HabboClient { return new ArrayList<>(); for (String s : possibleResults) - result.add(hexStringToByteArray(s)); + result.add(StringUtils.hexStringToByteArray(s)); } catch (IOException | URISyntaxException e) { e.printStackTrace(); } @@ -124,23 +125,12 @@ public class WindowsHabboClient extends HabboClient { List result = new ArrayList<>(); try { ArrayList possibleData = readPossibleBytes(false); - for (String possibleHexStr : possibleData) { - result.add(hexStringToByteArray(possibleHexStr)); + result.add(StringUtils.hexStringToByteArray(possibleHexStr)); } } catch (IOException | URISyntaxException e) { e.printStackTrace(); } return result; } - - private static byte[] hexStringToByteArray(String s) { - int len = s.length(); - byte[] data = new byte[len / 2]; - for (int i = 0; i < len; i += 2) { - data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) - + Character.digit(s.charAt(i+1), 16)); - } - return data; - } } diff --git a/G-Earth/src/main/java/gearth/protocol/packethandler/flash/FlashPacketHandler.java b/G-Earth/src/main/java/gearth/protocol/packethandler/flash/FlashPacketHandler.java index 98b7ffe..e4d2743 100644 --- a/G-Earth/src/main/java/gearth/protocol/packethandler/flash/FlashPacketHandler.java +++ b/G-Earth/src/main/java/gearth/protocol/packethandler/flash/FlashPacketHandler.java @@ -16,7 +16,7 @@ import java.util.List; public abstract class FlashPacketHandler extends PacketHandler { - protected static final boolean DEBUG = false; + protected static final boolean DEBUG = true; private volatile OutputStream out; private volatile boolean isTempBlocked = false; diff --git a/G-Earth/src/main/java/gearth/protocol/packethandler/nitro/NitroPacketHandler.java b/G-Earth/src/main/java/gearth/protocol/packethandler/nitro/NitroPacketHandler.java index d8b69b0..fc72f34 100644 --- a/G-Earth/src/main/java/gearth/protocol/packethandler/nitro/NitroPacketHandler.java +++ b/G-Earth/src/main/java/gearth/protocol/packethandler/nitro/NitroPacketHandler.java @@ -6,13 +6,17 @@ import gearth.protocol.connection.proxy.nitro.websocket.NitroSession; import gearth.protocol.packethandler.PacketHandler; import gearth.protocol.packethandler.PayloadBuffer; import gearth.services.extension_handler.ExtensionHandler; +import org.eclipse.jetty.websocket.api.Session; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import javax.websocket.Session; import java.io.IOException; import java.nio.ByteBuffer; public class NitroPacketHandler extends PacketHandler { + private static final Logger logger = LoggerFactory.getLogger(NitroPacketHandler.class); + private final HMessage.Direction direction; private final NitroSession session; private final PayloadBuffer payloadBuffer; @@ -39,7 +43,13 @@ public class NitroPacketHandler extends PacketHandler { buffer = buffer.clone(); } - localSession.getAsyncRemote().sendBinary(ByteBuffer.wrap(buffer)); + try { + localSession.getRemote().sendBytes(ByteBuffer.wrap(buffer)); + } catch (IOException e) { + logger.error("Error sending packet to nitro client", e); + return false; + } + return true; } diff --git a/G-Earth/src/main/java/gearth/protocol/portchecker/WindowsPortChecker.java b/G-Earth/src/main/java/gearth/protocol/portchecker/WindowsPortChecker.java index 771d946..218796c 100644 --- a/G-Earth/src/main/java/gearth/protocol/portchecker/WindowsPortChecker.java +++ b/G-Earth/src/main/java/gearth/protocol/portchecker/WindowsPortChecker.java @@ -18,7 +18,7 @@ public class WindowsPortChecker implements PortChecker{ } private String getProcessNameFromPid(String pid) throws IOException { - String task = getCommandOutput(new String[] {"tasklist /fi \"pid eq " + pid + "\" /nh /fo:CSV"}); + String task = getCommandOutput(new String[] {"cmd", "/c", "tasklist", "/fi", String.format("pid eq %s", pid) ,"/nh", "/fo:CSV"}); int index = task.indexOf(','); return task.substring(0, index); } diff --git a/G-Earth/src/main/java/gearth/services/extension_handler/ExtensionHandler.java b/G-Earth/src/main/java/gearth/services/extension_handler/ExtensionHandler.java index 61e01b7..87a57d8 100644 --- a/G-Earth/src/main/java/gearth/services/extension_handler/ExtensionHandler.java +++ b/G-Earth/src/main/java/gearth/services/extension_handler/ExtensionHandler.java @@ -200,11 +200,18 @@ public class ExtensionHandler { @Override public void sendMessage(HMessage.Direction direction, HPacket packet) { + boolean success; if (direction == HMessage.Direction.TOCLIENT) { - hConnection.sendToClient(packet); + success = hConnection.sendToClient(packet); } else { - hConnection.sendToServer(packet); + success = hConnection.sendToServer(packet); + } + + if (!success && hConnection.isPacketSendingAllowed(direction, packet) && !hConnection.isPacketSendingSafe(direction, packet)) { + extension.getExtensionObservable().fireEvent(extensionListener -> + extensionListener.log(String.format("Extension %s attempted to send an unsafe packet, but had no permission", + extension.getTitle()))); } } diff --git a/G-Earth/src/main/java/gearth/services/extension_handler/extensions/ExtensionListener.java b/G-Earth/src/main/java/gearth/services/extension_handler/extensions/ExtensionListener.java index 5e90397..59d5942 100644 --- a/G-Earth/src/main/java/gearth/services/extension_handler/extensions/ExtensionListener.java +++ b/G-Earth/src/main/java/gearth/services/extension_handler/extensions/ExtensionListener.java @@ -9,7 +9,7 @@ public abstract class ExtensionListener { protected void manipulatedPacket(HMessage hMessage) {} protected void flagsRequest() {} protected void sendMessage(HMessage.Direction direction, HPacket packet) {} - protected void log(String text) {} + public void log(String text) {} protected void hasClosed() {} protected void packetToStringRequest(HPacket packet) {} diff --git a/G-Earth/src/main/java/gearth/services/extension_handler/extensions/extensionproducers/ExtensionProducerFactory.java b/G-Earth/src/main/java/gearth/services/extension_handler/extensions/extensionproducers/ExtensionProducerFactory.java index bfb8dc2..9600a9c 100644 --- a/G-Earth/src/main/java/gearth/services/extension_handler/extensions/extensionproducers/ExtensionProducerFactory.java +++ b/G-Earth/src/main/java/gearth/services/extension_handler/extensions/extensionproducers/ExtensionProducerFactory.java @@ -1,6 +1,6 @@ package gearth.services.extension_handler.extensions.extensionproducers; -import gearth.services.extension_handler.extensions.implementations.network.NetworkExtensionsProducer; +import gearth.services.extension_handler.extensions.implementations.network.NetworkExtensionServer; import gearth.services.extension_handler.extensions.implementations.simple.SimpleExtensionProducer; import java.util.ArrayList; @@ -9,13 +9,17 @@ import java.util.List; public class ExtensionProducerFactory { // returns one of every ExtensionProducer class we have created, to support all types of extensions + private final static NetworkExtensionServer EXTENSION_SERVER = new NetworkExtensionServer(); + public static List getAll() { List all = new ArrayList<>(); - all.add(new NetworkExtensionsProducer()); + all.add(EXTENSION_SERVER); all.add(new SimpleExtensionProducer()); return all; } - + public static NetworkExtensionServer getExtensionServer() { + return EXTENSION_SERVER; + } } diff --git a/G-Earth/src/main/java/gearth/services/extension_handler/extensions/implementations/network/NetworkExtension.java b/G-Earth/src/main/java/gearth/services/extension_handler/extensions/implementations/network/NetworkExtension.java deleted file mode 100644 index 48d89dc..0000000 --- a/G-Earth/src/main/java/gearth/services/extension_handler/extensions/implementations/network/NetworkExtension.java +++ /dev/null @@ -1,260 +0,0 @@ -package gearth.services.extension_handler.extensions.implementations.network; - -import gearth.misc.HostInfo; -import gearth.services.packet_info.PacketInfoManager; -import gearth.protocol.HMessage; -import gearth.protocol.connection.HClient; -import gearth.services.extension_handler.extensions.ExtensionType; -import gearth.services.extension_handler.extensions.GEarthExtension; -import gearth.protocol.HPacket; - -import java.io.DataInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.Socket; -import java.nio.charset.StandardCharsets; - -/** - * Created by Jonas on 21/06/18. - */ -public class NetworkExtension extends GEarthExtension { - - private String title; - private String author; - private String version; - private String description; - - private boolean fireEventButtonVisible; - private boolean leaveButtonVisible; - private boolean deleteButtonVisible; - - private boolean isInstalledExtension; // <- extension is in the extensions directory - private String fileName; - private String cookie; - - private Socket connection; - - NetworkExtension(HPacket extensionInfo, Socket connection) { - this.title = extensionInfo.readString(); - this.author = extensionInfo.readString(); - this.version = extensionInfo.readString(); - this.description = extensionInfo.readString(); - this.fireEventButtonVisible = extensionInfo.readBoolean(); - - this.isInstalledExtension = extensionInfo.readBoolean(); - this.fileName = extensionInfo.readString(); - this.cookie = extensionInfo.readString(); - - this.leaveButtonVisible = extensionInfo.readBoolean(); - this.deleteButtonVisible = extensionInfo.readBoolean(); - - this.connection = connection; - - NetworkExtension selff = this; - new Thread(() -> { - try { - InputStream inputStream = connection.getInputStream(); - DataInputStream dIn = new DataInputStream(inputStream); - - while (!connection.isClosed()) { - int length = dIn.readInt(); - byte[] headerandbody = new byte[length + 4]; - - int amountRead = 0; - while (amountRead < length) { - amountRead += dIn.read(headerandbody, 4 + amountRead, Math.min(dIn.available(), length - amountRead)); - } - - HPacket message = new HPacket(headerandbody); - message.fixLength(); - - synchronized (selff.extensionObservable) { - if (message.headerId() == NetworkExtensionInfo.INCOMING_MESSAGES_IDS.REQUESTFLAGS) { - requestFlags(); - } - else if (message.headerId() == NetworkExtensionInfo.INCOMING_MESSAGES_IDS.SENDMESSAGE) { - byte side = message.readByte(); - int byteLength = message.readInteger(); - byte[] packetAsByteArray = message.readBytes(byteLength); - HPacket packet = new HPacket(packetAsByteArray); - if (!packet.isCorrupted()) { - log("Forwarding incoming packet (packet="+packet+")"); - sendMessage( - side == 0 ? HMessage.Direction.TOCLIENT : HMessage.Direction.TOSERVER, - packet - ); - } else { - log("Received corrupted packet (packet="+packet+")"); - } - } - else if (message.headerId() == NetworkExtensionInfo.INCOMING_MESSAGES_IDS.MANIPULATEDPACKET) { - String stringifiedresponse = message.readLongString(6); - HMessage responseMessage = new HMessage(stringifiedresponse); - sendManipulatedPacket(responseMessage); - } - else if (message.headerId() == NetworkExtensionInfo.INCOMING_MESSAGES_IDS.EXTENSIONCONSOLELOG) { - log(message.readString()); - } - else if (message.headerId() == NetworkExtensionInfo.INCOMING_MESSAGES_IDS.PACKETTOSTRING_REQUEST) { - HPacket p = new HPacket(new byte[0]); - p.constructFromString(message.readLongString()); - packetToStringRequest(p); - } - else if (message.headerId() == NetworkExtensionInfo.INCOMING_MESSAGES_IDS.STRINGTOPACKET_REQUEST) { - stringToPacketRequest(message.readLongString(StandardCharsets.UTF_8)); - } - - } - - } - - } catch (IOException e) { - // An extension disconnected, which is OK - } finally { - synchronized (selff.extensionObservable) { - hasClosed(); - } - if (!connection.isClosed()) { - try { - connection.close(); - } catch (IOException e) { -// e.printStackTrace(); - } - } - } - }).start(); - - - } - - - public String getAuthor() { - return author; - } - public String getDescription() { - return description; - } - public String getTitle() { - return title; - } - public String getVersion() { - return version; - } - public boolean isFireButtonUsed() { - return fireEventButtonVisible; - } - public String getFileName() { - return fileName; - } - public String getCookie() { - return cookie; - } - public boolean isDeleteButtonVisible() { - return deleteButtonVisible; - } - public boolean isLeaveButtonVisible() { - return leaveButtonVisible; - } - - public boolean isInstalledExtension() { - return isInstalledExtension; - } - - - private boolean sendMessage(HPacket message) { - try { - synchronized (this) { - connection.getOutputStream().write(message.toBytes()); - } - return true; - } catch (IOException e) { - return false; - } - } - - @Override - public void doubleclick() { - sendMessage(new HPacket(NetworkExtensionInfo.OUTGOING_MESSAGES_IDS.ONDOUBLECLICK)); - } - - @Override - public void packetIntercept(HMessage hMessage) { - String stringified = hMessage.stringify(); - HPacket manipulatePacketRequest = new HPacket(NetworkExtensionInfo.OUTGOING_MESSAGES_IDS.PACKETINTERCEPT); - manipulatePacketRequest.appendLongString(stringified); - sendMessage(manipulatePacketRequest); - } - - @Override - public void provideFlags(String[] flags) { - HPacket packet = new HPacket(NetworkExtensionInfo.OUTGOING_MESSAGES_IDS.FLAGSCHECK); - packet.appendInt(flags.length); - for (String flag : flags) { - packet.appendString(flag); - } - sendMessage(packet); - } - - @Override - public void connectionStart(String host, int port, String hotelVersion, String clientIdentifier, HClient clientType, PacketInfoManager packetInfoManager) { - HPacket connectionStartPacket = new HPacket(NetworkExtensionInfo.OUTGOING_MESSAGES_IDS.CONNECTIONSTART) - .appendString(host) - .appendInt(port) - .appendString(hotelVersion) - .appendString(clientIdentifier) - .appendString(clientType.name()); - - packetInfoManager.appendToPacket(connectionStartPacket); - sendMessage(connectionStartPacket); - } - - @Override - public void connectionEnd() { - sendMessage( - new HPacket(NetworkExtensionInfo.OUTGOING_MESSAGES_IDS.CONNECTIONEND) - ); - } - - @Override - public void init(boolean isConnected, HostInfo hostInfo) { - HPacket initPacket = new HPacket(NetworkExtensionInfo.OUTGOING_MESSAGES_IDS.INIT); - initPacket.appendBoolean(isConnected); - hostInfo.appendToPacket(initPacket); - - sendMessage(initPacket); - } - - @Override - public void close() { - try { - connection.close(); - } catch (IOException ignored) { } - } - - @Override - public void updateHostInfo(HostInfo hostInfo) { - HPacket packet = new HPacket(NetworkExtensionInfo.OUTGOING_MESSAGES_IDS.UPDATEHOSTINFO); - hostInfo.appendToPacket(packet); - sendMessage(packet); - } - - @Override - public void packetToStringResponse(String string, String expression) { - HPacket packet = new HPacket(NetworkExtensionInfo.OUTGOING_MESSAGES_IDS.PACKETTOSTRING_RESPONSE); - packet.appendLongString(string); - packet.appendLongString(expression, StandardCharsets.UTF_8); - sendMessage(packet); - } - - @Override - public void stringToPacketResponse(HPacket packetFromString) { - HPacket packet = new HPacket(NetworkExtensionInfo.OUTGOING_MESSAGES_IDS.STRINGTOPACKET_RESPONSE); - packet.appendLongString(packetFromString.stringify()); - sendMessage(packet); - } - - @Override - public ExtensionType extensionType() { - return ExtensionType.EXTERNAL; - } -} diff --git a/G-Earth/src/main/java/gearth/services/extension_handler/extensions/implementations/network/NetworkExtensionAuthenticator.java b/G-Earth/src/main/java/gearth/services/extension_handler/extensions/implementations/network/NetworkExtensionAuthenticator.java new file mode 100644 index 0000000..b234537 --- /dev/null +++ b/G-Earth/src/main/java/gearth/services/extension_handler/extensions/implementations/network/NetworkExtensionAuthenticator.java @@ -0,0 +1,119 @@ +package gearth.services.extension_handler.extensions.implementations.network; + +import gearth.misc.ConfirmationDialog; +import gearth.ui.titlebar.TitleBarController; +import gearth.ui.translations.LanguageBundle; +import javafx.application.Platform; +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonType; +import javafx.scene.control.Label; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.CountDownLatch; + +/** + * Created by Jonas on 16/10/18. + */ +public final class NetworkExtensionAuthenticator { + + private static final Map COOKIES = new HashMap<>(); + private static final Set PERSISTENT_COOKIES = new HashSet<>(); + + private static volatile boolean rememberOption = false; + + public static boolean evaluate(NetworkExtensionClient extension) { + + final String cookie = extension.getCookie(); + + if (cookie != null && PERSISTENT_COOKIES.contains(cookie)) + return true; + + return extension.isInstalledExtension() + ? claimSession(extension.getFileName(), cookie) + : askForPermission(extension); + } + + /** + * Authenticate an extension and remove the cookie + * + * @return {@code true} if the extension is authenticated. + */ + private static boolean claimSession(String filename, String cookie) { + if (COOKIES.containsKey(filename) && COOKIES.get(filename).equals(cookie)) { + COOKIES.remove(filename); + return true; + } + return false; + } + + /** + * For not yet installed extensions, open a confirmation dialog. + * + * @param extension the {@link NetworkExtensionClient extension} to ask permission for. + * + * @return {@code true} if permission is granted, {@code false} if not. + */ + private static boolean askForPermission(NetworkExtensionClient extension) { + boolean[] allowConnection = {true}; + + final String connectExtensionKey = "allow_extension_connection"; + + if (ConfirmationDialog.showDialog(connectExtensionKey)) { + + final CountDownLatch countDownLatch = new CountDownLatch(0); + + Platform.runLater(() -> { + Alert alert = ConfirmationDialog.createAlertWithOptOut(Alert.AlertType.WARNING, connectExtensionKey + , LanguageBundle.get("alert.confirmation.windowtitle"), null, + "", LanguageBundle.get("alert.confirmation.button.remember"), + ButtonType.YES, ButtonType.NO + ); + + alert.getDialogPane().setContent(new Label(String.format(LanguageBundle.get("alert.extconnection.content"), extension.getTitle()).replaceAll("\\\\n", System.lineSeparator()))); + + try { + if (!(TitleBarController.create(alert).showAlertAndWait() + .filter(t -> t == ButtonType.YES).isPresent())) { + allowConnection[0] = false; + } + } catch (IOException e) { + e.printStackTrace(); + } + countDownLatch.countDown(); + if (!ConfirmationDialog.showDialog(connectExtensionKey)) { + rememberOption = allowConnection[0]; + } + }); + + try { + countDownLatch.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return allowConnection[0]; + } + + return rememberOption; + } + + public static String generateCookieForExtension(String filename) { + final String cookie = generateRandomCookie(); + COOKIES.put(filename, cookie); + return cookie; + } + + public static String generatePermanentCookie() { + final String cookie = generateRandomCookie(); + PERSISTENT_COOKIES.add(cookie); + return cookie; + } + + private static String generateRandomCookie() { + final StringBuilder builder = new StringBuilder(); + final Random r = new Random(); + for (int i = 0; i < 40; i++) + builder.append(r.nextInt(40)); + return builder.toString(); + } +} diff --git a/G-Earth/src/main/java/gearth/services/extension_handler/extensions/implementations/network/NetworkExtensionClient.java b/G-Earth/src/main/java/gearth/services/extension_handler/extensions/implementations/network/NetworkExtensionClient.java new file mode 100644 index 0000000..7b1fa3d --- /dev/null +++ b/G-Earth/src/main/java/gearth/services/extension_handler/extensions/implementations/network/NetworkExtensionClient.java @@ -0,0 +1,207 @@ +package gearth.services.extension_handler.extensions.implementations.network; + +import gearth.misc.HostInfo; +import gearth.protocol.HMessage; +import gearth.protocol.HPacket; +import gearth.protocol.connection.HClient; +import gearth.services.extension_handler.extensions.ExtensionType; +import gearth.services.extension_handler.extensions.GEarthExtension; +import gearth.services.extension_handler.extensions.implementations.network.NetworkExtensionMessage.Incoming; +import gearth.services.extension_handler.extensions.implementations.network.NetworkExtensionMessage.Outgoing; +import gearth.services.packet_info.PacketInfoManager; +import io.netty.channel.Channel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; + +/** + * A client for managing remote extensions. + * + * @author Dorving + */ +public final class NetworkExtensionClient extends GEarthExtension { + + private final static Logger LOGGER = LoggerFactory.getLogger(NetworkExtensionClient.class); + + private final Channel channel; + + private final String title; + private final String author; + private final String version; + private final String description; + private final String fileName; + private final String cookie; + private final boolean fireEventButtonVisible; + private final boolean leaveButtonVisible; + private final boolean deleteButtonVisible; + private final boolean isInstalledExtension; + + /** + * Create a new {@link NetworkExtensionClient} instance. + * + * @param channel the channel through which to communicate with the remote extension. + * @param info the {@link Incoming.ExtensionInfo} detailing the extension. + */ + public NetworkExtensionClient(Channel channel, Incoming.ExtensionInfo info) { + this.channel = channel; + title = info.getTitle(); + author = info.getAuthor(); + version = info.getVersion(); + description = info.getDescription(); + fireEventButtonVisible = info.isOnClickUsed(); + leaveButtonVisible = info.isCanLeave(); + deleteButtonVisible = info.isCanDelete(); + isInstalledExtension = info.getFile() != null; + fileName = info.getFile(); + cookie = info.getCookie(); + } + + /** + * Handles {@link Incoming incoming messages}. + * + * @param incoming the {@link Incoming message} to be handled. + */ + public void handleIncomingMessage(Incoming incoming) { + try { + if (incoming instanceof Incoming.RequestFlags) + requestFlags(); + else if (incoming instanceof Incoming.SendMessage) { + final Incoming.SendMessage message = ((Incoming.SendMessage) incoming); + final HPacket packet = message.getPacket(); + if (!packet.isCorrupted()) + sendMessage(message.getDirection(), packet); + } else if (incoming instanceof Incoming.ManipulatedPacket) { + sendManipulatedPacket(((Incoming.ManipulatedPacket) incoming).gethMessage()); + } else if (incoming instanceof Incoming.ExtensionConsoleLog) { + log(((Incoming.ExtensionConsoleLog) incoming).getContents()); + } else if (incoming instanceof Incoming.PacketToStringRequest) { + final HPacket hPacket = new HPacket(new byte[0]); + hPacket.constructFromString(((Incoming.PacketToStringRequest) incoming).getString()); + packetToStringRequest(hPacket); + } else if (incoming instanceof Incoming.StringToPacketRequest) { + stringToPacketRequest(((Incoming.StringToPacketRequest) incoming).getString()); + } + } catch (Exception e){ + LOGGER.error("Failed to handle incoming message {} (channel={})", incoming, channel, e); + } + } + + @Override + public void init(boolean isConnected, HostInfo hostInfo) { + channel.writeAndFlush(new Outgoing.Init(isConnected, hostInfo)); + } + + @Override + public void close() { + try { + channel.close(); + } catch (Exception e){ + LOGGER.error("Failed to close client (channel={})", channel, e); + } finally { + hasClosed(); + } + } + + @Override + public void connectionStart(String host, int port, String hotelVersion, String clientIdentifier, HClient clientType, PacketInfoManager packetInfoManager) { + channel.writeAndFlush(new Outgoing.ConnectionStart( + host, + port, + hotelVersion, + clientIdentifier, + clientType, + packetInfoManager + )); + } + + @Override + public void connectionEnd() { + channel.writeAndFlush(new Outgoing.ConnectionEnd()); + } + + @Override + public void doubleclick() { + channel.writeAndFlush(new Outgoing.OnDoubleClick()); + } + + @Override + public void provideFlags(String[] flags) { + channel.writeAndFlush(new Outgoing.FlagsCheck(Arrays.asList(flags))); + } + + @Override + public void updateHostInfo(HostInfo hostInfo) { + channel.writeAndFlush(new Outgoing.UpdateHostInfo(hostInfo)); + } + + @Override + public void packetIntercept(HMessage hMessage) { + final String messageAsString = hMessage.stringify(); + channel.writeAndFlush(new Outgoing.PacketIntercept(messageAsString)); + } + + @Override + public void packetToStringResponse(String string, String expression) { + channel.writeAndFlush(new Outgoing.PacketToStringResponse(string, expression)); + } + + @Override + public void stringToPacketResponse(HPacket packet) { + channel.writeAndFlush(new Outgoing.StringToPacketResponse(packet.stringify())); + } + + @Override + public ExtensionType extensionType() { + return ExtensionType.EXTERNAL; + } + + @Override + public String getAuthor() { + return author; + } + + @Override + public String getDescription() { + return description; + } + + @Override + public String getTitle() { + return title; + } + + @Override + public String getVersion() { + return version; + } + + @Override + public String getFileName() { + return fileName; + } + + public String getCookie() { + return cookie; + } + + @Override + public boolean isFireButtonUsed() { + return fireEventButtonVisible; + } + + @Override + public boolean isDeleteButtonVisible() { + return deleteButtonVisible; + } + + @Override + public boolean isLeaveButtonVisible() { + return leaveButtonVisible; + } + + @Override + public boolean isInstalledExtension() { + return isInstalledExtension; + } +} diff --git a/G-Earth/src/main/java/gearth/services/extension_handler/extensions/implementations/network/NetworkExtensionCodec.java b/G-Earth/src/main/java/gearth/services/extension_handler/extensions/implementations/network/NetworkExtensionCodec.java new file mode 100644 index 0000000..6fb1807 --- /dev/null +++ b/G-Earth/src/main/java/gearth/services/extension_handler/extensions/implementations/network/NetworkExtensionCodec.java @@ -0,0 +1,307 @@ +package gearth.services.extension_handler.extensions.implementations.network; + +import gearth.misc.HostInfo; +import gearth.protocol.HMessage; +import gearth.protocol.HPacket; +import gearth.protocol.connection.HClient; +import gearth.services.extension_handler.extensions.implementations.network.NetworkExtensionMessage.Outgoing; +import gearth.services.extension_handler.extensions.implementations.network.NetworkExtensionMessage.Incoming; +import gearth.services.packet_info.PacketInfoManager; + +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.Function; + +import static gearth.protocol.HMessage.Direction.TOCLIENT; +import static gearth.protocol.HMessage.Direction.TOSERVER; + +/** + * THE EXTENSION COMMUNICATION PRINCIPLES & PROTOCOL: + * + * You will be able to write extensions in ANY language you want, but we will only provide an interface + * for Java so if you write your own in for example Python, make SURE you do it correctly or it could fuck G-Earth. + * + * Also, don't let the method where you manipulate the packets block. Similiar as how you must not block things in an UI thread. + * Why? Because Habbo relies on the TCP protocol, which ENSURES that packets get received in the right order, so we will not be fucking that up. + * That means that all packets following the packet you're manipulating in your extension will be blocked from being sent untill you're done. + * TIP: If you're trying to replace a packet in your extension but you know it will take time, just block the packet, end the method, and let something asynchronous send + * the edited packet when you're done. + * + * + * You may ignore everything beneath this line if you're extending the abstract Extension class we provide in Java. + * ----------------------------------------------------------------------------------------------------------------- + * + * (0. We recommend to use a cross-platform language for your extension) + * + * 1. An extension will run as a seperate process on your device and has to be called with the flag "-p ", + * where is a random port where the G-Earth local extension server will run on. Your extension has to connect with this server. + * + * 2. G-Earth will open your program only ONCE, that is on the boot of G-Earth or when you install the exension. + * Same story goes for closing the connection between the program and G-Earth, only once (on uninstall or close of G-Earth). + * + * You may also run your extension completely seperate from G-Earth for debugging purpose for example, then it won't be installed in G-Earth + * (but you have to configure the port yourself, which will be displayed in the extension page) + * + * 3. Once a connection is made, your extension will have to deal with the following incoming & outgoing messages as described (follows the same protocol structure as Habbo communication does): + * (if an object is sent; the object will be sent with its String representation from the StringifyAble interface, so the object's class must implement that) + * + * INCOMING MESSAGES: (marked with * if you're required to correctly respond or take action, ** if it's a response on something you requested) + * ----------------------------------------------------------------------------------------------------- + * | ID | TITLE | BODY & DESCRIPTION | + * ----------------------------------------------------------------------------------------------------- + * | 1 | ON-DOUBLECLICK | No body, the extension has been double clicked from within G-Earth | ( <- typically for tanji-module-like extensions you will open the UI here) + * ----------------------------------------------------------------------------------------------------- + * | 2 | INFO-REQUEST* | Needs response with extension info (name, desc, author, version, ..), | + * | | | exact implementation is found in the Java abstract Extension class | + * ----------------------------------------------------------------------------------------------------- + * | 3 | PACKET-INTERCEPT* | Includes the whole HMessage as body, needs response with the | + * | | | manipulated HMessage (OUTGOING id: 2) | + * ----------------------------------------------------------------------------------------------------- + * | 4 | FLAGS-CHECK** | Body: String with G-Earth's boot flags (args from static gearth method) | + * ----------------------------------------------------------------------------------------------------- + * | 5 | CONNECTION START | just a note that a new connection has been made, | + * | | | you could check this yourself as well (listen to out:4000 packet) | + * | | | host/port, hotel version | + * ----------------------------------------------------------------------------------------------------- + * | 6 | CONNECTION END | Empty body, just a note that a connection has ended | + * ----------------------------------------------------------------------------------------------------- + * | 7 | INIT | Empty body, a connection with G-Earth has been set up | + * ----------------------------------------------------------------------------------------------------- + * | 99 | FREE FLOW | extension-specific body | + * ----------------------------------------------------------------------------------------------------- + * + * OUTGOING MESSAGES: (marked with * if that is a response to one of the msgs above) + * ----------------------------------------------------------------------------------------------------- + * | ID | TITLE | BODY & DESCRIPTION | + * ----------------------------------------------------------------------------------------------------- + * | 1 | EXTENSION-INFO* | Response for INFO-REQUEST | + * ----------------------------------------------------------------------------------------------------- + * | 2 | MANIPULATED-PACKET*| Response for PACKET-INTERCEPT | + * ----------------------------------------------------------------------------------------------------- + * | 3 | REQUEST-FLAGS | Request G-Earth's flags, results in incoming FLAGS-CHECK response | + * ----------------------------------------------------------------------------------------------------- + * | 4 | SEND-MESSAGE | Body: HMessage object. Sends the HPacket wrapped in the HMessage | + * | | | to the client/server | + * ----------------------------------------------------------------------------------------------------- + * | 99 | FREE FLOW | extension-specific body | + * ----------------------------------------------------------------------------------------------------- + * + * 4. Your extension will only appear in the extension list once the EXTENSION-INFO has been received by G-Earth + */ +public final class NetworkExtensionCodec { + + private final static Map, PacketStructure> outgoingPacketStructures = new HashMap<>(); + private final static Map incomingPacketStructures = new HashMap<>(); + + public static PacketStructure getIncomingStructure(int headerId) { + return incomingPacketStructures.get(headerId); + } + + public static PacketStructure getOutgoingStructure(T message) { + return outgoingPacketStructures.get(message.getClass()); + } + + static { + registerOutgoingMessages(); + registerIncomingMessages(); + } + + private static void registerIncomingMessages() { + register(Incoming.ExtensionInfo.HEADER_ID, + Incoming.ExtensionInfo.class, + (message, hPacket) -> { + hPacket.appendString(message.getTitle()); + hPacket.appendString(message.getAuthor()); + hPacket.appendString(message.getVersion()); + hPacket.appendString(message.getDescription()); + hPacket.appendBoolean(message.isOnClickUsed()); + hPacket.appendBoolean(message.getFile() != null); + hPacket.appendString(Optional.ofNullable(message.getFile()).orElse("")); + hPacket.appendString(Optional.ofNullable(message.getCookie()).orElse("")); + hPacket.appendBoolean(message.isCanLeave()); + hPacket.appendBoolean(message.isCanDelete()); + }, + (hPacket -> { + final String title = hPacket.readString(); + final String author = hPacket.readString(); + final String version = hPacket.readString(); + final String description = hPacket.readString(); + final boolean isOnClickUsed = hPacket.readBoolean(); + final boolean hasFile = hPacket.readBoolean(); + String file = hPacket.readString(); + if (!hasFile) + file = null; + String cookie = hPacket.readString(); + if (cookie.isEmpty()) + cookie = null; + final boolean canLeave = hPacket.readBoolean(); + final boolean canDelete = hPacket.readBoolean(); + return new Incoming.ExtensionInfo(title, author, version, description, isOnClickUsed, file, cookie, canLeave, canDelete); + })); + register(Incoming.ManipulatedPacket.MANIPULATED_PACKET, + Incoming.ManipulatedPacket.class, + (message, hPacket) -> hPacket.appendLongString(message.gethMessage().stringify()), + (hPacket -> { + final String packetString = hPacket.readLongString(6); + final HMessage hMessage = new HMessage(packetString); + return new Incoming.ManipulatedPacket(hMessage); + })); + register(Incoming.SendMessage.HEADER_ID, + Incoming.SendMessage.class, + ((message, hPacket) -> { + hPacket.appendByte((byte) (message.getDirection() == TOCLIENT ? 0 : 1)); + hPacket.appendInt(message.getPacket().getBytesLength()); + hPacket.appendBytes(message.getPacket().toBytes()); + }), + (hPacket -> { + final byte side = hPacket.readByte(); + final int length = hPacket.readInteger(); + final byte[] data = hPacket.readBytes(length); + final HPacket packet = new HPacket(data); + return new Incoming.SendMessage(packet, side == 0 ? TOCLIENT : TOSERVER); + })); + register(Incoming.RequestFlags.HEADER_ID, + Incoming.RequestFlags.class, + (message, hPacket) -> { + }, + (hPacket -> new Incoming.RequestFlags())); + register(Incoming.ExtensionConsoleLog.HEADER_ID, + Incoming.ExtensionConsoleLog.class, + (message, hPacket) -> hPacket.appendString(message.getContents()), + (hPacket -> new Incoming.ExtensionConsoleLog(hPacket.readString()))); + register(Incoming.PacketToStringRequest.HEADER_ID, + Incoming.PacketToStringRequest.class, + (message, hPacket) -> hPacket.appendLongString(message.getString()), + (hPacket -> new Incoming.PacketToStringRequest(hPacket.readLongString()))); + register(Incoming.StringToPacketRequest.HEADER_ID, + Incoming.StringToPacketRequest.class, + (message, hPacket) -> hPacket.appendLongString(message.getString(), StandardCharsets.UTF_8), + (hPacket -> new Incoming.StringToPacketRequest(hPacket.readLongString(StandardCharsets.UTF_8)))); + } + + private static void registerOutgoingMessages() { + register(Outgoing.InfoRequest.HEADER_ID, + Outgoing.InfoRequest.class, + (message, hPacket) -> { + }, + (hPacket -> new Outgoing.InfoRequest())); + register(Outgoing.ConnectionStart.HEADER_ID, + Outgoing.ConnectionStart.class, + (message, hPacket) -> { + hPacket.appendString(message.getHost()); + hPacket.appendInt(message.getConnectionPort()); + hPacket.appendString(message.getHotelVersion()); + hPacket.appendString(message.getClientIdentifier()); + hPacket.appendString(message.getClientType().name()); + message.getPacketInfoManager().appendToPacket(hPacket); + }, + (hPacket -> new Outgoing.ConnectionStart( + hPacket.readString(), + hPacket.readInteger(), + hPacket.readString(), + hPacket.readString(), + HClient.valueOf(hPacket.readString()), + PacketInfoManager.readFromPacket(hPacket) + ))); + register(Outgoing.ConnectionEnd.HEADER_ID, + Outgoing.ConnectionEnd.class, + (message, hPacket) -> { + }, + (hPacket -> new Outgoing.ConnectionEnd())); + register(Outgoing.FlagsCheck.HEADER_ID, + Outgoing.FlagsCheck.class, + (message, hPacket) -> { + hPacket.appendInt(message.getFlags().size()); + message.getFlags().forEach(hPacket::appendString); + }, + (hPacket -> { + final int size = hPacket.readInteger(); + final List flags = new ArrayList<>(); + for (int i = 0; i < size; i++) + flags.add(hPacket.readString()); + return new Outgoing.FlagsCheck(flags); + })); + register(Outgoing.Init.HEADER_ID, + Outgoing.Init.class, + (message, hPacket) -> { + hPacket.appendBoolean(message.isDelayInit()); + message.getHostInfo().appendToPacket(hPacket); + }, + (hPacket -> new Outgoing.Init(hPacket.readBoolean(), HostInfo.fromPacket(hPacket)))); + register(Outgoing.OnDoubleClick.HEADER_ID, + Outgoing.OnDoubleClick.class, + (message, hPacket) -> { + }, + (hPacket -> new Outgoing.OnDoubleClick())); + register(Outgoing.PacketIntercept.HEADER_ID, + Outgoing.PacketIntercept.class, + (message, hPacket) -> hPacket.appendLongString(message.getPacketString()), + (hPacket -> new Outgoing.PacketIntercept(hPacket.readLongString()))); + register(Outgoing.UpdateHostInfo.HEADER_ID, + Outgoing.UpdateHostInfo.class, + (message, hPacket) -> message.getHostInfo().appendToPacket(hPacket), + (hPacket -> new Outgoing.UpdateHostInfo(HostInfo.fromPacket(hPacket)))); + register(Outgoing.PacketToStringResponse.HEADER_ID, + Outgoing.PacketToStringResponse.class, + (message, hPacket) -> { + hPacket.appendLongString(message.getString()); + hPacket.appendLongString(message.getExpression(), StandardCharsets.UTF_8); + }, + (hPacket -> new Outgoing.PacketToStringResponse(hPacket.readLongString(), hPacket.readLongString(StandardCharsets.UTF_8))) + ); + register(Outgoing.StringToPacketResponse.HEADER_ID, + Outgoing.StringToPacketResponse.class, + (message, hPacket) -> hPacket.appendLongString(message.getString()), + (hPacket -> new Outgoing.StringToPacketResponse(hPacket.readLongString())) + ); + } + + private static void register(final int headerId, Class tClass, BiConsumer writer, Function reader) { + final PacketStructure packetStructure = new PacketStructure(headerId, tClass.getSimpleName(), writer, reader); + if (tClass.getSuperclass() == Outgoing.class) + outgoingPacketStructures.put(tClass, packetStructure); + else + incomingPacketStructures.put(headerId, packetStructure); + } + + /** + * Represents the packet structure of a {@link NetworkExtensionMessage}. + * + * Can be used to {@link PacketStructure#writer write} messages to packets + * and {@link PacketStructure#reader read} messages from packets. + * + * @apiNote At the moment both outgoing and incoming messages have a reader and writer defined, + * this is so that in the future the same codec can be used for the Extensions API. + */ + static class PacketStructure { + + private final int headerId; + private final String name; + private final BiConsumer writer; + private final Function reader; + + public PacketStructure(int headerId, String name, BiConsumer writer, Function reader) { + this.headerId = headerId; + this.name = name; + this.writer = writer; + this.reader = reader; + } + + public int getHeaderId() { + return headerId; + } + + public String getName() { + return name; + } + + public BiConsumer getWriter() { + return writer; + } + + public Function getReader() { + return reader; + } + } +} diff --git a/G-Earth/src/main/java/gearth/services/extension_handler/extensions/implementations/network/NetworkExtensionInfo.java b/G-Earth/src/main/java/gearth/services/extension_handler/extensions/implementations/network/NetworkExtensionInfo.java deleted file mode 100644 index 50282cf..0000000 --- a/G-Earth/src/main/java/gearth/services/extension_handler/extensions/implementations/network/NetworkExtensionInfo.java +++ /dev/null @@ -1,110 +0,0 @@ -package gearth.services.extension_handler.extensions.implementations.network; - -public class NetworkExtensionInfo { - - /** - * THE EXTENSION COMMUNCATION PRINCIPLES & PROTOCOL: - * - * You will be able to write extensions in ANY language you want, but we will only provide an interface - * for Java so if you write your own in for example Python, make SURE you do it correctly or it could fuck G-Earth. - * - * Also, don't let the method where you manipulate the packets block. Similiar as how you must not block things in an UI thread. - * Why? Because Habbo relies on the TCP protocol, which ENSURES that packets get received in the right order, so we will not be fucking that up. - * That means that all packets following the packet you're manipulating in your extension will be blocked from being sent untill you're done. - * TIP: If you're trying to replace a packet in your extension but you know it will take time, just block the packet, end the method, and let something asynchronous send - * the editted packet when you're done. - * - * - * You may ignore everything beneath this line if you're extending the abstract Extension class we provide in Java. - * ----------------------------------------------------------------------------------------------------------------- - * - * (0. We recommend to use a cross-platform language for your extension) - * - * 1. An extension will run as a seperate process on your device and has to be called with the flag "-p ", - * where is a random port where the G-Earth local extension server will run on. Your extension has to connect with this server. - * - * 2. G-Earth will open your program only ONCE, that is on the boot of G-Earth or when you install the exension. - * Same story goes for closing the connection between the program and G-Earth, only once (on uninstall or close of G-Earth). - * - * You may also run your extension completely seperate from G-Earth for debugging purpose for example, then it won't be installed in G-Earth - * (but you have to configure the port yourself, which will be displayed in the extension page) - * - * 3. Once a connection is made, your extension will have to deal with the following incoming & outgoing messages as described (follows the same protocol structure as Habbo communication does): - * (if an object is sent; the object will be sent with its String representation from the StringifyAble interface, so the object's class must implement that) - * - * INCOMING MESSAGES: (marked with * if you're required to correctly respond or take action, ** if it's a response on something you requested) - * ----------------------------------------------------------------------------------------------------- - * | ID | TITLE | BODY & DESCRIPTION | - * ----------------------------------------------------------------------------------------------------- - * | 1 | ON-DOUBLECLICK | No body, the extension has been double clicked from within G-Earth | ( <- typically for tanji-module-like extensions you will open the UI here) - * ----------------------------------------------------------------------------------------------------- - * | 2 | INFO-REQUEST* | Needs response with extension info (name, desc, author, version, ..), | - * | | | exact implementation is found in the Java abstract Extension class | - * ----------------------------------------------------------------------------------------------------- - * | 3 | PACKET-INTERCEPT* | Includes the whole HMessage as body, needs response with the | - * | | | manipulated HMessage (OUTGOING id: 2) | - * ----------------------------------------------------------------------------------------------------- - * | 4 | FLAGS-CHECK** | Body: String with G-Earth's boot flags (args from static gearth method) | - * ----------------------------------------------------------------------------------------------------- - * | 5 | CONNECTION START | just a note that a new connection has been made, | - * | | | you could check this yourself as well (listen to out:4000 packet) | - * | | | host/port, hotel version | - * ----------------------------------------------------------------------------------------------------- - * | 6 | CONNECTION END | Empty body, just a note that a connection has ended | - * ----------------------------------------------------------------------------------------------------- - * | 7 | INIT | Empty body, a connection with G-Earth has been set up | - * ----------------------------------------------------------------------------------------------------- - * | 99 | FREE FLOW | extension-specific body | - * ----------------------------------------------------------------------------------------------------- - * - * OUTGOING MESSAGES: (marked with * if that is a response to one of the msgs above) - * ----------------------------------------------------------------------------------------------------- - * | ID | TITLE | BODY & DESCRIPTION | - * ----------------------------------------------------------------------------------------------------- - * | 1 | EXTENSION-INFO* | Response for INFO-REQUEST | - * ----------------------------------------------------------------------------------------------------- - * | 2 | MANIPULATED-PACKET*| Response for PACKET-INTERCEPT | - * ----------------------------------------------------------------------------------------------------- - * | 3 | REQUEST-FLAGS | Request G-Earth's flags, results in incoming FLAGS-CHECK response | - * ----------------------------------------------------------------------------------------------------- - * | 4 | SEND-MESSAGE | Body: HMessage object. Sends the HPacket wrapped in the HMessage | - * | | | to the client/server | - * ----------------------------------------------------------------------------------------------------- - * | 99 | FREE FLOW | extension-specific body | - * ----------------------------------------------------------------------------------------------------- - * - * 4. Your extension will only appear in the extension list once the EXTENSION-INFO has been received by G-Earth - * - * - */ - - - public static class OUTGOING_MESSAGES_IDS { - public static final int ONDOUBLECLICK = 1; - public static final int INFOREQUEST = 2; - public static final int PACKETINTERCEPT = 3; - public static final int FLAGSCHECK = 4; - public static final int CONNECTIONSTART = 5; - public static final int CONNECTIONEND = 6; - public static final int INIT = 7; - - public static final int UPDATEHOSTINFO = 10; - - public static final int PACKETTOSTRING_RESPONSE = 20; - public static final int STRINGTOPACKET_RESPONSE = 21; - } - - - public static class INCOMING_MESSAGES_IDS { - public static final int EXTENSIONINFO = 1; - public static final int MANIPULATEDPACKET = 2; - public static final int REQUESTFLAGS = 3; - public static final int SENDMESSAGE = 4; - - public static final int PACKETTOSTRING_REQUEST = 20; - public static final int STRINGTOPACKET_REQUEST = 21; - - public static final int EXTENSIONCONSOLELOG = 98; - } - -} diff --git a/G-Earth/src/main/java/gearth/services/extension_handler/extensions/implementations/network/NetworkExtensionMessage.java b/G-Earth/src/main/java/gearth/services/extension_handler/extensions/implementations/network/NetworkExtensionMessage.java new file mode 100644 index 0000000..460aca0 --- /dev/null +++ b/G-Earth/src/main/java/gearth/services/extension_handler/extensions/implementations/network/NetworkExtensionMessage.java @@ -0,0 +1,417 @@ +package gearth.services.extension_handler.extensions.implementations.network; + +import gearth.misc.HostInfo; +import gearth.protocol.HMessage; +import gearth.protocol.HPacket; +import gearth.protocol.connection.HClient; +import gearth.services.packet_info.PacketInfoManager; + +import java.util.List; + +/** + * Represents a message send or received by G-Earth and a remote extension. + * + * @see NetworkExtensionCodec the encoding/decoding structures + * @see Incoming messages coming from the remote extension to G-Earth + * @see Outgoing messages coming from G-Earth to the remote extension + * + * @author Dorving, Jonas + */ +public class NetworkExtensionMessage { + + /** + * Represents {@link NetworkExtensionMessage messages} coming from the remote extension to G-Earth. + */ + public static class Incoming extends NetworkExtensionMessage { + + /** + * This contains info about the remote extension trying to connect. + * + * Once this {@link NetworkExtensionMessage message} is received, + * a new {@link NetworkExtensionClient} is created to handle the communication. + * + * @see Outgoing.InfoRequest the request. + */ + public static class ExtensionInfo extends Incoming { + + public static final int HEADER_ID = 1; + + private final String title; + private final String author; + private final String version; + private final String description; + private final boolean onClickUsed; + private final String file; + private final String cookie; + private final boolean canLeave; + private final boolean canDelete; + + public ExtensionInfo(String title, String author, String version, String description, boolean onClickUsed, String file, String cookie, boolean canLeave, boolean canDelete) { + this.title = title; + this.author = author; + this.version = version; + this.description = description; + this.onClickUsed = onClickUsed; + this.file = file; + this.cookie = cookie; + this.canLeave = canLeave; + this.canDelete = canDelete; + } + + public String getTitle() { + return title; + } + + public String getAuthor() { + return author; + } + + public String getVersion() { + return version; + } + + public String getDescription() { + return description; + } + + public boolean isOnClickUsed() { + return onClickUsed; + } + + public String getFile() { + return file; + } + + public String getCookie() { + return cookie; + } + + public boolean isCanLeave() { + return canLeave; + } + + public boolean isCanDelete() { + return canDelete; + } + } + + /** + * Remote extension request G-Earth's flags. + * + * @see Outgoing.FlagsCheck the response. + */ + public static class RequestFlags extends Incoming { + public static final int HEADER_ID = 3; + } + + /** + * Received a {@link HPacket} from the remote connection + * and forward it either to the game {@link HMessage.Direction#TOSERVER server} + * or game {@link HMessage.Direction#TOCLIENT client}. + */ + public static class SendMessage extends Incoming { + + public static final int HEADER_ID = 4; + + private final HPacket packet; + private final HMessage.Direction direction; + + public SendMessage(HPacket packet, HMessage.Direction direction) { + this.packet = packet; + this.direction = direction; + } + + public HPacket getPacket() { + return packet; + } + + public HMessage.Direction getDirection() { + return direction; + } + } + + /** + * TODO: add documentation. + * + * @see Outgoing.PacketToStringResponse the response. + */ + public static class PacketToStringRequest extends Incoming { + + public static final int HEADER_ID = 20; + + private final String string; + + public PacketToStringRequest(String string) { + this.string = string; + } + + public String getString() { + return string; + } + } + + /** + * TODO: add documentation. + * + * @see Outgoing.StringToPacketResponse the response. + */ + public static class StringToPacketRequest extends Incoming { + + public static final int HEADER_ID = 21; + + private final String string; + + public StringToPacketRequest(String string) { + this.string = string; + } + + public String getString() { + return string; + } + } + + /** + * TODO: add documentation. + */ + public static class ExtensionConsoleLog extends Incoming { + + public static final int HEADER_ID = 98; + + private final String contents; + + public ExtensionConsoleLog(String contents) { + this.contents = contents; + } + + public String getContents() { + return contents; + } + } + + /** + * Represents a packet modified by the remote extension. + * + * @see Outgoing.PacketIntercept the ougoing message containing the original packet. + */ + public static class ManipulatedPacket extends Incoming { + public static final int MANIPULATED_PACKET = 2; + private final HMessage hMessage; + + public ManipulatedPacket(HMessage hMessage) { + this.hMessage = hMessage; + } + + public HMessage gethMessage() { + return hMessage; + } + } + } + /** + * Represents {@link NetworkExtensionMessage messages} coming from G-Earth to the remote extension. + */ + public static class Outgoing extends NetworkExtensionMessage{ + + /** + * The extension has been double-clicked from within G-Earth. + */ + public static class OnDoubleClick extends Outgoing { + public static final int HEADER_ID = 1; + } + + /** + * Request for remote extension to send {@link Incoming.ExtensionInfo}. + * + * This is the very first message send after a connection is established. + * + * @see Incoming.ExtensionInfo the response. + */ + public static class InfoRequest extends Outgoing { + public static final int HEADER_ID = 2; + } + + /** + * Forwards a packet intercepted by G-Earth to the remote extension. + * + * @see Incoming.ManipulatedPacket the response. + */ + public static class PacketIntercept extends Outgoing { + + public static final int HEADER_ID = 3; + + private final String packetString; + + public PacketIntercept(String packetString) { + this.packetString = packetString; + } + + public String getPacketString() { + return packetString; + } + } + + /** + * Contains program arguments of G-Earth. + * + * @see Incoming.RequestFlags the request. + */ + public static class FlagsCheck extends Outgoing { + + public static final int HEADER_ID = 4; + + private final List flags; + + public FlagsCheck(List flags) { + this.flags = flags; + } + + public List getFlags() { + return flags; + } + } + + /** + * Notifies remote extension that a connection to a hotel has been established. + * + * @apiNote could check this yourself as well (listen to out:4000 packet) + */ + public static class ConnectionStart extends Outgoing { + + public static final int HEADER_ID = 5; + + private final String host; + private final int connectionPort; + private final String hotelVersion; + private final String clientIdentifier; + private final HClient clientType; + private final PacketInfoManager packetInfoManager; + + public ConnectionStart(String host, int connectionPort, String hotelVersion, String clientIdentifier, HClient clientType, PacketInfoManager packetInfoManager) { + this.host = host; + this.connectionPort = connectionPort; + this.hotelVersion = hotelVersion; + this.clientIdentifier = clientIdentifier; + this.clientType = clientType; + this.packetInfoManager = packetInfoManager; + } + + public String getHost() { + return host; + } + + public int getConnectionPort() { + return connectionPort; + } + + public String getHotelVersion() { + return hotelVersion; + } + + public String getClientIdentifier() { + return clientIdentifier; + } + + public HClient getClientType() { + return clientType; + } + + public PacketInfoManager getPacketInfoManager() { + return packetInfoManager; + } + } + + /** + * Notifies a remote extension that the connection to the hotel has been closed. + */ + public static class ConnectionEnd extends Outgoing { + public static final int HEADER_ID = 6; + } + + /** + * Notifies a remote extension that it has been accepted by G-Earth. + */ + public static class Init extends Outgoing { + + public static final int HEADER_ID = 7; + + private final boolean delayInit; + private final HostInfo hostInfo; + + public Init(boolean delayInit, HostInfo hostInfo) { + this.delayInit = delayInit; + this.hostInfo = hostInfo; + } + + public boolean isDelayInit() { + return delayInit; + } + + public HostInfo getHostInfo() { + return hostInfo; + } + } + + /** + * TODO: add documentation. + */ + public static class UpdateHostInfo extends Outgoing { + + public static final int HEADER_ID = 10; + + private final HostInfo hostInfo; + + public UpdateHostInfo(HostInfo hostInfo) { + this.hostInfo = hostInfo; + } + + public HostInfo getHostInfo() { + return hostInfo; + } + } + + /** + * TODO: add documentation. + * + * @see Incoming.PacketToStringRequest the request. + */ + public static class PacketToStringResponse extends Outgoing { + + public static final int HEADER_ID = 20; + + private final String string; + private final String expression; + + public PacketToStringResponse(String string, String expression) { + this.string = string; + this.expression = expression; + } + + public String getString() { + return string; + } + + public String getExpression() { + return expression; + } + } + + /** + * TODO: add documentation. + * + * @see Incoming.StringToPacketRequest the request. + */ + public static class StringToPacketResponse extends Outgoing { + + public static final int HEADER_ID = 21; + + private final String string; + + public StringToPacketResponse(String string) { + this.string = string; + } + + public String getString() { + return string; + } + } + } +} diff --git a/G-Earth/src/main/java/gearth/services/extension_handler/extensions/implementations/network/NetworkExtensionServer.java b/G-Earth/src/main/java/gearth/services/extension_handler/extensions/implementations/network/NetworkExtensionServer.java new file mode 100644 index 0000000..54c8bc2 --- /dev/null +++ b/G-Earth/src/main/java/gearth/services/extension_handler/extensions/implementations/network/NetworkExtensionServer.java @@ -0,0 +1,277 @@ +package gearth.services.extension_handler.extensions.implementations.network; + +import gearth.protocol.HPacket; +import gearth.services.extension_handler.extensions.extensionproducers.ExtensionProducer; +import gearth.services.extension_handler.extensions.extensionproducers.ExtensionProducerFactory; +import gearth.services.extension_handler.extensions.extensionproducers.ExtensionProducerObserver; +import gearth.services.extension_handler.extensions.implementations.network.NetworkExtensionCodec.PacketStructure; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.channel.*; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.ByteToMessageDecoder; +import io.netty.handler.codec.MessageToByteEncoder; +import io.netty.util.Attribute; +import io.netty.util.AttributeKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.DatagramSocket; +import java.net.ServerSocket; +import java.util.List; +import java.util.Optional; +import java.util.function.BiConsumer; + +import static gearth.services.extension_handler.extensions.implementations.network.NetworkExtensionMessage.*; + +/** + * Represents an {@link ExtensionProducer} that implements a server to which + * remotely-ran extensions can connect. + * + * @see ExtensionProducerFactory#getAll() for instance creation. + * + * @author Dorving, Jonas + */ +public final class NetworkExtensionServer implements ExtensionProducer { + + private final static Logger LOGGER = LoggerFactory.getLogger(NetworkExtensionServer.class); + + /** + * Initial port server tries to listen at, if {@link ServerSocket} creation fails, + * it tries next port. + */ + private static final int PORT_ONSET = 9092; + + /** + * The port at which the server is listening. + */ + private int port = -1; + + @Override + public void startProducing(ExtensionProducerObserver observer) { + + final ServerBootstrap bootstrap = new ServerBootstrap() + .channel(NioServerSocketChannel.class) + .childHandler(new Initializer(observer)) + .childOption(ChannelOption.TCP_NODELAY, true) + .group(new NioEventLoopGroup()); + + port = PORT_ONSET; + while (!available(port)) + port++; + LOGGER.debug("Found open port {}, attempting to bind...", port); + + final ChannelFuture channelFuture = bootstrap.bind(port).awaitUninterruptibly(); + if (!channelFuture.isSuccess()) + LOGGER.error("Failed to bind to port {}", port); + else + LOGGER.debug("Successfully bound to port {}", port); + } + + /** + * The port that the server is bound to. + * + * @return the port number to which the server is bound or -1 if the socket is not bound (yet). + */ + public int getPort() { + return port; + } + + /** + * Checks to see if a specific port is available. + * + * Taken from http://svn.apache.org/viewvc/camel/trunk/components/camel-test/src/main/java/org/apache/camel/test/AvailablePortFinder.java?view=markup#l130 + * + * @param port the port to check for availability + */ + private static boolean available(int port) { + ServerSocket ss = null; + DatagramSocket ds = null; + try { + ss = new ServerSocket(port); + ss.setReuseAddress(true); + ds = new DatagramSocket(port); + ds.setReuseAddress(true); + return true; + } catch (IOException ignored) { + } finally { + if (ds != null) { + ds.close(); + } + + if (ss != null) { + try { + ss.close(); + } catch (IOException e) { + /* should not be thrown */ + } + } + } + + return false; + } + + static class Initializer extends ChannelInitializer { + + private final ExtensionProducerObserver observer; + + public Initializer(ExtensionProducerObserver observer) { + this.observer = observer; + } + + @Override + protected void initChannel(SocketChannel ch) { + ch.pipeline() + .addLast("decoder", new Decoder()) + .addLast("encoder", new Encoder()) + .addLast("handler", new Handler(observer)); + ch.writeAndFlush(new Outgoing.InfoRequest()); + } + } + + static class Decoder extends ByteToMessageDecoder { + + private final static int HEADER_LENGTH = Integer.BYTES; + private final static Logger LOGGER = LoggerFactory.getLogger(Decoder.class); + + private volatile Stage stage = Stage.LENGTH; + private volatile int payloadLength = 0; + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) { + switch (stage) { + case LENGTH: + + if (in.readableBytes() < HEADER_LENGTH) + return; + + payloadLength = in.readInt(); + stage = Stage.PAYLOAD; + break; + case PAYLOAD: + + if (in.readableBytes() < payloadLength) + return; + + try { + + final byte[] data = new byte[HEADER_LENGTH + payloadLength]; + in.readBytes(data, HEADER_LENGTH, payloadLength); + + final HPacket hPacket = new HPacket(data); + hPacket.fixLength(); + + final PacketStructure incomingPacketStructure = NetworkExtensionCodec.getIncomingStructure(hPacket.headerId()); + if (incomingPacketStructure != null) { + final NetworkExtensionMessage message = incomingPacketStructure.getReader().apply(hPacket); + out.add(message); + } else { + LOGGER.error("Did not find decoder for packet {}", hPacket); + } + } catch (Exception e) { + LOGGER.error("Failed to decode message", e); + } finally { + payloadLength = 0; + stage = Stage.LENGTH; + } + break; + } + } + + enum Stage { + LENGTH, + PAYLOAD + } + } + + static class Encoder extends MessageToByteEncoder { + + @Override + protected void encode(ChannelHandlerContext ctx, Outgoing msg, ByteBuf out) { + final PacketStructure structure = NetworkExtensionCodec.getOutgoingStructure(msg); + if (structure == null){ + LOGGER.error("Structure for Outgoing message not defined (msg={})", msg); + return; + } + try { + final HPacket hPacket = new HPacket(structure.getHeaderId()); + final BiConsumer writer = (BiConsumer) structure.getWriter(); + writer.accept(msg, hPacket); + out.writeBytes(hPacket.toBytes()); + } catch (Exception e) { + LOGGER.error("Failed to encode Outgoing message as a HPacket (msg={})", msg, e); + } + } + } + + static class Handler extends ChannelInboundHandlerAdapter { + + private static final AttributeKey CLIENT = AttributeKey.valueOf("client"); + + private final ExtensionProducerObserver observer; + + public Handler(ExtensionProducerObserver observer) { + this.observer = observer; + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + LOGGER.trace("Channel registered (channel={})", ctx.channel()); + super.handlerAdded(ctx); + } + + @Override + public void channelUnregistered(ChannelHandlerContext ctx) throws Exception { + LOGGER.trace("Channel unregistered (channel={})", ctx.channel()); + close(ctx); + super.channelUnregistered(ctx); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + final Channel channel = ctx.channel(); + final Attribute clientAttribute = ctx.attr(CLIENT); + NetworkExtensionClient client = clientAttribute.get(); + if (msg instanceof Incoming.ExtensionInfo) { + if (client != null) + LOGGER.warn("Overriding pre-existing CLIENT for channel (client={}, channel={})", client, channel); + client = new NetworkExtensionClient(channel, (Incoming.ExtensionInfo) msg); + if (NetworkExtensionAuthenticator.evaluate(client)) { + LOGGER.info("Successfully authenticated client {}", client); + clientAttribute.set(client); + observer.onExtensionProduced(client); + } else { + LOGGER.warn("Failed to authenticate client {}, closing connection", client); + client.close(); + } + } + else if (client == null) + LOGGER.error("Client was null, could not handle incoming message {}, expected {} first", msg, Incoming.ExtensionInfo.class); + else if (msg instanceof Incoming) + client.handleIncomingMessage((Incoming) msg); + else + LOGGER.error("Read invalid message type (message={})", msg); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + LOGGER.error("Channel exception caught (channel={}), closing channel", ctx.channel(), cause); + close(ctx); + } + + private void close(ChannelHandlerContext ctx) { + final Optional optionalClient = findClient(ctx); + if (optionalClient.isPresent()) + optionalClient.get().close(); + else + ctx.channel().close(); + } + + private Optional findClient(ChannelHandlerContext ctx) { + return Optional.ofNullable(ctx.attr(CLIENT).get()); + } + } +} diff --git a/G-Earth/src/main/java/gearth/services/extension_handler/extensions/implementations/network/NetworkExtensionsProducer.java b/G-Earth/src/main/java/gearth/services/extension_handler/extensions/implementations/network/NetworkExtensionsProducer.java deleted file mode 100644 index d381e50..0000000 --- a/G-Earth/src/main/java/gearth/services/extension_handler/extensions/implementations/network/NetworkExtensionsProducer.java +++ /dev/null @@ -1,167 +0,0 @@ -package gearth.services.extension_handler.extensions.implementations.network; - -import gearth.protocol.HPacket; -import gearth.services.extension_handler.extensions.extensionproducers.ExtensionProducer; -import gearth.services.extension_handler.extensions.extensionproducers.ExtensionProducerFactory; -import gearth.services.extension_handler.extensions.extensionproducers.ExtensionProducerObserver; -import gearth.services.extension_handler.extensions.implementations.network.authentication.Authenticator; - -import java.io.DataInputStream; -import java.io.IOException; -import java.net.ServerSocket; -import java.net.Socket; - -/** - * Represents an {@link ExtensionProducer} that implements a server to which - * remotely-ran extensions can connect. - * - * @see ExtensionProducerFactory#getAll() for instance creation. - * - * Created by Jonas on 21/06/18. - */ -public final class NetworkExtensionsProducer implements ExtensionProducer { - - /** - * Initial port server tries to listen at, if {@link ServerSocket} creation fails, - * it tries next port. - */ - private static final int PORT_ONSET = 9092; - - /** - * Represents the number of bytes per boolean encoded in an incoming packet. - */ - private static final int BOOLEAN_SIZE = 1; - - /** - * Represents the maximum number of bytes per string encoded in an incoming packet. - */ - private static final int MAX_STRING_SIZE = Character.BYTES * 4_000; - - /** - * Length is encoded as an {@link Integer} and header id as an {@link Short}. - */ - private static final int PACKET_HEADER_SIZE = Integer.BYTES + Short.BYTES; - - /** - * Represents the maximum number of bytes in the body of an incoming packet. - *

- * Used as a form of validation for packets, prevents other Apps that connect - * with the server from sending unexpected data and inexplicably causing huge byte array allocations. - *

- * Since the server only accepts {@link NetworkExtensionInfo.INCOMING_MESSAGES_IDS#EXTENSIONINFO} packets, - * this value is calculated based on that packet. - */ - private static final int MAX_PACKET_BODY_SIZE = (MAX_STRING_SIZE * 6) + (BOOLEAN_SIZE * 4); - - /** - * The port at which the {@link #serverSocket} is listening for incoming connections. - */ - public static int extensionPort = -1; - - private ServerSocket serverSocket; - - @Override - public void startProducing(ExtensionProducerObserver observer) { - - /* - Initialise the serverSocket at the argued port. - */ - int port = PORT_ONSET; - while (!createServer(port)) - ++port; - - /* - Start connection listener thread. - */ - new Thread(() -> { - - try { - - while (!serverSocket.isClosed()) { - - // accept a new connection - final Socket extensionSocket = serverSocket.accept(); - extensionSocket.setTcpNoDelay(true); - - /* - Start client session handler thread. - */ - new Thread(() -> { - - try { - - // write INFOREQUEST packet to client - synchronized (extensionSocket) { - extensionSocket.getOutputStream().write((new HPacket(NetworkExtensionInfo.OUTGOING_MESSAGES_IDS.INFOREQUEST)).toBytes()); - } - - final DataInputStream dIn = new DataInputStream(extensionSocket.getInputStream()); - - // listen to incoming data from client - while (!extensionSocket.isClosed()) { - - final int bodyLength = dIn.readInt() - Short.BYTES; - final short headerId = dIn.readShort(); - - if (headerId == NetworkExtensionInfo.INCOMING_MESSAGES_IDS.EXTENSIONINFO) { - - if (bodyLength > MAX_PACKET_BODY_SIZE) { - System.err.printf("Incoming packet(h=%d, l=%d) exceeds max packet body size %d.\n", headerId, bodyLength, MAX_PACKET_BODY_SIZE); - break; - } - - final HPacket packet = readPacket(dIn, bodyLength, headerId); - - final NetworkExtension gEarthExtension = new NetworkExtension(packet, extensionSocket); - - if (Authenticator.evaluate(gEarthExtension)) - observer.onExtensionProduced(gEarthExtension); - else - gEarthExtension.close(); - - break; - } - } - } catch (IOException ignored) { - } - }).start(); - } - } catch (IOException e) { - e.printStackTrace(); - } - }).start(); - } - - private boolean createServer(int port) { - try { - serverSocket = new ServerSocket(port); - extensionPort = port; - return true; - } catch (IOException e) { - return false; - } - } - - private HPacket readPacket(DataInputStream dIn, int amountToRead, short id) throws IOException { - final byte[] headerAndBody = new byte[amountToRead + PACKET_HEADER_SIZE]; - - int amountRead = 0; - while (amountRead < amountToRead) - amountRead += dIn.read(headerAndBody, amountRead + PACKET_HEADER_SIZE, Math.min(dIn.available(), amountToRead - amountRead)); - - final HPacket packet = new HPacket(headerAndBody); - packet.fixLength(); - packet.replaceShort(4, id); // add header id - - return packet; - } - - /** - * Retrieves the {@link ServerSocket#getLocalPort()} of {@link #serverSocket}. - * - * @return the port number to which {@link #serverSocket} is listening or -1 if the socket is not bound yet. - */ - public int getPort() { - return serverSocket.getLocalPort(); - } -} diff --git a/G-Earth/src/main/java/gearth/services/extension_handler/extensions/implementations/network/authentication/Authenticator.java b/G-Earth/src/main/java/gearth/services/extension_handler/extensions/implementations/network/authentication/Authenticator.java deleted file mode 100644 index 5f6b694..0000000 --- a/G-Earth/src/main/java/gearth/services/extension_handler/extensions/implementations/network/authentication/Authenticator.java +++ /dev/null @@ -1,116 +0,0 @@ -package gearth.services.extension_handler.extensions.implementations.network.authentication; - -import gearth.misc.ConfirmationDialog; -import gearth.services.extension_handler.extensions.implementations.network.NetworkExtension; -import gearth.ui.titlebar.TitleBarController; -import javafx.application.Platform; -import javafx.scene.control.Alert; -import javafx.scene.control.ButtonType; -import javafx.scene.control.Label; - -import java.io.IOException; -import java.util.*; - -/** - * Created by Jonas on 16/10/18. - */ -public class Authenticator { - - private static Map cookies = new HashMap<>(); - private static Set perma_cookies = new HashSet<>(); - - public static String generateCookieForExtension(String filename) { - String cookie = getRandomCookie(); - cookies.put(filename, cookie); - return cookie; - } - public static String generatePermanentCookie() { - String cookie = getRandomCookie(); - perma_cookies.add(cookie); - return cookie; - } - - public static boolean evaluate(NetworkExtension extension) { - if (extension.getCookie() != null && perma_cookies.contains(extension.getCookie())) { - return true; - } - - if (extension.isInstalledExtension()) { - return claimSession(extension.getFileName(), extension.getCookie()); - } - else { - return askForPermission(extension); - } - } - - /** - * authenticator: authenticate an extension and remove the cookie - * @param filename - * @param cookie - * @return if the extension is authenticated - */ - private static boolean claimSession(String filename, String cookie) { - if (cookies.containsKey(filename) && cookies.get(filename).equals(cookie)) { - cookies.remove(filename); - return true; - } - return false; - } - - private static volatile boolean rememberOption = false; - //for not-installed extensions, popup a dialog - private static boolean askForPermission(NetworkExtension extension) { - boolean[] allowConnection = {true}; - - final String connectExtensionKey = "allow_extension_connection"; - - if (ConfirmationDialog.showDialog(connectExtensionKey)) { - boolean[] done = {false}; - Platform.runLater(() -> { - Alert alert = ConfirmationDialog.createAlertWithOptOut(Alert.AlertType.WARNING, connectExtensionKey - ,"Confirmation Dialog", null, - "", "Remember my choice", - ButtonType.YES, ButtonType.NO - ); - - alert.getDialogPane().setContent(new Label("Extension \""+extension.getTitle()+"\" tries to connect but isn't known to G-Earth,\n" + - "accept this connection?")); - - try { - if (!(TitleBarController.create(alert).showAlertAndWait() - .filter(t -> t == ButtonType.YES).isPresent())) { - allowConnection[0] = false; - } - } catch (IOException e) { - e.printStackTrace(); - } - done[0] = true; - if (!ConfirmationDialog.showDialog(connectExtensionKey)) { - rememberOption = allowConnection[0]; - } - }); - - while (!done[0]) { - try { - Thread.sleep(1); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - - return allowConnection[0]; - } - - return rememberOption; - } - - private static String getRandomCookie() { - StringBuilder builder = new StringBuilder(); - Random r = new Random(); - for (int i = 0; i < 40; i++) { - builder.append(r.nextInt(40)); - } - - return builder.toString(); - } -} diff --git a/G-Earth/src/main/java/gearth/services/extension_handler/extensions/implementations/network/executer/ExecutionInfo.java b/G-Earth/src/main/java/gearth/services/extension_handler/extensions/implementations/network/executer/ExecutionInfo.java index 6732de1..c2c7a2d 100644 --- a/G-Earth/src/main/java/gearth/services/extension_handler/extensions/implementations/network/executer/ExecutionInfo.java +++ b/G-Earth/src/main/java/gearth/services/extension_handler/extensions/implementations/network/executer/ExecutionInfo.java @@ -8,39 +8,39 @@ import java.util.Map; /** * Created by Jonas on 22/09/18. */ -public class ExecutionInfo { +public final class ExecutionInfo { - private static Map extensionTypeToExecutionCommand; - public final static List ALLOWEDEXTENSIONTYPES; - public final static String EXTENSIONSDIRECTORY = "Extensions"; + private static final Map EXTENSION_TYPE_TO_EXECUTION_COMMAND; + + public final static List ALLOWED_EXTENSION_TYPES; + public final static String EXTENSIONS_DIRECTORY = "Extensions"; static { - extensionTypeToExecutionCommand = new HashMap<>(); - extensionTypeToExecutionCommand.put("*.jar", new String[]{"java", "-jar", "{path}"}); - extensionTypeToExecutionCommand.put("*.py", new String[]{"python", "{path}"}); - extensionTypeToExecutionCommand.put("*.py3", new String[]{"python3", "{path}"}); - extensionTypeToExecutionCommand.put("*.sh", new String[]{"{path}"}); - extensionTypeToExecutionCommand.put("*.exe", new String[]{"{path}"}); - extensionTypeToExecutionCommand.put("*.js", new String[]{"node", "{path}"}); - String[] extraArgs = {"-p", "{port}", "-f", "{filename}", "-c", "{cookie}"}; - for(String type : extensionTypeToExecutionCommand.keySet()) { - String[] commandShort = extensionTypeToExecutionCommand.get(type); - String[] combined = new String[extraArgs.length + commandShort.length]; + EXTENSION_TYPE_TO_EXECUTION_COMMAND = new HashMap<>(); + EXTENSION_TYPE_TO_EXECUTION_COMMAND.put("*.jar", new String[]{"java", "-jar", "{path}"}); + EXTENSION_TYPE_TO_EXECUTION_COMMAND.put("*.py", new String[]{"python", "{path}"}); + EXTENSION_TYPE_TO_EXECUTION_COMMAND.put("*.py3", new String[]{"python3", "{path}"}); + EXTENSION_TYPE_TO_EXECUTION_COMMAND.put("*.sh", new String[]{"{path}"}); + EXTENSION_TYPE_TO_EXECUTION_COMMAND.put("*.exe", new String[]{"{path}"}); + EXTENSION_TYPE_TO_EXECUTION_COMMAND.put("*.js", new String[]{"node", "{path}"}); + + final String[] extraArgs = {"-p", "{port}", "-f", "{filename}", "-c", "{cookie}"}; + + for(String type : EXTENSION_TYPE_TO_EXECUTION_COMMAND.keySet()) { + + final String[] commandShort = EXTENSION_TYPE_TO_EXECUTION_COMMAND.get(type); + final String[] combined = new String[extraArgs.length + commandShort.length]; System.arraycopy(commandShort, 0, combined, 0, commandShort.length); System.arraycopy(extraArgs, 0, combined, commandShort.length, extraArgs.length); - extensionTypeToExecutionCommand.put( - type, - combined - ); + EXTENSION_TYPE_TO_EXECUTION_COMMAND.put(type, combined); } - ALLOWEDEXTENSIONTYPES = new ArrayList<>(extensionTypeToExecutionCommand.keySet()); + ALLOWED_EXTENSION_TYPES = new ArrayList<>(EXTENSION_TYPE_TO_EXECUTION_COMMAND.keySet()); } public static String[] getExecutionCommand(String type) { - return extensionTypeToExecutionCommand.get(type); + return EXTENSION_TYPE_TO_EXECUTION_COMMAND.get(type); } - } diff --git a/G-Earth/src/main/java/gearth/services/extension_handler/extensions/implementations/network/executer/ExtensionRunnerFactory.java b/G-Earth/src/main/java/gearth/services/extension_handler/extensions/implementations/network/executer/ExtensionRunnerFactory.java index fd26dcb..2b569c2 100644 --- a/G-Earth/src/main/java/gearth/services/extension_handler/extensions/implementations/network/executer/ExtensionRunnerFactory.java +++ b/G-Earth/src/main/java/gearth/services/extension_handler/extensions/implementations/network/executer/ExtensionRunnerFactory.java @@ -3,9 +3,9 @@ package gearth.services.extension_handler.extensions.implementations.network.exe /** * Created by Jonas on 22/09/18. */ -public class ExtensionRunnerFactory { +public final class ExtensionRunnerFactory { - private static ExtensionRunner runner = new NormalExtensionRunner(); + private static final ExtensionRunner runner = new NormalExtensionRunner(); public static ExtensionRunner get() { return runner; diff --git a/G-Earth/src/main/java/gearth/services/extension_handler/extensions/implementations/network/executer/NormalExtensionRunner.java b/G-Earth/src/main/java/gearth/services/extension_handler/extensions/implementations/network/executer/NormalExtensionRunner.java index ba8bc29..4f3a113 100644 --- a/G-Earth/src/main/java/gearth/services/extension_handler/extensions/implementations/network/executer/NormalExtensionRunner.java +++ b/G-Earth/src/main/java/gearth/services/extension_handler/extensions/implementations/network/executer/NormalExtensionRunner.java @@ -1,207 +1,198 @@ package gearth.services.extension_handler.extensions.implementations.network.executer; import gearth.GEarth; -import gearth.services.extension_handler.extensions.implementations.network.authentication.Authenticator; +import gearth.services.extension_handler.extensions.implementations.network.NetworkExtensionAuthenticator; import gearth.services.internal_extensions.extensionstore.tools.StoreExtensionTools; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.net.URISyntaxException; -import java.nio.file.*; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Arrays; import java.util.Random; /** * Created by Jonas on 22/09/18. */ -public class NormalExtensionRunner implements ExtensionRunner { +public final class NormalExtensionRunner implements ExtensionRunner { - public static final String JARPATH; + private final static Logger LOGGER = LoggerFactory.getLogger(NormalExtensionRunner.class); + + public static final String JAR_PATH; static { + final URL url = getLocation(); String value; try { - value = new File(GEarth.class.getProtectionDomain().getCodeSource().getLocation().toURI()).getParent(); + value = new File(url.toURI()).getParent(); } catch (URISyntaxException e) { - value = new File(GEarth.class.getProtectionDomain().getCodeSource().getLocation().getPath()).getParent(); - e.printStackTrace(); + value = new File(url.getPath()).getParent(); + LOGGER.warn("Failed to load JAR_PATH from url {} as URI, using Path instead", url, e); } - JARPATH = value; + JAR_PATH = value; + LOGGER.debug("Set JAR_PATH as {}", JAR_PATH); } @Override public void runAllExtensions(int port) { - if (dirExists(ExecutionInfo.EXTENSIONSDIRECTORY)){ - File folder = - new File(JARPATH + - FileSystems.getDefault().getSeparator()+ - ExecutionInfo.EXTENSIONSDIRECTORY); - File[] childs = folder.listFiles(); - for (File file : childs) { - tryRunExtension(file.getPath(), port); + if (dirExists(ExecutionInfo.EXTENSIONS_DIRECTORY)) { + + final File extensionsDirectory = Paths.get(JAR_PATH, ExecutionInfo.EXTENSIONS_DIRECTORY).toFile(); + final File[] extensionFiles = extensionsDirectory.listFiles(); + + if (extensionFiles == null) { + LOGGER.error("Provided extensionsDirectory does not exist (extensionsDirectory={})", extensionsDirectory); + return; } - } + + for (File file : extensionFiles) + tryRunExtension(file.getPath(), port); + } else + LOGGER.warn("Did not run extensions because extensions directory does not exist at {}", ExecutionInfo.EXTENSIONS_DIRECTORY); } @Override - public void installAndRunExtension(String path, int port) { - if (!dirExists(ExecutionInfo.EXTENSIONSDIRECTORY)) { - createDirectory(ExecutionInfo.EXTENSIONSDIRECTORY); - } + public void installAndRunExtension(String stringPath, int port) { + if (!dirExists(ExecutionInfo.EXTENSIONS_DIRECTORY)) + tryCreateDirectory(ExecutionInfo.EXTENSIONS_DIRECTORY); - String name = Paths.get(path).getFileName().toString(); - String[] split = name.split("\\."); - String ext = "*." + split[split.length - 1]; + final Path path = Paths.get(stringPath); + final String name = path.getFileName().toString(); + final String[] split = name.split("\\."); + final String ext = "*." + split[split.length - 1]; - String realname = String.join(".",Arrays.copyOf(split, split.length-1)); - String newname = realname + "-" + getRandomString() + ext.substring(1); + final String realName = String.join(".", Arrays.copyOf(split, split.length - 1)); + final String newName = realName + "-" + getRandomString() + ext.substring(1); - Path originalPath = Paths.get(path); - Path newPath = Paths.get( - JARPATH, - ExecutionInfo.EXTENSIONSDIRECTORY, - newname - ); + final Path newPath = Paths.get(JAR_PATH, ExecutionInfo.EXTENSIONS_DIRECTORY, newName); try { - Files.copy( - originalPath, - newPath - ); -// addExecPermission(newPath.toString()); + Files.copy(path, newPath); + tryRunExtension(newPath.toString(), port); } catch (IOException e) { e.printStackTrace(); } - - } public void tryRunExtension(String path, int port) { try { + if (new File(path).isDirectory()) { - // this extension is installed from the extension store and requires - // different behavior + // this extension is installed from the extension store and requires different behavior StoreExtensionTools.executeExtension(path, port); return; } - String filename = Paths.get(path).getFileName().toString(); - - String[] execCommand = ExecutionInfo.getExecutionCommand(getFileExtension(path)); - execCommand = Arrays.copyOf(execCommand, execCommand.length); - String cookie = Authenticator.generateCookieForExtension(filename); + final String filename = Paths.get(path).getFileName().toString(); + final String[] execCommand = ExecutionInfo + .getExecutionCommand(getFileExtension(path)) + .clone(); + final String cookie = NetworkExtensionAuthenticator.generateCookieForExtension(filename); for (int i = 0; i < execCommand.length; i++) { execCommand[i] = execCommand[i] .replace("{path}", path) - .replace("{port}", port+"") + .replace("{port}", port + "") .replace("{filename}", filename) .replace("{cookie}", cookie); } - ProcessBuilder pb = new ProcessBuilder(execCommand); -// Process proc = Runtime.getRuntime().exec(execCommand); - Process proc = pb.start(); - maybeLogExtension(path, proc); + final ProcessBuilder processBuilder = new ProcessBuilder(execCommand); + final Process process = processBuilder.start(); + + maybeLogExtension(path, process); + } catch (IOException e) { - e.printStackTrace(); + LOGGER.error("Failed to run extension at path {} using port {}", path, port, e); } } - public static void maybeLogExtension(String path, Process proc) { + + public static void maybeLogExtension(String path, Process process) { if (GEarth.hasFlag(ExtensionRunner.SHOW_EXTENSIONS_LOG)) { - String sep = "" + System.lineSeparator(); - synchronized (System.out) { - System.out.println(path + sep + "Launching" + sep + "----------" + sep); - } - BufferedReader stdInput = new BufferedReader(new - InputStreamReader(proc.getInputStream())); + final Logger logger = LoggerFactory.getLogger(path); + logger.info("Launching..."); + + final BufferedReader processInputReader = new BufferedReader(new InputStreamReader(process.getInputStream())); new Thread(() -> { try { String line; - while((line = stdInput.readLine()) != null) { - synchronized (System.out) { - System.out.println(path + sep + "Output" + sep + line + sep + "----------" + sep); - } - } + while ((line = processInputReader.readLine()) != null) + logger.info(line); } catch (IOException e) { - e.printStackTrace(); + LOGGER.error("Failed to read input line from process {}", process, e); } - }).start(); - - BufferedReader stdError = new BufferedReader(new - InputStreamReader(proc.getErrorStream())); + }, path+"-input").start(); + final BufferedReader processErrorReader = new BufferedReader(new InputStreamReader(process.getErrorStream())); new Thread(() -> { try { String line; - while((line = stdError.readLine()) != null) { - synchronized (System.out) { - System.out.println(path + sep + "Error" + sep + line + sep + "----------" + sep); - } - } + while ((line = processErrorReader.readLine()) != null) + logger.error(line); } catch (IOException e) { - e.printStackTrace(); + LOGGER.error("Failed to read error line from process {}", process, e); } }).start(); - } } @Override public void uninstallExtension(String filename) { try { - Path path = Paths.get(JARPATH, ExecutionInfo.EXTENSIONSDIRECTORY, filename); + final Path path = Paths.get(JAR_PATH, ExecutionInfo.EXTENSIONS_DIRECTORY, filename); if (new File(path.toString()).isDirectory()) { // is installed through extension store StoreExtensionTools.removeExtension(path.toString()); - } - else { + } else Files.delete(path); - } - } catch (IOException e) { - e.printStackTrace(); + LOGGER.error("Failed to uninstall extension at {}", filename, e); } } -// private void addExecPermission(String path) { -// //not needed at first sight -// } + private static void tryCreateDirectory(String path) { + if (!dirExists(path)) { + try { + Files.createDirectories(Paths.get(JAR_PATH, path)); + } catch (IOException e) { + LOGGER.error("Failed to create directory at {}", path, e); + } + } + } - private String getFileExtension(String path) { - String name = Paths.get(path).getFileName().toString(); - String[] split = name.split("\\."); + private static boolean dirExists(String dir) { + return Files.isDirectory(Paths.get(JAR_PATH, dir)); + } + + private static URL getLocation() { + return GEarth.class.getProtectionDomain().getCodeSource().getLocation(); + } + + private static String getFileExtension(String path) { + final String name = Paths.get(path).getFileName().toString(); + final String[] split = name.split("\\."); return "*." + split[split.length - 1]; } - private boolean dirExists(String dir) { - return Files.isDirectory(Paths.get(JARPATH, dir)); - } - private void createDirectory(String dir) { - if (!dirExists(dir)) { - try { - Files.createDirectories(Paths.get(JARPATH, dir)); - } catch (IOException e) { - e.printStackTrace(); - } - } - } - private String getRandomString() { - StringBuilder builder = new StringBuilder(); - Random r = new Random(); - for (int i = 0; i < 12; i++) { - builder.append(r.nextInt(10)); - } - + private static String getRandomString() { + final StringBuilder builder = new StringBuilder(); + final Random random = new Random(); + for (int i = 0; i < 12; i++) + builder.append(random.nextInt(10)); return builder.toString(); } } diff --git a/G-Earth/src/main/java/gearth/services/g_python/GPythonShell.java b/G-Earth/src/main/java/gearth/services/g_python/GPythonShell.java index da92ac6..080ddb9 100644 --- a/G-Earth/src/main/java/gearth/services/g_python/GPythonShell.java +++ b/G-Earth/src/main/java/gearth/services/g_python/GPythonShell.java @@ -3,6 +3,7 @@ package gearth.services.g_python; import gearth.GEarth; import gearth.ui.subforms.extra.ExtraController; import gearth.ui.titlebar.TitleBarController; +import gearth.ui.translations.LanguageBundle; import javafx.application.Platform; import javafx.scene.control.Alert; import javafx.scene.control.ButtonType; @@ -170,13 +171,11 @@ public class GPythonShell { private void showError() { Platform.runLater(() -> { - Alert alert = new Alert(Alert.AlertType.ERROR, "G-Python error", ButtonType.OK); - alert.setTitle("G-Python error"); + Alert alert = new Alert(Alert.AlertType.ERROR, LanguageBundle.get("alert.gpythonerror.title"), ButtonType.OK); + alert.setTitle(LanguageBundle.get("alert.gpythonerror.title")); FlowPane fp = new FlowPane(); - Label lbl = new Label("Something went wrong launching the G-Python shell," + - System.lineSeparator() + "are you sure you followed the installation guide correctly?" + - System.lineSeparator() + System.lineSeparator() + "More information here:"); + Label lbl = new Label(LanguageBundle.get("alert.gpythonerror.content").replaceAll("\\\\n", System.lineSeparator())); Hyperlink link = new Hyperlink(ExtraController.INFO_URL_GPYTHON); fp.getChildren().addAll(lbl, link); link.setOnAction(event -> { diff --git a/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/GExtensionStoreController.java b/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/GExtensionStoreController.java index 16f12da..4afa23d 100644 --- a/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/GExtensionStoreController.java +++ b/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/GExtensionStoreController.java @@ -23,6 +23,7 @@ import java.util.*; import java.util.function.Supplier; public class GExtensionStoreController implements Initializable { + private static GExtensionStoreController instance; private GExtensionStore extensionStore = null; @@ -37,6 +38,8 @@ public class GExtensionStoreController implements Initializable { @Override public void initialize(URL location, ResourceBundle resources) { + instance = this; + webView = new WebView(); borderPane.setCenter(webView); @@ -106,6 +109,7 @@ public class GExtensionStoreController implements Initializable { }); webView.getEngine().load(GExtensionStoreController.class.getResource("webview/index.html").toString()); + } @@ -236,4 +240,8 @@ public class GExtensionStoreController implements Initializable { public String getContentItemsContainer() { return contentItemsContainer; } + + public static void reloadPage() { + instance.webView.getEngine().reload(); + } } diff --git a/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/WebUtils.java b/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/WebUtils.java index 7565b34..7e45109 100644 --- a/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/WebUtils.java +++ b/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/WebUtils.java @@ -1,5 +1,7 @@ package gearth.services.internal_extensions.extensionstore.application; +import gearth.GEarth; +import gearth.ui.translations.LanguageBundle; import org.w3c.dom.Element; import java.time.LocalDateTime; @@ -43,19 +45,19 @@ public class WebUtils { } public static String elapsedTime(long time) { - if (time < 60) return time + (time == 1 ? " second" : " seconds"); + if (time < 60) return time + " " + LanguageBundle.get("ext.store.elapsedtime.second." + (time == 1 ? "single": "multiple")); time = time/60; - if (time < 60) return time + (time == 1 ? " minute" : " minutes"); + if (time < 60) return time + " " + LanguageBundle.get("ext.store.elapsedtime.minute." + (time == 1 ? "single": "multiple")); time = time/60; - if (time < 24) return time + (time == 1 ? " hour" : " hours"); + if (time < 24) return time + " " + LanguageBundle.get("ext.store.elapsedtime.hour." + (time == 1 ? "single": "multiple")); long days = time/24; - if (days < 7) return days + (days == 1 ? " day" : " days"); + if (days < 7) return days + " " + LanguageBundle.get("ext.store.elapsedtime.day." + (days == 1 ? "single": "multiple")); long weeks = days/7; - if (weeks < 6) return weeks + (weeks == 1 ? " week" : " weeks"); + if (weeks < 6) return weeks + " " + LanguageBundle.get("ext.store.elapsedtime.week." + (weeks == 1 ? "single": "multiple")); long months = days/31; - if (months < 12) return months + (months == 1 ? " month" : " months"); + if (months < 12) return months + " " + LanguageBundle.get("ext.store.elapsedtime.month." + (months == 1 ? "single": "multiple")); long years = days/365; - return years + (years == 1 ? " year" : " years"); + return years + " " + LanguageBundle.get("ext.store.elapsedtime.year." + (years == 1 ? "single": "multiple")); } public static String escapeMessage(String text) { diff --git a/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/StoreExtensionItem.java b/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/StoreExtensionItem.java index a32516e..d859bbc 100644 --- a/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/StoreExtensionItem.java +++ b/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/StoreExtensionItem.java @@ -1,11 +1,16 @@ package gearth.services.internal_extensions.extensionstore.application.entities; +import gearth.GEarth; +import gearth.protocol.HMessage; +import gearth.protocol.HPacket; import gearth.services.internal_extensions.extensionstore.GExtensionStore; import gearth.services.internal_extensions.extensionstore.application.GExtensionStoreController; import gearth.services.internal_extensions.extensionstore.application.WebUtils; import gearth.services.internal_extensions.extensionstore.application.entities.extensiondetails.StoreExtensionDetailsOverview; +import gearth.services.internal_extensions.extensionstore.application.entities.queriedoverviews.CategorizedOverview; import gearth.services.internal_extensions.extensionstore.repository.StoreRepository; import gearth.services.internal_extensions.extensionstore.repository.models.StoreExtension; +import gearth.ui.translations.LanguageBundle; import netscape.javascript.JSObject; public class StoreExtensionItem implements ContentItem { @@ -22,7 +27,7 @@ public class StoreExtensionItem implements ContentItem { new StoreExtensionDetailsOverview( gExtensionStore.getController().getCurrentOverview(), 0, - GExtensionStore.MAX_PAGES, + GExtensionStore.PAGESIZE, storeExtension, gExtensionStore.getRepository() ) @@ -53,12 +58,12 @@ public class StoreExtensionItem implements ContentItem { .append("

") .append("
").append(WebUtils.escapeMessage(storeExtension.getTitle())).append("
") - .append("
By ").append(storeExtension.getAuthors().get(0).getName()).append(", last updated ").append(WebUtils.elapsedSince(storeExtension.getUpdateDate())).append(" ago
") + .append("
").append(String.format(LanguageBundle.get("ext.store.extension.madeby"), storeExtension.getAuthors().get(0).getName())).append(", ").append(String.format(LanguageBundle.get("ext.store.extension.lastupdated"), WebUtils.elapsedSince(storeExtension.getUpdateDate()))).append("
") .append("
") .append("
") - .append("
").append("Version: ").append(displayVersion()).append("
") - .append("
").append("Rating: ").append(storeExtension.getRating()).append("
") + .append("
").append(LanguageBundle.get("ext.store.extension.version")).append(": ").append(displayVersion()).append("
") + .append("
").append(LanguageBundle.get("ext.store.extension.rating")).append(": ").append(storeExtension.getRating()).append("
") // .append("
").append(storeExtension.getFramework().getFramework().getName().replace("Native", "")).append("
") .append("
") diff --git a/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/categories/CategoryItem.java b/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/categories/CategoryItem.java index 22af532..d7757ac 100644 --- a/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/categories/CategoryItem.java +++ b/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/categories/CategoryItem.java @@ -8,6 +8,7 @@ import gearth.services.internal_extensions.extensionstore.application.entities.q import gearth.services.internal_extensions.extensionstore.repository.StoreRepository; import gearth.services.internal_extensions.extensionstore.repository.models.ExtCategory; import gearth.services.internal_extensions.extensionstore.repository.querying.ExtensionOrdering; +import gearth.ui.translations.LanguageBundle; import netscape.javascript.JSObject; import java.util.Collections; @@ -57,7 +58,7 @@ public class CategoryItem implements ContentItem { .append("") .append("
") - .append("
").append(releasesCount).append(" releases").append("
") + .append("
").append(releasesCount).append(" ").append(LanguageBundle.get("ext.store.extension.author.releases")).append("
") // .append("
").append(storeExtension.getFramework().getFramework().getName().replace("Native", "")).append("
") .append("
") diff --git a/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/categories/CategoryOverview.java b/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/categories/CategoryOverview.java index c773b4f..4222c75 100644 --- a/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/categories/CategoryOverview.java +++ b/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/categories/CategoryOverview.java @@ -1,10 +1,12 @@ package gearth.services.internal_extensions.extensionstore.application.entities.categories; +import gearth.GEarth; import gearth.services.internal_extensions.extensionstore.GExtensionStore; import gearth.services.internal_extensions.extensionstore.application.entities.ContentItem; import gearth.services.internal_extensions.extensionstore.application.entities.HOverview; import gearth.services.internal_extensions.extensionstore.repository.StoreRepository; import gearth.services.internal_extensions.extensionstore.repository.models.ExtCategory; +import gearth.ui.translations.LanguageBundle; import java.util.List; import java.util.stream.Collectors; @@ -57,17 +59,17 @@ public class CategoryOverview extends HOverview { @Override public String title() { - return "Categories"; + return LanguageBundle.get("ext.store.categories.title"); } @Override public String description() { - return "Explore the different kinds of extensions G-Earth has to offer"; + return LanguageBundle.get("ext.store.categories.description"); } @Override public String contentTitle() { - return "Categories"; + return LanguageBundle.get("ext.store.categories.contenttitle"); } }; } diff --git a/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/extensiondetails/StoreExtensionDetailsItem.java b/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/extensiondetails/StoreExtensionDetailsItem.java index e4a335e..3668be0 100644 --- a/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/extensiondetails/StoreExtensionDetailsItem.java +++ b/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/extensiondetails/StoreExtensionDetailsItem.java @@ -8,6 +8,7 @@ import gearth.services.internal_extensions.extensionstore.application.entities.H import gearth.services.internal_extensions.extensionstore.repository.models.ExtCategory; import gearth.services.internal_extensions.extensionstore.repository.models.StoreExtension; import gearth.services.internal_extensions.extensionstore.tools.EncodingUtil; +import gearth.ui.translations.LanguageBundle; import netscape.javascript.JSObject; import org.apache.commons.io.IOUtils; import org.json.JSONObject; @@ -45,32 +46,32 @@ public class StoreExtensionDetailsItem implements ContentItem { contentBuilder .append(String.format("*%s*", storeExtension.getTitle())).append(" - v").append(storeExtension.getVersion()).append("\n\n") - .append("*Description*\n").append(descriptionQuoted).append("\n \n") - .append("*Author(s):* ").append(storeExtension.getAuthors().stream().map(StoreExtension.Author::getName).collect(Collectors.joining(", "))).append("\n\n") - .append("*Categories:* ").append(storeExtension.getCategories().stream().map(ExtCategory::getName).collect(Collectors.joining(", "))).append("\n\n"); + .append("*").append(LanguageBundle.get("ext.store.extension.details.description")).append(":*\n").append(descriptionQuoted).append("\n \n") + .append("*").append(LanguageBundle.get("ext.store.extension.details.authors")).append(":* ").append(storeExtension.getAuthors().stream().map(StoreExtension.Author::getName).collect(Collectors.joining(", "))).append("\n\n") + .append("*").append(LanguageBundle.get("ext.store.extension.details.categories")).append(":* ").append(storeExtension.getCategories().stream().map(ExtCategory::getName).collect(Collectors.joining(", "))).append("\n\n"); - contentBuilder.append("*Technical information*").append("\n"); + contentBuilder.append("*").append(LanguageBundle.get("ext.store.extension.details.technical_information")).append("*").append("\n"); if(storeExtension.getReleases() != null) - contentBuilder.append("> Releases: --url:Click Here-").append(storeExtension.getReleases()).append("\n"); + contentBuilder.append("> ").append(LanguageBundle.get("ext.store.extension.details.releases")).append(": --url:").append(LanguageBundle.get("ext.store.extension.details.click_here")).append("-").append(storeExtension.getReleases()).append("\n"); - contentBuilder.append("> Language: ").append(storeExtension.getLanguage()).append("\n") - .append("> Source: --url:Click Here-").append(storeExtension.getSource()).append("\n") - .append("> Framework: ").append(storeExtension.getFramework().getFramework().getName()).append(" - v").append(storeExtension.getFramework().getVersion()).append("\n") - .append("> Systems: ").append(String.join(", ", storeExtension.getCompatibility().getSystems())).append("\n \n"); + contentBuilder.append("> ").append(LanguageBundle.get("ext.store.extension.details.language")).append(": ").append(storeExtension.getLanguage()).append("\n") + .append("> ").append(LanguageBundle.get("ext.store.extension.details.source")).append(": --url:").append(LanguageBundle.get("ext.store.extension.details.click_here")).append("-").append(storeExtension.getSource()).append("\n") + .append("> ").append(LanguageBundle.get("ext.store.extension.details.framework")).append(": ").append(storeExtension.getFramework().getFramework().getName()).append(" - v").append(storeExtension.getFramework().getVersion()).append("\n") + .append("> ").append(LanguageBundle.get("ext.store.extension.details.systems")).append(": ").append(String.join(", ", storeExtension.getCompatibility().getSystems())).append("\n \n"); - contentBuilder.append("*Compatible clients:* ").append(String.join(", ", storeExtension.getCompatibility().getClients())).append("\n\n"); + contentBuilder.append("*").append(LanguageBundle.get("ext.store.extension.details.clients")).append(":* ").append(String.join(", ", storeExtension.getCompatibility().getClients())).append("\n\n"); if (storeExtension.getFramework().getFramework().isInstallationRequired()) { - contentBuilder.append("Warning: the framework requires --url:additional installations-") + contentBuilder.append(LanguageBundle.get("ext.store.extension.warning.requirement")) .append(storeExtension.getFramework().getFramework().getInstallationInstructions()).append(" !\n\n"); } if (!storeExtension.isStable()) { - contentBuilder.append("Warning: this extension has been marked unstable!\n\n"); + contentBuilder.append(LanguageBundle.get("ext.store.extension.warning.unstable")).append("\n\n"); } contentBuilder.append("--startdiv--") - .append("\n*Screenshot: *").append("\n") + .append("\n*").append(LanguageBundle.get("ext.store.extension.details.screenshot")).append(": *").append("\n") .append("--img:").append(gExtensionStore.getRepository().getResourceUrl("store", "extensions", storeExtension.getTitle(), "screenshot.png")) .append(" --enddiv--"); @@ -151,8 +152,8 @@ public class StoreExtensionDetailsItem implements ContentItem { .append("
") .append("
") .append("
").append(WebUtils.escapeMessage(mainAuthor.getName())).append("
") - .append("
").append(mainAuthor.getReputation()).append(" reputation
") - .append("
").append(mainAuthor.getExtensionsCount()).append(" releases
") + .append("
").append(mainAuthor.getReputation()).append(" ").append(LanguageBundle.get("ext.store.extension.author.reputation")).append("
") + .append("
").append(mainAuthor.getExtensionsCount()).append(" ").append(LanguageBundle.get("ext.store.extension.author.releases")).append("
") .append("
\"\"
") // todo look .append("
") .append("
").append(contentsInHtml()).append("
") diff --git a/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/extensiondetails/StoreExtensionDetailsOverview.java b/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/extensiondetails/StoreExtensionDetailsOverview.java index 7c06d56..7a98b65 100644 --- a/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/extensiondetails/StoreExtensionDetailsOverview.java +++ b/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/extensiondetails/StoreExtensionDetailsOverview.java @@ -1,7 +1,6 @@ package gearth.services.internal_extensions.extensionstore.application.entities.extensiondetails; -import gearth.services.extension_handler.extensions.implementations.network.NetworkExtensionsProducer; -import gearth.services.extension_handler.extensions.implementations.network.executer.NormalExtensionRunner; +import gearth.services.extension_handler.extensions.extensionproducers.ExtensionProducerFactory; import gearth.services.internal_extensions.extensionstore.GExtensionStore; import gearth.services.internal_extensions.extensionstore.application.GExtensionStoreController; import gearth.services.internal_extensions.extensionstore.application.WebUtils; @@ -12,15 +11,14 @@ import gearth.services.internal_extensions.extensionstore.repository.models.Stor import gearth.services.internal_extensions.extensionstore.tools.InstalledExtension; import gearth.services.internal_extensions.extensionstore.tools.StoreExtensionTools; import gearth.ui.titlebar.TitleBarController; +import gearth.ui.translations.LanguageBundle; import javafx.application.Platform; import javafx.scene.control.Alert; -import javafx.scene.control.ButtonType; import org.apache.maven.artifact.versioning.ComparableVersion; import org.w3c.dom.Element; import java.io.IOException; import java.util.Collections; -import java.util.Comparator; import java.util.List; import java.util.Optional; @@ -62,7 +60,7 @@ public class StoreExtensionDetailsOverview extends HOverview { public String buttonText() { int mode = mode(); // return mode == 2 ? "Update" : "Install"; - return mode == 0 ? "Install" : (mode == 1 ? "Installed" : "Update"); + return LanguageBundle.get("ext.store.button." + (mode == 0 ? "install" : (mode == 1 ? "installed" : "update"))); } @Override @@ -91,23 +89,23 @@ public class StoreExtensionDetailsOverview extends HOverview { private void awaitPopup(String mode) { popup(Alert.AlertType.WARNING, - String.format("%s extension", mode), - String.format("%s extension [%s]", mode, extension.getTitle()), - String.format("Press \"OK\" and wait while the extension is being %sed", mode.toLowerCase())); + LanguageBundle.get(String.format("ext.store.extension.status.await.%s", mode)), + String.format("%s [%s]", LanguageBundle.get(String.format("ext.store.extension.status.await.%s", mode)), extension.getTitle()), + LanguageBundle.get(String.format("ext.store.extension.status.await.%s.message", mode))); } private void successPopup(String mode) { popup(Alert.AlertType.INFORMATION, - String.format("%s completed", mode), - String.format("%s completed [%s]", mode, extension.getTitle()), - String.format("Extension %s completed successfully", mode.toLowerCase())); + LanguageBundle.get(String.format("ext.store.extension.status.success.%s", mode)), + String.format("%s [%s]", LanguageBundle.get(String.format("ext.store.extension.status.success.%s", mode)), extension.getTitle()), + LanguageBundle.get(String.format("ext.store.extension.status.success.%s.message", mode))); } private void errorPopup(String mode, String error) { popup(Alert.AlertType.ERROR, - String.format("%s failed", mode), - String.format("%s failed [%s]", mode, extension.getTitle()), - String.format("%s failed with the following message: %s", mode, error)); + LanguageBundle.get(String.format("ext.store.extension.status.success.%s", mode)), + String.format("%s [%s]", LanguageBundle.get(String.format("ext.store.extension.status.success.%s", mode)), extension.getTitle()), + String.format("%s: %s", LanguageBundle.get(String.format("ext.store.extension.status.success.%s.message", mode)), error)); } private void popup(Alert.AlertType alertType, String title, String header, String context) { @@ -128,14 +126,14 @@ public class StoreExtensionDetailsOverview extends HOverview { int mode = mode(); if (mode == 1) return; - String modeString = mode() == 0 ? "Install" : "Update"; + String modeString = mode() == 0 ? "install" : "update"; HOverview selff = this; StoreExtensionTools.InstallExtListener listener = new StoreExtensionTools.InstallExtListener() { @Override public void success(String installationFolder) { Platform.runLater(() -> successPopup(modeString)); - StoreExtensionTools.executeExtension(installationFolder, NetworkExtensionsProducer.extensionPort); + StoreExtensionTools.executeExtension(installationFolder, ExtensionProducerFactory.getExtensionServer().getPort()); } @Override diff --git a/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/installed/InstalledOverview.java b/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/installed/InstalledOverview.java index b64d1c2..4d0b10e 100644 --- a/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/installed/InstalledOverview.java +++ b/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/installed/InstalledOverview.java @@ -7,6 +7,7 @@ import gearth.services.internal_extensions.extensionstore.repository.StoreReposi import gearth.services.internal_extensions.extensionstore.repository.models.StoreExtension; import gearth.services.internal_extensions.extensionstore.tools.InstalledExtension; import gearth.services.internal_extensions.extensionstore.tools.StoreExtensionTools; +import gearth.ui.translations.LanguageBundle; import org.apache.maven.artifact.versioning.ComparableVersion; import java.awt.*; @@ -30,7 +31,7 @@ public class InstalledOverview extends HOverview { @Override public String buttonText() { - return "Open folder"; + return LanguageBundle.get("ext.store.overview.folder"); } @Override @@ -75,17 +76,17 @@ public class InstalledOverview extends HOverview { @Override public String title() { - return "Installed Extensions"; + return LanguageBundle.get("ext.store.overview.title"); } @Override public String description() { - return "Extensions that are already installed into G-Earth"; + return LanguageBundle.get("ext.store.overview.description"); } @Override public String contentTitle() { - return "Installed extensions"; + return LanguageBundle.get("ext.store.overview.contenttitle"); } }; } diff --git a/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/installed/StoreExtensionInstalledItem.java b/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/installed/StoreExtensionInstalledItem.java index f5d6cda..84cb3ff 100644 --- a/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/installed/StoreExtensionInstalledItem.java +++ b/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/installed/StoreExtensionInstalledItem.java @@ -6,6 +6,7 @@ import gearth.services.internal_extensions.extensionstore.application.WebUtils; import gearth.services.internal_extensions.extensionstore.application.entities.StoreExtensionItem; import gearth.services.internal_extensions.extensionstore.repository.models.StoreExtension; import gearth.services.internal_extensions.extensionstore.tools.InstalledExtension; +import gearth.ui.translations.LanguageBundle; import org.apache.maven.artifact.versioning.ComparableVersion; public class StoreExtensionInstalledItem extends StoreExtensionItem { @@ -55,11 +56,11 @@ public class StoreExtensionInstalledItem extends StoreExtensionItem { .append("
") .append("
").append(WebUtils.escapeMessage(installedExtension.getName())).append("
") - .append("
Not found in G-ExtensionStore
") + .append("
").append(LanguageBundle.get("ext.store.extension.notinstore")).append("
") .append("
") .append("
") - .append("
").append("Version: ").append(displayVersion()).append("
") + .append("
").append(LanguageBundle.get("ext.store.extension.version")).append(": ").append(displayVersion()).append("
") .append("
") .append("
") diff --git a/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/queriedoverviews/ByDateOverview.java b/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/queriedoverviews/ByDateOverview.java index 1ea07db..acd0906 100644 --- a/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/queriedoverviews/ByDateOverview.java +++ b/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/queriedoverviews/ByDateOverview.java @@ -1,11 +1,11 @@ package gearth.services.internal_extensions.extensionstore.application.entities.queriedoverviews; import gearth.misc.OSValidator; -import gearth.services.internal_extensions.extensionstore.application.GExtensionStoreController; import gearth.services.internal_extensions.extensionstore.application.entities.HOverview; import gearth.services.internal_extensions.extensionstore.repository.StoreRepository; import gearth.services.internal_extensions.extensionstore.repository.models.StoreExtension; import gearth.services.internal_extensions.extensionstore.repository.querying.ExtensionOrdering; +import gearth.ui.translations.LanguageBundle; import java.util.Collections; import java.util.List; @@ -32,17 +32,17 @@ public class ByDateOverview extends QueriedExtensionOverview { @Override public String title() { - return "New Releases"; + return LanguageBundle.get("ext.store.search.ordering.bydate.title"); } @Override public String description() { - return "Extensions that were recently added to the G-ExtensionStore"; + return LanguageBundle.get("ext.store.search.ordering.bydate.description"); } @Override public String contentTitle() { - return "New Releases"; + return LanguageBundle.get("ext.store.search.ordering.bydate.contenttitle"); } }; } diff --git a/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/queriedoverviews/ByRatingOverview.java b/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/queriedoverviews/ByRatingOverview.java index bd080b9..de8d8d9 100644 --- a/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/queriedoverviews/ByRatingOverview.java +++ b/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/queriedoverviews/ByRatingOverview.java @@ -1,11 +1,11 @@ package gearth.services.internal_extensions.extensionstore.application.entities.queriedoverviews; import gearth.misc.OSValidator; -import gearth.services.internal_extensions.extensionstore.application.GExtensionStoreController; import gearth.services.internal_extensions.extensionstore.application.entities.HOverview; import gearth.services.internal_extensions.extensionstore.repository.StoreRepository; import gearth.services.internal_extensions.extensionstore.repository.models.StoreExtension; import gearth.services.internal_extensions.extensionstore.repository.querying.ExtensionOrdering; +import gearth.ui.translations.LanguageBundle; import java.util.Collections; import java.util.List; @@ -32,17 +32,17 @@ public class ByRatingOverview extends QueriedExtensionOverview { @Override public String title() { - return "Popular Extensions"; + return LanguageBundle.get("ext.store.search.ordering.byrating.title"); } @Override public String description() { - return "Extensions sorted by rating"; + return LanguageBundle.get("ext.store.search.ordering.byrating.description"); } @Override public String contentTitle() { - return "Popular Extensions"; + return LanguageBundle.get("ext.store.search.ordering.byrating.contenttitle"); } }; } diff --git a/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/queriedoverviews/ByUpdateOverview.java b/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/queriedoverviews/ByUpdateOverview.java index 36137ba..cb87d34 100644 --- a/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/queriedoverviews/ByUpdateOverview.java +++ b/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/queriedoverviews/ByUpdateOverview.java @@ -5,6 +5,7 @@ import gearth.services.internal_extensions.extensionstore.application.entities.H import gearth.services.internal_extensions.extensionstore.repository.StoreRepository; import gearth.services.internal_extensions.extensionstore.repository.models.StoreExtension; import gearth.services.internal_extensions.extensionstore.repository.querying.ExtensionOrdering; +import gearth.ui.translations.LanguageBundle; import java.util.Collections; import java.util.List; @@ -31,17 +32,17 @@ public class ByUpdateOverview extends QueriedExtensionOverview { @Override public String title() { - return "Recently Updated"; + return LanguageBundle.get("ext.store.search.ordering.byupdate.title"); } @Override public String description() { - return "Extensions that were recently updated"; + return LanguageBundle.get("ext.store.search.ordering.byupdate.description"); } @Override public String contentTitle() { - return "Recently Updated"; + return LanguageBundle.get("ext.store.search.ordering.byupdate.contenttitle"); } }; } diff --git a/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/queriedoverviews/CategorizedOverview.java b/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/queriedoverviews/CategorizedOverview.java index e530256..3829d6c 100644 --- a/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/queriedoverviews/CategorizedOverview.java +++ b/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/queriedoverviews/CategorizedOverview.java @@ -1,12 +1,12 @@ package gearth.services.internal_extensions.extensionstore.application.entities.queriedoverviews; import gearth.misc.OSValidator; -import gearth.services.internal_extensions.extensionstore.application.WebUtils; import gearth.services.internal_extensions.extensionstore.application.entities.HOverview; import gearth.services.internal_extensions.extensionstore.repository.StoreRepository; import gearth.services.internal_extensions.extensionstore.repository.models.ExtCategory; import gearth.services.internal_extensions.extensionstore.repository.models.StoreExtension; import gearth.services.internal_extensions.extensionstore.repository.querying.ExtensionOrdering; +import gearth.ui.translations.LanguageBundle; import java.util.Collections; import java.util.List; @@ -47,7 +47,7 @@ public class CategorizedOverview extends QueriedExtensionOverview { @Override public String contentTitle() { - return "Category: " + category.getName(); + return LanguageBundle.get("ext.store.category") + ": " + category.getName(); } }; } diff --git a/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/queriedoverviews/SearchedQueryOverview.java b/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/queriedoverviews/SearchedQueryOverview.java index 0709ced..cf951a9 100644 --- a/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/queriedoverviews/SearchedQueryOverview.java +++ b/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/queriedoverviews/SearchedQueryOverview.java @@ -5,6 +5,7 @@ import gearth.services.internal_extensions.extensionstore.application.entities.H import gearth.services.internal_extensions.extensionstore.repository.StoreRepository; import gearth.services.internal_extensions.extensionstore.repository.models.StoreExtension; import gearth.services.internal_extensions.extensionstore.repository.querying.ExtensionOrdering; +import gearth.ui.translations.LanguageBundle; import java.util.Collections; import java.util.List; @@ -48,17 +49,17 @@ public class SearchedQueryOverview extends QueriedExtensionOverview { @Override public String title() { - return "Search"; + return LanguageBundle.get("ext.store.search.title"); } @Override public String description() { - return "Find the extension that fits your needs"; + return LanguageBundle.get("ext.store.search.description"); } @Override public String contentTitle() { - return "Search results"; + return LanguageBundle.get("ext.store.search.results"); } }; } diff --git a/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/search/SearchComponent.java b/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/search/SearchComponent.java index b53b3f1..a8f462f 100644 --- a/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/search/SearchComponent.java +++ b/G-Earth/src/main/java/gearth/services/internal_extensions/extensionstore/application/entities/search/SearchComponent.java @@ -1,10 +1,12 @@ package gearth.services.internal_extensions.extensionstore.application.entities.search; +import gearth.GEarth; import gearth.services.internal_extensions.extensionstore.GExtensionStore; import gearth.services.internal_extensions.extensionstore.application.GExtensionStoreController; import gearth.services.internal_extensions.extensionstore.application.entities.ContentItem; import gearth.services.internal_extensions.extensionstore.repository.StoreRepository; import gearth.services.internal_extensions.extensionstore.repository.querying.ExtensionOrdering; +import gearth.ui.translations.LanguageBundle; import netscape.javascript.JSObject; import java.util.ArrayList; @@ -92,13 +94,13 @@ public class SearchComponent implements ContentItem { .append("
") .append("
") - .append("") + .append("") .append(String.format("", searchKeyword, id)) .append("
") .append("
") - .append(""); + .append(""); // add ordering stuff htmlBuilder.append(String.format(" -