Merge branch 'network-extensions-rewrite' of github.com:dorving/G-Earth into dorving-network-extensions-rewrite

This commit is contained in:
sirjonasxx 2022-12-27 19:27:10 +01:00
commit df45404a36
17 changed files with 1417 additions and 749 deletions

View File

@ -1,12 +1,14 @@
package gearth.extensions; package gearth.extensions;
import gearth.misc.HostInfo; 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.services.packet_info.PacketInfoManager;
import gearth.protocol.HMessage; import gearth.protocol.HMessage;
import gearth.protocol.HPacket; import gearth.protocol.HPacket;
import gearth.protocol.connection.HClient; import gearth.protocol.connection.HClient;
import gearth.services.Constants; import gearth.services.Constants;
import gearth.services.extension_handler.extensions.implementations.network.NetworkExtensionInfo;
import java.io.*; import java.io.*;
import java.net.Socket; import java.net.Socket;
@ -108,10 +110,10 @@ public abstract class Extension extends ExtensionBase {
packet.fixLength(); packet.fixLength();
if (packet.headerId() == NetworkExtensionInfo.OUTGOING_MESSAGES_IDS.INFOREQUEST) { if (packet.headerId() == Outgoing.InfoRequest.HEADER_ID) {
ExtensionInfo info = getInfoAnnotations(); ExtensionInfo info = getInfoAnnotations();
HPacket response = new HPacket(NetworkExtensionInfo.INCOMING_MESSAGES_IDS.EXTENSIONINFO); HPacket response = new HPacket(Incoming.ExtensionInfo.HEADER_ID);
response.appendString(info.Title()) response.appendString(info.Title())
.appendString(info.Author()) .appendString(info.Author())
.appendString(info.Version()) .appendString(info.Version())
@ -124,7 +126,7 @@ public abstract class Extension extends ExtensionBase {
.appendBoolean(canDelete()); .appendBoolean(canDelete());
writeToStream(response.toBytes()); writeToStream(response.toBytes());
} }
else if (packet.headerId() == NetworkExtensionInfo.OUTGOING_MESSAGES_IDS.CONNECTIONSTART) { else if (packet.headerId() == Outgoing.ConnectionStart.HEADER_ID) {
String host = packet.readString(); String host = packet.readString();
int connectionPort = packet.readInteger(); int connectionPort = packet.readInteger();
String hotelVersion = packet.readString(); String hotelVersion = packet.readString();
@ -143,10 +145,10 @@ public abstract class Extension extends ExtensionBase {
); );
onStartConnection(); onStartConnection();
} }
else if (packet.headerId() == NetworkExtensionInfo.OUTGOING_MESSAGES_IDS.CONNECTIONEND) { else if (packet.headerId() == Outgoing.ConnectionEnd.HEADER_ID) {
onEndConnection(); 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 // body = an array of G-Earths gearth flags
if (flagRequestCallback != null) { if (flagRequestCallback != null) {
int arraysize = packet.readInteger(); int arraysize = packet.readInteger();
@ -158,7 +160,7 @@ public abstract class Extension extends ExtensionBase {
} }
flagRequestCallback = null; flagRequestCallback = null;
} }
else if (packet.headerId() == NetworkExtensionInfo.OUTGOING_MESSAGES_IDS.INIT) { else if (packet.headerId() == Outgoing.Init.HEADER_ID) {
delayed_init = packet.readBoolean(); delayed_init = packet.readBoolean();
HostInfo hostInfo = HostInfo.fromPacket(packet); HostInfo hostInfo = HostInfo.fromPacket(packet);
updateHostInfo(hostInfo); updateHostInfo(hostInfo);
@ -167,21 +169,21 @@ public abstract class Extension extends ExtensionBase {
} }
writeToConsole("green","Extension \"" + getInfoAnnotations().Title() + "\" successfully initialized", false); 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(); onClick();
} }
else if (packet.headerId() == NetworkExtensionInfo.OUTGOING_MESSAGES_IDS.PACKETINTERCEPT) { else if (packet.headerId() == Outgoing.PacketIntercept.HEADER_ID) {
String stringifiedMessage = packet.readLongString(); String stringifiedMessage = packet.readLongString();
HMessage habboMessage = new HMessage(stringifiedMessage); HMessage habboMessage = new HMessage(stringifiedMessage);
modifyMessage(habboMessage); modifyMessage(habboMessage);
HPacket response = new HPacket(NetworkExtensionInfo.INCOMING_MESSAGES_IDS.MANIPULATEDPACKET); HPacket response = new HPacket(Incoming.ManipulatedPacket.MANIPULATED_PACKET);
response.appendLongString(habboMessage.stringify()); response.appendLongString(habboMessage.stringify());
writeToStream(response.toBytes()); 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); HostInfo hostInfo = HostInfo.fromPacket(packet);
updateHostInfo(hostInfo); updateHostInfo(hostInfo);
} }
@ -231,7 +233,7 @@ public abstract class Extension extends ExtensionBase {
if (!packet.isPacketComplete()) packet.completePacket(packetInfoManager); if (!packet.isPacketComplete()) packet.completePacket(packetInfoManager);
if (!packet.isPacketComplete()) return false; 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.appendByte(direction == HMessage.Direction.TOCLIENT ? (byte)0 : (byte)1);
packet1.appendInt(packet.getBytesLength()); packet1.appendInt(packet.getBytesLength());
packet1.appendBytes(packet.toBytes()); packet1.appendBytes(packet.toBytes());
@ -253,7 +255,7 @@ public abstract class Extension extends ExtensionBase {
if (this.flagRequestCallback != null) return false; if (this.flagRequestCallback != null) return false;
this.flagRequestCallback = flagRequestCallback; this.flagRequestCallback = flagRequestCallback;
try { try {
writeToStream(new HPacket(NetworkExtensionInfo.INCOMING_MESSAGES_IDS.REQUESTFLAGS).toBytes()); writeToStream(new HPacket(Incoming.RequestFlags.HEADER_ID).toBytes());
return true; return true;
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
@ -279,7 +281,7 @@ public abstract class Extension extends ExtensionBase {
private void writeToConsole(String colorClass, String s, boolean mentionTitle) { private void writeToConsole(String colorClass, String s, boolean mentionTitle) {
String text = "[" + colorClass + "]" + (mentionTitle ? (getInfoAnnotations().Title() + " --> ") : "") + s; 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); packet.appendString(text);
try { try {
writeToStream(packet.toBytes()); writeToStream(packet.toBytes());

View File

@ -1,6 +1,6 @@
package gearth.services.extension_handler.extensions.extensionproducers; 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 gearth.services.extension_handler.extensions.implementations.simple.SimpleExtensionProducer;
import java.util.ArrayList; import java.util.ArrayList;
@ -9,13 +9,17 @@ import java.util.List;
public class ExtensionProducerFactory { public class ExtensionProducerFactory {
// returns one of every ExtensionProducer class we have created, to support all types of extensions // 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<ExtensionProducer> getAll() { public static List<ExtensionProducer> getAll() {
List<ExtensionProducer> all = new ArrayList<>(); List<ExtensionProducer> all = new ArrayList<>();
all.add(new NetworkExtensionsProducer()); all.add(EXTENSION_SERVER);
all.add(new SimpleExtensionProducer()); all.add(new SimpleExtensionProducer());
return all; return all;
} }
public static NetworkExtensionServer getExtensionServer() {
return EXTENSION_SERVER;
}
} }

View File

@ -1,258 +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()) {
sendMessage(
side == 0 ? HMessage.Direction.TOCLIENT : HMessage.Direction.TOSERVER,
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;
}
}

View File

@ -1,7 +1,6 @@
package gearth.services.extension_handler.extensions.implementations.network.authentication; package gearth.services.extension_handler.extensions.implementations.network;
import gearth.misc.ConfirmationDialog; import gearth.misc.ConfirmationDialog;
import gearth.services.extension_handler.extensions.implementations.network.NetworkExtension;
import gearth.ui.titlebar.TitleBarController; import gearth.ui.titlebar.TitleBarController;
import gearth.ui.translations.LanguageBundle; import gearth.ui.translations.LanguageBundle;
import javafx.application.Platform; import javafx.application.Platform;
@ -11,62 +10,59 @@ import javafx.scene.control.Label;
import java.io.IOException; import java.io.IOException;
import java.util.*; import java.util.*;
import java.util.concurrent.CountDownLatch;
/** /**
* Created by Jonas on 16/10/18. * Created by Jonas on 16/10/18.
*/ */
public class Authenticator { public final class NetworkExtensionAuthenticator {
private static Map<String, String> cookies = new HashMap<>(); private static final Map<String, String> COOKIES = new HashMap<>();
private static Set<String> perma_cookies = new HashSet<>(); private static final Set<String> PERSISTENT_COOKIES = new HashSet<>();
public static String generateCookieForExtension(String filename) { private static volatile boolean rememberOption = false;
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) { public static boolean evaluate(NetworkExtensionClient extension) {
if (extension.getCookie() != null && perma_cookies.contains(extension.getCookie())) {
final String cookie = extension.getCookie();
if (cookie != null && PERSISTENT_COOKIES.contains(cookie))
return true; return true;
}
if (extension.isInstalledExtension()) { return extension.isInstalledExtension()
return claimSession(extension.getFileName(), extension.getCookie()); ? claimSession(extension.getFileName(), cookie)
} : askForPermission(extension);
else {
return askForPermission(extension);
}
} }
/** /**
* authenticator: authenticate an extension and remove the cookie * Authenticate an extension and remove the cookie
* @param filename *
* @param cookie * @return {@code true} if the extension is authenticated.
* @return if the extension is authenticated
*/ */
private static boolean claimSession(String filename, String cookie) { private static boolean claimSession(String filename, String cookie) {
if (cookies.containsKey(filename) && cookies.get(filename).equals(cookie)) { if (COOKIES.containsKey(filename) && COOKIES.get(filename).equals(cookie)) {
cookies.remove(filename); COOKIES.remove(filename);
return true; return true;
} }
return false; return false;
} }
private static volatile boolean rememberOption = false; /**
//for not-installed extensions, popup a dialog * For not yet installed extensions, open a confirmation dialog.
private static boolean askForPermission(NetworkExtension extension) { *
* @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}; boolean[] allowConnection = {true};
final String connectExtensionKey = "allow_extension_connection"; final String connectExtensionKey = "allow_extension_connection";
if (ConfirmationDialog.showDialog(connectExtensionKey)) { if (ConfirmationDialog.showDialog(connectExtensionKey)) {
boolean[] done = {false};
final CountDownLatch countDownLatch = new CountDownLatch(0);
Platform.runLater(() -> { Platform.runLater(() -> {
Alert alert = ConfirmationDialog.createAlertWithOptOut(Alert.AlertType.WARNING, connectExtensionKey Alert alert = ConfirmationDialog.createAlertWithOptOut(Alert.AlertType.WARNING, connectExtensionKey
, LanguageBundle.get("alert.confirmation.windowtitle"), null, , LanguageBundle.get("alert.confirmation.windowtitle"), null,
@ -84,33 +80,40 @@ public class Authenticator {
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
} }
done[0] = true; countDownLatch.countDown();
if (!ConfirmationDialog.showDialog(connectExtensionKey)) { if (!ConfirmationDialog.showDialog(connectExtensionKey)) {
rememberOption = allowConnection[0]; rememberOption = allowConnection[0];
} }
}); });
while (!done[0]) {
try { try {
Thread.sleep(1); countDownLatch.await();
} catch (InterruptedException e) { } catch (InterruptedException e) {
e.printStackTrace(); e.printStackTrace();
} }
}
return allowConnection[0]; return allowConnection[0];
} }
return rememberOption; return rememberOption;
} }
private static String getRandomCookie() { public static String generateCookieForExtension(String filename) {
StringBuilder builder = new StringBuilder(); final String cookie = generateRandomCookie();
Random r = new Random(); COOKIES.put(filename, cookie);
for (int i = 0; i < 40; i++) { return cookie;
builder.append(r.nextInt(40));
} }
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(); return builder.toString();
} }
} }

View File

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

View File

@ -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 <PORT>",
* where <PORT> 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<Class<?>, PacketStructure> outgoingPacketStructures = new HashMap<>();
private final static Map<Integer, PacketStructure> incomingPacketStructures = new HashMap<>();
public static PacketStructure getIncomingStructure(int headerId) {
return incomingPacketStructures.get(headerId);
}
public static<T extends NetworkExtensionMessage> 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<String> 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 <T extends NetworkExtensionMessage> void register(final int headerId, Class<T> tClass, BiConsumer<T, HPacket> writer, Function<HPacket, T> 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<? extends NetworkExtensionMessage, HPacket> writer;
private final Function<HPacket, ? extends NetworkExtensionMessage> reader;
public PacketStructure(int headerId, String name, BiConsumer<? extends NetworkExtensionMessage, HPacket> writer, Function<HPacket, ? extends NetworkExtensionMessage> 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<? extends NetworkExtensionMessage, HPacket> getWriter() {
return writer;
}
public Function<HPacket, ? extends NetworkExtensionMessage> getReader() {
return reader;
}
}
}

View File

@ -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 <PORT>",
* where <PORT> 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;
}
}

View File

@ -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<String> flags;
public FlagsCheck(List<String> flags) {
this.flags = flags;
}
public List<String> 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;
}
}
}
}

View File

@ -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 <a href="http://svn.apache.org/viewvc/camel/trunk/components/camel-test/src/main/java/org/apache/camel/test/AvailablePortFinder.java?view=markup#l130">http://svn.apache.org/viewvc/camel/trunk/components/camel-test/src/main/java/org/apache/camel/test/AvailablePortFinder.java?view=markup#l130</a>
*
* @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<SocketChannel> {
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<Object> 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<Outgoing> {
@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<Outgoing, HPacket> writer = (BiConsumer<Outgoing, HPacket>) 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<NetworkExtensionClient> 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<NetworkExtensionClient> 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<NetworkExtensionClient> optionalClient = findClient(ctx);
if (optionalClient.isPresent())
optionalClient.get().close();
else
ctx.channel().close();
}
private Optional<NetworkExtensionClient> findClient(ChannelHandlerContext ctx) {
return Optional.ofNullable(ctx.attr(CLIENT).get());
}
}
}

View File

@ -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.
* <p>
* 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.
* <p>
* 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();
}
}

View File

@ -8,39 +8,39 @@ import java.util.Map;
/** /**
* Created by Jonas on 22/09/18. * Created by Jonas on 22/09/18.
*/ */
public class ExecutionInfo { public final class ExecutionInfo {
private static Map<String, String[]> extensionTypeToExecutionCommand; private static final Map<String, String[]> EXTENSION_TYPE_TO_EXECUTION_COMMAND;
public final static List<String> ALLOWEDEXTENSIONTYPES;
public final static String EXTENSIONSDIRECTORY = "Extensions"; public final static List<String> ALLOWED_EXTENSION_TYPES;
public final static String EXTENSIONS_DIRECTORY = "Extensions";
static { 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}"}; EXTENSION_TYPE_TO_EXECUTION_COMMAND = new HashMap<>();
for(String type : extensionTypeToExecutionCommand.keySet()) { EXTENSION_TYPE_TO_EXECUTION_COMMAND.put("*.jar", new String[]{"java", "-jar", "{path}"});
String[] commandShort = extensionTypeToExecutionCommand.get(type); EXTENSION_TYPE_TO_EXECUTION_COMMAND.put("*.py", new String[]{"python", "{path}"});
String[] combined = new String[extraArgs.length + commandShort.length]; 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(commandShort, 0, combined, 0, commandShort.length);
System.arraycopy(extraArgs, 0, combined, commandShort.length, extraArgs.length); System.arraycopy(extraArgs, 0, combined, commandShort.length, extraArgs.length);
extensionTypeToExecutionCommand.put( EXTENSION_TYPE_TO_EXECUTION_COMMAND.put(type, combined);
type,
combined
);
} }
ALLOWEDEXTENSIONTYPES = new ArrayList<>(extensionTypeToExecutionCommand.keySet()); ALLOWED_EXTENSION_TYPES = new ArrayList<>(EXTENSION_TYPE_TO_EXECUTION_COMMAND.keySet());
} }
public static String[] getExecutionCommand(String type) { public static String[] getExecutionCommand(String type) {
return extensionTypeToExecutionCommand.get(type); return EXTENSION_TYPE_TO_EXECUTION_COMMAND.get(type);
} }
} }

View File

@ -3,9 +3,9 @@ package gearth.services.extension_handler.extensions.implementations.network.exe
/** /**
* Created by Jonas on 22/09/18. * 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() { public static ExtensionRunner get() {
return runner; return runner;

View File

@ -1,102 +1,105 @@
package gearth.services.extension_handler.extensions.implementations.network.executer; package gearth.services.extension_handler.extensions.implementations.network.executer;
import gearth.GEarth; 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 gearth.services.internal_extensions.extensionstore.tools.StoreExtensionTools;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.net.URISyntaxException; 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.Arrays;
import java.util.Random; import java.util.Random;
/** /**
* Created by Jonas on 22/09/18. * 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 { static {
final URL url = getLocation();
String value; String value;
try { try {
value = new File(GEarth.class.getProtectionDomain().getCodeSource().getLocation().toURI()).getParent(); value = new File(url.toURI()).getParent();
} catch (URISyntaxException e) { } catch (URISyntaxException e) {
value = new File(GEarth.class.getProtectionDomain().getCodeSource().getLocation().getPath()).getParent(); value = new File(url.getPath()).getParent();
e.printStackTrace(); 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 @Override
public void runAllExtensions(int port) { public void runAllExtensions(int port) {
if (dirExists(ExecutionInfo.EXTENSIONSDIRECTORY)){
File folder =
new File(JARPATH +
FileSystems.getDefault().getSeparator()+
ExecutionInfo.EXTENSIONSDIRECTORY);
File[] childs = folder.listFiles(); if (dirExists(ExecutionInfo.EXTENSIONS_DIRECTORY)) {
for (File file : childs) {
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); tryRunExtension(file.getPath(), port);
} } else
} LOGGER.warn("Did not run extensions because extensions directory does not exist at {}", ExecutionInfo.EXTENSIONS_DIRECTORY);
} }
@Override @Override
public void installAndRunExtension(String path, int port) { public void installAndRunExtension(String stringPath, int port) {
if (!dirExists(ExecutionInfo.EXTENSIONSDIRECTORY)) {
createDirectory(ExecutionInfo.EXTENSIONSDIRECTORY);
}
if (!dirExists(ExecutionInfo.EXTENSIONS_DIRECTORY))
tryCreateDirectory(ExecutionInfo.EXTENSIONS_DIRECTORY);
String name = Paths.get(path).getFileName().toString(); final Path path = Paths.get(stringPath);
String[] split = name.split("\\."); final String name = path.getFileName().toString();
String ext = "*." + split[split.length - 1]; final String[] split = name.split("\\.");
final String ext = "*." + split[split.length - 1];
String realname = String.join(".",Arrays.copyOf(split, split.length-1)); final String realName = String.join(".", Arrays.copyOf(split, split.length - 1));
String newname = realname + "-" + getRandomString() + ext.substring(1); final String newName = realName + "-" + getRandomString() + ext.substring(1);
Path originalPath = Paths.get(path); final Path newPath = Paths.get(JAR_PATH, ExecutionInfo.EXTENSIONS_DIRECTORY, newName);
Path newPath = Paths.get(
JARPATH,
ExecutionInfo.EXTENSIONSDIRECTORY,
newname
);
try { try {
Files.copy(
originalPath,
newPath
);
// addExecPermission(newPath.toString()); Files.copy(path, newPath);
tryRunExtension(newPath.toString(), port); tryRunExtension(newPath.toString(), port);
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
} }
} }
public void tryRunExtension(String path, int port) { public void tryRunExtension(String path, int port) {
try { try {
if (new File(path).isDirectory()) { if (new File(path).isDirectory()) {
// this extension is installed from the extension store and requires // this extension is installed from the extension store and requires different behavior
// different behavior
StoreExtensionTools.executeExtension(path, port); StoreExtensionTools.executeExtension(path, port);
return; return;
} }
String filename = Paths.get(path).getFileName().toString(); final String filename = Paths.get(path).getFileName().toString();
final String[] execCommand = ExecutionInfo
String[] execCommand = ExecutionInfo.getExecutionCommand(getFileExtension(path)); .getExecutionCommand(getFileExtension(path))
execCommand = Arrays.copyOf(execCommand, execCommand.length); .clone();
String cookie = Authenticator.generateCookieForExtension(filename); final String cookie = NetworkExtensionAuthenticator.generateCookieForExtension(filename);
for (int i = 0; i < execCommand.length; i++) { for (int i = 0; i < execCommand.length; i++) {
execCommand[i] = execCommand[i] execCommand[i] = execCommand[i]
.replace("{path}", path) .replace("{path}", path)
@ -104,104 +107,92 @@ public class NormalExtensionRunner implements ExtensionRunner {
.replace("{filename}", filename) .replace("{filename}", filename)
.replace("{cookie}", cookie); .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) { } 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)) { 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 final Logger logger = LoggerFactory.getLogger(path);
InputStreamReader(proc.getInputStream()));
logger.info("Launching...");
final BufferedReader processInputReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
new Thread(() -> { new Thread(() -> {
try { try {
String line; String line;
while((line = stdInput.readLine()) != null) { while ((line = processInputReader.readLine()) != null)
synchronized (System.out) { logger.info(line);
System.out.println(path + sep + "Output" + sep + line + sep + "----------" + sep);
}
}
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); LOGGER.error("Failed to read input line from process {}", process, e);
} }
}).start(); }, path+"-input").start();
BufferedReader stdError = new BufferedReader(new
InputStreamReader(proc.getErrorStream()));
final BufferedReader processErrorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
new Thread(() -> { new Thread(() -> {
try { try {
String line; String line;
while((line = stdError.readLine()) != null) { while ((line = processErrorReader.readLine()) != null)
synchronized (System.out) { logger.error(line);
System.out.println(path + sep + "Error" + sep + line + sep + "----------" + sep);
}
}
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); LOGGER.error("Failed to read error line from process {}", process, e);
} }
}).start(); }).start();
} }
} }
@Override @Override
public void uninstallExtension(String filename) { public void uninstallExtension(String filename) {
try { 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()) { if (new File(path.toString()).isDirectory()) {
// is installed through extension store // is installed through extension store
StoreExtensionTools.removeExtension(path.toString()); StoreExtensionTools.removeExtension(path.toString());
} } else
else {
Files.delete(path); Files.delete(path);
}
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); LOGGER.error("Failed to uninstall extension at {}", filename, e);
} }
} }
// private void addExecPermission(String path) { private static void tryCreateDirectory(String path) {
// //not needed at first sight 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) { private static boolean dirExists(String dir) {
String name = Paths.get(path).getFileName().toString(); return Files.isDirectory(Paths.get(JAR_PATH, dir));
String[] split = name.split("\\."); }
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]; return "*." + split[split.length - 1];
} }
private boolean dirExists(String dir) { private static String getRandomString() {
return Files.isDirectory(Paths.get(JARPATH, dir)); final StringBuilder builder = new StringBuilder();
} final Random random = new Random();
private void createDirectory(String dir) { for (int i = 0; i < 12; i++)
if (!dirExists(dir)) { builder.append(random.nextInt(10));
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));
}
return builder.toString(); return builder.toString();
} }
} }

View File

@ -1,8 +1,6 @@
package gearth.services.internal_extensions.extensionstore.application.entities.extensiondetails; package gearth.services.internal_extensions.extensionstore.application.entities.extensiondetails;
import gearth.GEarth; import gearth.services.extension_handler.extensions.extensionproducers.ExtensionProducerFactory;
import gearth.services.extension_handler.extensions.implementations.network.NetworkExtensionsProducer;
import gearth.services.extension_handler.extensions.implementations.network.executer.NormalExtensionRunner;
import gearth.services.internal_extensions.extensionstore.GExtensionStore; import gearth.services.internal_extensions.extensionstore.GExtensionStore;
import gearth.services.internal_extensions.extensionstore.application.GExtensionStoreController; import gearth.services.internal_extensions.extensionstore.application.GExtensionStoreController;
import gearth.services.internal_extensions.extensionstore.application.WebUtils; import gearth.services.internal_extensions.extensionstore.application.WebUtils;
@ -16,13 +14,11 @@ import gearth.ui.titlebar.TitleBarController;
import gearth.ui.translations.LanguageBundle; import gearth.ui.translations.LanguageBundle;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.scene.control.Alert; import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import org.apache.maven.artifact.versioning.ComparableVersion; import org.apache.maven.artifact.versioning.ComparableVersion;
import org.w3c.dom.Element; import org.w3c.dom.Element;
import java.io.IOException; import java.io.IOException;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -137,7 +133,7 @@ public class StoreExtensionDetailsOverview extends HOverview {
@Override @Override
public void success(String installationFolder) { public void success(String installationFolder) {
Platform.runLater(() -> successPopup(modeString)); Platform.runLater(() -> successPopup(modeString));
StoreExtensionTools.executeExtension(installationFolder, NetworkExtensionsProducer.extensionPort); StoreExtensionTools.executeExtension(installationFolder, ExtensionProducerFactory.getExtensionServer().getPort());
} }
@Override @Override

View File

@ -2,7 +2,7 @@ package gearth.services.internal_extensions.extensionstore.tools;
import gearth.GEarth; import gearth.GEarth;
import gearth.misc.OSValidator; import gearth.misc.OSValidator;
import gearth.services.extension_handler.extensions.implementations.network.authentication.Authenticator; import gearth.services.extension_handler.extensions.implementations.network.NetworkExtensionAuthenticator;
import gearth.services.extension_handler.extensions.implementations.network.executer.ExecutionInfo; import gearth.services.extension_handler.extensions.implementations.network.executer.ExecutionInfo;
import gearth.services.extension_handler.extensions.implementations.network.executer.NormalExtensionRunner; import gearth.services.extension_handler.extensions.implementations.network.executer.NormalExtensionRunner;
import gearth.services.internal_extensions.extensionstore.repository.StoreFetch; import gearth.services.internal_extensions.extensionstore.repository.StoreFetch;
@ -34,7 +34,7 @@ public class StoreExtensionTools {
} }
public final static String EXTENSIONS_PATH = Paths.get(NormalExtensionRunner.JARPATH, ExecutionInfo.EXTENSIONSDIRECTORY).toString(); public final static String EXTENSIONS_PATH = Paths.get(NormalExtensionRunner.JAR_PATH, ExecutionInfo.EXTENSIONS_DIRECTORY).toString();
public static void executeExtension(String extensionPath, int port) { public static void executeExtension(String extensionPath, int port) {
@ -42,7 +42,7 @@ public class StoreExtensionTools {
String installedExtensionId = Paths.get(extensionPath).getFileName().toString(); String installedExtensionId = Paths.get(extensionPath).getFileName().toString();
String commandPath = Paths.get(extensionPath, "command.txt").toString(); String commandPath = Paths.get(extensionPath, "command.txt").toString();
String cookie = Authenticator.generateCookieForExtension(installedExtensionId); String cookie = NetworkExtensionAuthenticator.generateCookieForExtension(installedExtensionId);
List<String> command = new JSONArray(FileUtils.readFileToString(new File(commandPath), "UTF-8")) List<String> command = new JSONArray(FileUtils.readFileToString(new File(commandPath), "UTF-8"))
.toList().stream().map(o -> (String)o).map(s -> s .toList().stream().map(o -> (String)o).map(s -> s
.replace("{port}", port+"") .replace("{port}", port+"")

View File

@ -100,7 +100,7 @@ public class ExtensionItemContainer extends GridPane {
reloadButton.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> { reloadButton.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
reloadButton.setVisible(false); reloadButton.setVisible(false);
ExtensionRunner runner = ExtensionRunnerFactory.get(); ExtensionRunner runner = ExtensionRunnerFactory.get();
runner.tryRunExtension(Paths.get(NormalExtensionRunner.JARPATH, ExecutionInfo.EXTENSIONSDIRECTORY, item.getFileName()).toString(), port); runner.tryRunExtension(Paths.get(NormalExtensionRunner.JAR_PATH, ExecutionInfo.EXTENSIONS_DIRECTORY, item.getFileName()).toString(), port);
}); });
DeleteButton deleteButton = new DeleteButton(); DeleteButton deleteButton = new DeleteButton();

View File

@ -1,10 +1,9 @@
package gearth.ui.subforms.extensions; package gearth.ui.subforms.extensions;
import gearth.GEarth;
import gearth.services.extension_handler.ExtensionHandler; import gearth.services.extension_handler.ExtensionHandler;
import gearth.services.extension_handler.extensions.ExtensionListener; import gearth.services.extension_handler.extensions.ExtensionListener;
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.network.authentication.Authenticator; import gearth.services.extension_handler.extensions.implementations.network.NetworkExtensionAuthenticator;
import gearth.services.extension_handler.extensions.implementations.network.executer.ExecutionInfo; import gearth.services.extension_handler.extensions.implementations.network.executer.ExecutionInfo;
import gearth.services.extension_handler.extensions.implementations.network.executer.ExtensionRunner; import gearth.services.extension_handler.extensions.implementations.network.executer.ExtensionRunner;
import gearth.services.extension_handler.extensions.implementations.network.executer.ExtensionRunnerFactory; import gearth.services.extension_handler.extensions.implementations.network.executer.ExtensionRunnerFactory;
@ -39,7 +38,7 @@ public class ExtensionsController extends SubForm {
private ExtensionRunner extensionRunner = null; private ExtensionRunner extensionRunner = null;
private ExtensionHandler extensionHandler; private ExtensionHandler extensionHandler;
private NetworkExtensionsProducer networkExtensionsProducer; // needed for port private NetworkExtensionServer networkExtensionsProducer; // needed for port
private ExtensionLogger extensionLogger = null; private ExtensionLogger extensionLogger = null;
public Label lbl_tableTitle, lbl_tableDesc, lbl_tableAuthor, lbl_tableVersion, lbl_tableEdit, lbl_port; public Label lbl_tableTitle, lbl_tableDesc, lbl_tableAuthor, lbl_tableVersion, lbl_tableEdit, lbl_port;
@ -61,8 +60,8 @@ public class ExtensionsController extends SubForm {
//noinspection OptionalGetWithoutIsPresent //noinspection OptionalGetWithoutIsPresent
networkExtensionsProducer networkExtensionsProducer
= (NetworkExtensionsProducer) extensionHandler.getExtensionProducers().stream() = (NetworkExtensionServer) extensionHandler.getExtensionProducers().stream()
.filter(producer1 -> producer1 instanceof NetworkExtensionsProducer) .filter(producer1 -> producer1 instanceof NetworkExtensionServer)
.findFirst().get(); .findFirst().get();
@ -87,7 +86,7 @@ public class ExtensionsController extends SubForm {
FileChooser fileChooser = new FileChooser(); FileChooser fileChooser = new FileChooser();
fileChooser.setTitle(LanguageBundle.get("tab.extensions.button.install.windowtitle")); fileChooser.setTitle(LanguageBundle.get("tab.extensions.button.install.windowtitle"));
fileChooser.getExtensionFilters().addAll( fileChooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter(LanguageBundle.get("tab.extensions.button.install.filetype"), ExecutionInfo.ALLOWEDEXTENSIONTYPES)); new FileChooser.ExtensionFilter(LanguageBundle.get("tab.extensions.button.install.filetype"), ExecutionInfo.ALLOWED_EXTENSION_TYPES));
File selectedFile = fileChooser.showOpenDialog(parentController.getStage()); File selectedFile = fileChooser.showOpenDialog(parentController.getStage());
if (selectedFile != null) { if (selectedFile != null) {
extensionRunner.installAndRunExtension(selectedFile.getPath(), networkExtensionsProducer.getPort()); extensionRunner.installAndRunExtension(selectedFile.getPath(), networkExtensionsProducer.getPort());
@ -123,7 +122,7 @@ public class ExtensionsController extends SubForm {
GPythonShell shell = new GPythonShell( GPythonShell shell = new GPythonShell(
String.format("%s %d", LanguageBundle.get("tab.extensions.button.pythonshell.windowtitle"),gpytonShellCounter++), String.format("%s %d", LanguageBundle.get("tab.extensions.button.pythonshell.windowtitle"),gpytonShellCounter++),
networkExtensionsProducer.getPort(), networkExtensionsProducer.getPort(),
Authenticator.generatePermanentCookie() NetworkExtensionAuthenticator.generatePermanentCookie()
); );
shell.launch((b) -> { shell.launch((b) -> {
pythonShellLaunching = false; pythonShellLaunching = false;