diff --git a/README.md b/README.md
index c77f8fd..11cc502 100755
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-## Zero dependency, small footprint, cross-platform I2P Java Router with simple tunnel/socks controller and SAM interface
+## Zero dependency, small footprint, cross-platform I2P Java Router with GUI and simple tunnel/socks controller and SAM interface
##
@@ -47,14 +47,19 @@ the jlink tool to build zero-dependency platform-specific launchers.
To run the Linux router, type:
`dist/linux/router/bin/launch.sh`
+or `dist/linux/router/bin/launch-gui.sh`
To run the MacOS router, type:
`dist/mac/router/bin/launch.sh`
+or `dist/mac/router/bin/launch-gui.sh`
For Windows, run:
`dist/windows/router/bin/launch.bat`
+or `dist/windows/router/bin/launch-gui.bat`
+
+Note that for the Windows GUI to run, you may need to install the latest Microsoft Visual C++ Redistributable
If it launches successfully, you'll see the message:
diff --git a/bin/build-launcher.sh b/bin/build-launcher.sh
index 590bb6f..f696378 100755
--- a/bin/build-launcher.sh
+++ b/bin/build-launcher.sh
@@ -8,39 +8,61 @@ fi
source $basedir/bin/java-config.sh
+rm -fr target/org.getmonero.i2p.zero/classes
+rm -fr target/org.getmonero.i2p.zero.gui/classes
+
# compile the Main class that starts the I2P router and SAM listener
echo "*** Compiling Main class"
-$JAVA_HOME/bin/javac --module-path import/lib -d target/classes $(find src -name '*.java')
+$JAVA_HOME/bin/javac --module-path import/lib -d target/org.getmonero.i2p.zero/classes $(find org.getmonero.i2p.zero/src -name '*.java')
+$JAVA_HOME/bin/javac --module-path import/lib:import/javafx-sdk-11.0.2/lib:target/org.getmonero.i2p.zero/classes -d target/org.getmonero.i2p.zero.gui/classes $(find org.getmonero.i2p.zero.gui/src -name '*.java')
+cp -r org.getmonero.i2p.zero.gui/src/* target/org.getmonero.i2p.zero.gui/classes
+find target -type f -name '*.java' -delete
# package as a modular jar
echo "*** Packaging as a modular jar"
-$JAVA_HOME/bin/jar --create --file target/org.getmonero.i2p.zero.jar --main-class org.getmonero.i2p.zero.Main -C target/classes .
+$JAVA_HOME/bin/jar --create --file target/org.getmonero.i2p.zero.jar --main-class org.getmonero.i2p.zero.Main -C target/org.getmonero.i2p.zero/classes .
+$JAVA_HOME/bin/jar --create --file target/org.getmonero.i2p.zero.gui.jar --main-class org.getmonero.i2p.zero.gui.Gui -C target/org.getmonero.i2p.zero.gui/classes .
rm -fr $basedir/dist
-mkdir -p $basedir/dist/linux $basedir/dist/mac $basedir/dist/win
+for i in linux mac win linux-gui mac-gui win-gui; do mkdir -p $basedir/dist/$i; done
# create OS specific launchers which will bundle together the code and a minimal JVM
-echo "*** Performing jlink (Linux)"
-$JAVA_HOME/bin/jlink --module-path ${JAVA_HOME_LINUX}/jmods:target/modules:target/org.getmonero.i2p.zero.jar --add-modules org.getmonero.i2p.zero --output dist/linux/router --strip-debug --compress 2 --no-header-files --no-man-pages
-
-echo "*** Performing jlink (Mac)"
-$JAVA_HOME/bin/jlink --module-path ${JAVA_HOME_MAC}/Contents/Home/jmods:target/modules:target/org.getmonero.i2p.zero.jar --add-modules org.getmonero.i2p.zero --output dist/mac/router --strip-debug --compress 2 --no-header-files --no-man-pages
-
-echo "*** Performing jlink (Windows)"
-$JAVA_HOME/bin/jlink --module-path ${JAVA_HOME_WIN}/jmods:target/modules:target/org.getmonero.i2p.zero.jar --add-modules org.getmonero.i2p.zero --output dist/win/router --strip-debug --compress 2 --no-header-files --no-man-pages
+for i in linux mac win; do
+ echo "*** Performing jlink ($i)"
+ case $i in
+ linux )
+ JAVA_HOME_VARIANT=${JAVA_HOME_LINUX} ;;
+ mac )
+ JAVA_HOME_VARIANT=${JAVA_HOME_MAC} ;;
+ win )
+ JAVA_HOME_VARIANT=${JAVA_HOME_WIN} ;;
+ esac
+ echo "Using JAVA_HOME_VARIANT: $JAVA_HOME_VARIANT"
+ $JAVA_HOME/bin/jlink --module-path ${JAVA_HOME_VARIANT}/jmods:target/modules:target/org.getmonero.i2p.zero.jar --add-modules org.getmonero.i2p.zero --output dist/$i/router --strip-debug --compress 2 --no-header-files --no-man-pages
+ $JAVA_HOME/bin/jlink --module-path ${JAVA_HOME_VARIANT}/jmods:import/javafx-jmods/$i/javafx-jmods-11.0.2:target/modules:target/org.getmonero.i2p.zero.jar:target/org.getmonero.i2p.zero.gui.jar --add-modules org.getmonero.i2p.zero,org.getmonero.i2p.zero.gui,javafx.controls,javafx.fxml,java.desktop --output dist/$i-gui/router --strip-debug --compress 2 --no-header-files --no-man-pages
+done
-for i in linux mac; do
+for i in linux mac linux-gui mac-gui; do
cp $basedir/resources/launch.sh $basedir/dist/$i/router/bin/
cp $basedir/resources/tunnel-control.sh $basedir/dist/$i/router/bin/
done
-cp $basedir/resources/launch.bat $basedir/dist/win/router/bin/
+for i in win win-gui; do
+ cp $basedir/resources/launch.bat $basedir/dist/$i/router/bin/
+done
-for i in linux mac win; do cp -r $basedir/import/i2p.base $basedir/dist/$i/router/; done
-for i in linux mac win; do mkdir -p $basedir/dist/$i/router/i2p.config; done
+for i in linux-gui mac-gui; do
+ cp $basedir/resources/launch-gui.sh $basedir/dist/$i/router/bin/
+done
+for i in win-gui; do
+ cp $basedir/resources/launch-gui.bat $basedir/dist/$i/router/bin/
+done
+
+for i in linux mac win linux-gui mac-gui win-gui; do cp -r $basedir/import/i2p.base $basedir/dist/$i/router/; done
+for i in linux mac win linux-gui mac-gui win-gui; do mkdir -p $basedir/dist/$i/router/i2p.config; done
# remove unnecessary native libs from jbigi.jar
-for i in linux mac win; do
+for i in linux mac win linux-gui mac-gui win-gui; do
for j in freebsd linux mac win; do
if [ "$i" != "$j" ]; then
if [ "$j" = "mac" ]; then j="osx"; fi
@@ -54,3 +76,4 @@ du -sk dist/* | awk '{printf "%.1f MB %s\n",$1/1024,$2}'
echo "*** Done ***"
echo "To run, type: dist/linux/router/bin/launch.sh"
+echo "To run the GUI, type: dist/linux-gui/router/bin/launch-gui.sh"
diff --git a/bin/import-packages.sh b/bin/import-packages.sh
index 9d6b569..196b311 100755
--- a/bin/import-packages.sh
+++ b/bin/import-packages.sh
@@ -37,4 +37,28 @@ if [ ! -d "$basedir/import/apache-ant-1.10.5" ]; then
tar zxvf apache-ant-1.10.5-bin.tar.gz
fi
+if [ ! -d "$basedir/import/javafx-sdk-11.0.2" ]; then
+ if [ $(uname -s) = Darwin ]; then
+ wget JAVAFX_SDK_DOWNLOAD_URL_MAC
+ unzip openjfx-11.0.2_osx-x64_bin-sdk.zip
+ else
+ wget $JAVAFX_SDK_DOWNLOAD_URL_LINUX
+ unzip openjfx-11.0.2_linux-x64_bin-sdk.zip
+ fi
+fi
+
+if [ ! -d "$basedir/import/javafx-jmods" ]; then
+ mkdir -p javafx-jmods
+ mkdir -p javafx-jmods/linux javafx-jmods/mac javafx-jmods/win
+ wget --directory-prefix=javafx-jmods/linux $JAVAFX_JMODS_DOWNLOAD_URL_LINUX
+ wget --directory-prefix=javafx-jmods/mac $JAVAFX_JMODS_DOWNLOAD_URL_MAC
+ wget --directory-prefix=javafx-jmods/win $JAVAFX_JMODS_DOWNLOAD_URL_WIN
+
+ unzip jdks/linux/$JAVAFX_JMODS_DOWNLOAD_FILENAME_LINUX -d javafx-jmods/linux/
+ unzip jdks/mac/$JAVAFX_JMODS_DOWNLOAD_FILENAME_MAC -d javafx-jmods/mac/
+ unzip jdks/win/$JAVAFX_JMODS_DOWNLOAD_FILENAME_WIN -d javafx-jmods/win/
+fi
+
+
+
diff --git a/bin/java-config.sh b/bin/java-config.sh
index c6535c9..9b83fd8 100644
--- a/bin/java-config.sh
+++ b/bin/java-config.sh
@@ -18,6 +18,17 @@ JAVA_HOME_LINUX=$basedir/import/jdks/linux/jdk-11.0.1+13
JAVA_HOME_MAC=$basedir/import/jdks/mac/jdk-11.0.1+13/Contents/Home
JAVA_HOME_WIN=$basedir/import/jdks/win/jdk-11.0.1+13
+JAVAFX_SDK_DOWNLOAD_URL_LINUX=https://download2.gluonhq.com/openjfx/11.0.2/openjfx-11.0.2_linux-x64_bin-sdk.zip
+JAVAFX_SDK_DOWNLOAD_URL_MAC=https://download2.gluonhq.com/openjfx/11.0.2/openjfx-11.0.2_osx-x64_bin-sdk.zip
+
+JAVAFX_JMODS_DOWNLOAD_URL_LINUX=https://download2.gluonhq.com/openjfx/11.0.2/openjfx-11.0.2_linux-x64_bin-jmods.zip
+JAVAFX_JMODS_DOWNLOAD_URL_MAC=https://download2.gluonhq.com/openjfx/11.0.2/openjfx-11.0.2_osx-x64_bin-jmods.zip
+JAVAFX_JMODS_DOWNLOAD_URL_WIN=https://download2.gluonhq.com/openjfx/11.0.2/openjfx-11.0.2_windows-x64_bin-jmods.zip
+
+JAVAFX_JMODS_DOWNLOAD_FILENAME_LINUX=openjfx-11.0.2_linux-x64_bin-jmods.zip
+JAVAFX_JMODS_DOWNLOAD_FILENAME_MAC=openjfx-11.0.2_osx-x64_bin-jmods.zip
+JAVAFX_JMODS_DOWNLOAD_FILENAME_WIN=openjfx-11.0.2_windows-x64_bin-jmods.zip
+
OS=`uname -s`
if [ $OS = "Darwin" ]; then
export JAVA_HOME=$JAVA_HOME_MAC
diff --git a/org.getmonero.i2p.zero.gui/org.getmonero.i2p.zero.gui.iml b/org.getmonero.i2p.zero.gui/org.getmonero.i2p.zero.gui.iml
new file mode 100644
index 0000000..b1306c9
--- /dev/null
+++ b/org.getmonero.i2p.zero.gui/org.getmonero.i2p.zero.gui.iml
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/org.getmonero.i2p.zero.gui/src/module-info.java b/org.getmonero.i2p.zero.gui/src/module-info.java
new file mode 100644
index 0000000..62ffc01
--- /dev/null
+++ b/org.getmonero.i2p.zero.gui/src/module-info.java
@@ -0,0 +1,8 @@
+module org.getmonero.i2p.zero.gui {
+ requires org.getmonero.i2p.zero;
+ requires java.desktop;
+ requires javafx.controls;
+ requires javafx.fxml;
+ exports org.getmonero.i2p.zero.gui;
+ opens org.getmonero.i2p.zero.gui;
+}
diff --git a/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/AddTunnelController.java b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/AddTunnelController.java
new file mode 100644
index 0000000..d4a74a1
--- /dev/null
+++ b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/AddTunnelController.java
@@ -0,0 +1,124 @@
+package org.getmonero.i2p.zero.gui;
+
+import javafx.fxml.FXML;
+import javafx.scene.control.*;
+import javafx.scene.layout.Pane;
+import org.getmonero.i2p.zero.TunnelControl;
+
+import java.util.stream.Stream;
+import static org.getmonero.i2p.zero.TunnelControl.Tunnel;
+
+public class AddTunnelController {
+
+ @FXML Pane clientTunnelConfigPane;
+ @FXML Pane serverTunnelConfigPane;
+ @FXML Pane socksProxyConfigPane;
+ @FXML Button addButton;
+ @FXML Button cancelButton;
+ @FXML ToggleGroup tunnelType;
+ @FXML RadioButton clientTunnelRadioButton;
+ @FXML RadioButton serverTunnelRadioButton;
+ @FXML RadioButton socksProxyRadioButton;
+ @FXML TextField clientDestAddrField;
+ @FXML TextField clientPortField;
+ @FXML TextField serverHostField;
+ @FXML TextField serverPortField;
+ @FXML TextField serverKeyField;
+ @FXML TextField serverAddrField;
+ @FXML TextField socksPortField;
+
+
+ private void updateAddButtonState() {
+ if(tunnelType.getSelectedToggle().equals(clientTunnelRadioButton)) {
+ addButton.setDisable(Stream.of(clientDestAddrField, clientPortField).anyMatch(f->f.getText().isBlank()));
+ }
+ else if(tunnelType.getSelectedToggle().equals(serverTunnelRadioButton)) {
+ addButton.setDisable(Stream.of(serverHostField, serverPortField, serverKeyField, serverAddrField).anyMatch(f->f.getText().isBlank()));
+ }
+ else if(tunnelType.getSelectedToggle().equals(socksProxyRadioButton)) {
+ addButton.setDisable(Stream.of(socksPortField).anyMatch(f->f.getText().isBlank()));
+ }
+ }
+
+ @FXML
+ private void initialize() {
+
+ serverTunnelConfigPane.setVisible(false);
+ socksProxyConfigPane.setVisible(false);
+
+ addButton.setOnAction(ev->{
+ try {
+ var controller = Gui.instance.getController();
+ var tunnelControl = controller.getRouterWrapper().getTunnelControl();
+ var tunnels = tunnelControl.getTunnels();
+ if (tunnelType.getSelectedToggle().equals(clientTunnelRadioButton)) {
+ Tunnel t = new TunnelControl.ClientTunnel(clientDestAddrField.getText(), Integer.parseInt(clientPortField.getText()));
+ tunnels.add(t);
+ controller.tunnelTableList.add(t);
+ } else if (tunnelType.getSelectedToggle().equals(serverTunnelRadioButton)) {
+ Tunnel t = new TunnelControl.ServerTunnel(serverHostField.getText(), Integer.parseInt(serverPortField.getText()), new TunnelControl.KeyPair(serverKeyField.getText()), tunnelControl.getTunnelControlTempDir());
+ tunnels.add(t);
+ controller.tunnelTableList.add(t);
+ } else if (tunnelType.getSelectedToggle().equals(socksProxyRadioButton)) {
+ Tunnel t = new TunnelControl.SocksTunnel(Integer.parseInt(socksPortField.getText()));
+ tunnels.add(t);
+ controller.tunnelTableList.add(t);
+ }
+ clientTunnelConfigPane.getScene().getWindow().hide();
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ }
+ });
+
+ TextField[] allTextFields = new TextField[] {clientDestAddrField, clientPortField, serverHostField, serverPortField, serverKeyField, serverAddrField, socksPortField};
+ for(TextField f : allTextFields) {
+ f.textProperty().addListener((observable, oldValue, newValue) -> updateAddButtonState());
+ }
+
+
+ cancelButton.setOnAction(e->{
+ clientTunnelConfigPane.getScene().getWindow().hide();
+ });
+
+ tunnelType.selectedToggleProperty().addListener((ov, oldToggle, newToggle)-> {
+ try {
+ var tunnelControl = Gui.instance.getController().getRouterWrapper().getTunnelControl();
+ clientTunnelConfigPane.setVisible(false);
+ serverTunnelConfigPane.setVisible(false);
+ socksProxyConfigPane.setVisible(false);
+ if (newToggle.equals(clientTunnelRadioButton)) clientTunnelConfigPane.setVisible(true);
+ if (newToggle.equals(serverTunnelRadioButton)) {
+ var keyPair = tunnelControl.genKeyPair();
+ serverKeyField.setText(keyPair.seckey + "," + keyPair.pubkey);
+ serverAddrField.setText(keyPair.b32Dest);
+ serverTunnelConfigPane.setVisible(true);
+ }
+ if (newToggle.equals(socksProxyRadioButton)) socksProxyConfigPane.setVisible(true);
+ updateAddButtonState();
+ }
+ catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ });
+
+ serverAddrField.setEditable(false);
+
+ serverKeyField.textProperty().addListener((observable, oldValue, newValue) -> {
+ serverAddrField.setText("");
+ String key = newValue;
+ if(key!=null && !key.isEmpty()) {
+ try {
+ TunnelControl.KeyPair keyPair = new TunnelControl.KeyPair(key);
+ serverAddrField.setText(keyPair.b32Dest);
+ }
+ catch (Exception e) {
+ // ignore exception. user may be part way through entering string
+ }
+
+ }
+ });
+
+ }
+
+}
diff --git a/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/Controller.java b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/Controller.java
new file mode 100644
index 0000000..38b0e4e
--- /dev/null
+++ b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/Controller.java
@@ -0,0 +1,198 @@
+package org.getmonero.i2p.zero.gui;
+
+import javafx.application.Platform;
+import javafx.beans.binding.DoubleBinding;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.event.Event;
+import javafx.event.EventHandler;
+import javafx.fxml.FXML;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Scene;
+import javafx.scene.control.*;
+import javafx.scene.control.cell.PropertyValueFactory;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.AnchorPane;
+import javafx.scene.layout.BorderPane;
+import javafx.stage.Modality;
+import javafx.stage.Stage;
+import org.getmonero.i2p.zero.RouterWrapper;
+import static org.getmonero.i2p.zero.TunnelControl.Tunnel;
+
+import java.text.DecimalFormat;
+import java.util.Properties;
+
+public class Controller {
+
+ private RouterWrapper routerWrapper;
+
+ @FXML
+ private BorderPane rootBorderPane;
+ @FXML private Slider bandwidthSlider;
+ @FXML private Label maxBandwidthLabel;
+ @FXML private ImageView masterToggle;
+ @FXML private AnchorPane bandwidthDisabledOverlay;
+ @FXML private Tab bandwidthTab;
+ @FXML private Tab tunnelsTab;
+ @FXML private Tab helpTab;
+ @FXML private Label statusLabel;
+ @FXML private Button tunnelAddButton;
+ @FXML private Button tunnelRemoveButton;
+ @FXML private TableView tunnelsTableView;
+ @FXML private TableColumn typeCol;
+ @FXML private TableColumn hostCol;
+ @FXML private TableColumn portCol;
+ @FXML private TableColumn i2PCol;
+ @FXML private Label bandwidthIn1s;
+ @FXML private Label bandwidthIn5m;
+ @FXML private Label bandwidthInAll;
+ @FXML private Label totalTransferredIn;
+ @FXML private Label bandwidthOut1s;
+ @FXML private Label bandwidthOut5m;
+ @FXML private Label bandwidthOutAll;
+ @FXML private Label totalTransferredOut;
+
+ DecimalFormat format2dp = new DecimalFormat("0.00");
+ private boolean masterState = true;
+ public final ObservableList tunnelTableList = FXCollections.observableArrayList();
+
+ private Stage getStage() {
+ return (Stage) rootBorderPane.getScene().getWindow();
+ }
+
+ @FXML private void initialize() {
+
+ typeCol.setCellValueFactory(new PropertyValueFactory("type"));
+ hostCol.setCellValueFactory(new PropertyValueFactory("host"));
+ portCol.setCellValueFactory(new PropertyValueFactory("port"));
+ i2PCol.setCellValueFactory(new PropertyValueFactory("I2P"));
+
+ DoubleBinding usedWidth = typeCol.widthProperty().add(hostCol.widthProperty()).add(portCol.widthProperty());
+ i2PCol.prefWidthProperty().bind(tunnelsTableView.widthProperty().subtract(usedWidth).subtract(2));
+
+ tunnelsTableView.setItems(tunnelTableList);
+
+ tunnelsTableView.getSelectionModel().selectedItemProperty().addListener((observableValue, oldSelection, newSelection) -> {
+ if (newSelection == null) {
+ tunnelRemoveButton.setDisable(true);
+ }
+ else {
+ tunnelRemoveButton.setDisable(false);
+ }
+ });
+
+ tunnelRemoveButton.setOnAction(e->{
+ Tunnel t = tunnelsTableView.getSelectionModel().getSelectedItem();
+ getRouterWrapper().getTunnelControl().getTunnels().remove(t);
+ tunnelTableList.remove(t);
+ t.destroy();
+ tunnelRemoveButton.setDisable(true);
+ });
+
+ tunnelAddButton.setOnAction(event->{
+ try {
+ Stage dialogStage = new Stage();
+ dialogStage.initModality(Modality.WINDOW_MODAL);
+ dialogStage.initOwner(getStage());
+ dialogStage.setResizable(false);
+ dialogStage.setTitle("New tunnel");
+ Scene dialogScene = new Scene(FXMLLoader.load(getClass().getResource("addTunnel.fxml")));
+ dialogScene.getStylesheets().add("org/getmonero/i2p/zero/gui/gui.css");
+ dialogStage.setScene(dialogScene);
+ dialogStage.show();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ });
+
+ EventHandler tabSelectionEventHandler = e->{
+ Stage stage = getStage();
+ if(bandwidthTab.isSelected()) {
+ stage.setWidth(360);
+ stage.setHeight(370);
+ }
+ else if(tunnelsTab.isSelected()) {
+ stage.setWidth(700);
+ }
+ };
+ bandwidthTab.setOnSelectionChanged(tabSelectionEventHandler);
+ tunnelsTab.setOnSelectionChanged(tabSelectionEventHandler);
+ helpTab.setOnSelectionChanged(tabSelectionEventHandler);
+
+ bandwidthSlider.valueProperty().addListener((observableValue, oldValue, newValue)-> {
+ maxBandwidthLabel.setText(String.format("%.1f", newValue.floatValue()) + " Mbps");
+ routerWrapper.debouncedUpdateBandwidthLimitKBPerSec((int) Math.round(1024d*newValue.doubleValue()/8d));
+ });
+
+ masterToggle.setOnMouseClicked(e->{
+ masterState = !masterState;
+ bandwidthDisabledOverlay.setVisible(!masterState);
+
+ if(masterState) {
+ masterToggle.setImage(new Image("org/getmonero/i2p/zero/gui/toggle-on.png"));
+ statusLabel.setVisible(true);
+ routerWrapper.start();
+ tunnelAddButton.setDisable(false);
+ }
+ else {
+ masterToggle.setImage(new Image("org/getmonero/i2p/zero/gui/toggle-off.png"));
+ statusLabel.setVisible(false);
+ routerWrapper.stop();
+ tunnelTableList.clear();
+ tunnelAddButton.setDisable(true);
+ }
+ });
+
+ startRouter();
+
+ var bandwidthUpdateThread = new Thread(()->{
+ while(!Gui.instance.isStopping()) {
+ if (routerWrapper.isStarted()) {
+ Platform.runLater(() -> {
+ bandwidthIn1s.setText(format2dp.format(routerWrapper.get1sRateInKBps()) + " KBps");
+ bandwidthIn5m.setText(format2dp.format(routerWrapper.get5mRateInKBps()) + " KBps");
+ bandwidthInAll.setText(format2dp.format(routerWrapper.getAvgRateInKBps()) + " KBps");
+ totalTransferredIn.setText(format2dp.format(routerWrapper.getTotalInMB()) + " MB ");
+ bandwidthOut1s.setText(format2dp.format(routerWrapper.get1sRateOutKBps()) + " KBps");
+ bandwidthOut5m.setText(format2dp.format(routerWrapper.get5mRateOutKBps()) + " KBps");
+ bandwidthOutAll.setText(format2dp.format(routerWrapper.getAvgRateOutKBps()) + " KBps");
+ totalTransferredOut.setText(format2dp.format(routerWrapper.getTotalOutMB()) + " MB ");
+ });
+ }
+ try { Thread.sleep(1000); } catch (InterruptedException e) {}
+ }
+ });
+ bandwidthUpdateThread.start();
+
+ Runtime.getRuntime().addShutdownHook(new Thread(()->bandwidthUpdateThread.interrupt()));
+
+ }
+
+ public RouterWrapper getRouterWrapper() {
+ return routerWrapper;
+ }
+
+ private int getBandwidthLimitKBPerSec() {
+ return (int)Math.round(bandwidthSlider.getValue()*1024d/8d);
+ }
+
+ private void startRouter() {
+
+ // need to launch Gui with parameters: --i2p.dir.base= and --i2p.dir.config=
+
+ var params = Gui.instance.getParameters().getNamed();
+
+ Properties routerProperties = new Properties();
+ routerProperties.put("i2p.dir.base", params.get("i2p.dir.base"));
+ routerProperties.put("i2p.dir.config", params.get("i2p.dir.config"));
+ routerProperties.put("i2np.inboundKBytesPerSecond", getBandwidthLimitKBPerSec());
+ routerProperties.put("i2np.outboundKBytesPerSecond", getBandwidthLimitKBPerSec());
+ routerProperties.put("router.sharePercentage", 80);
+ routerWrapper = new RouterWrapper(routerProperties);
+ routerWrapper.start();
+
+ }
+
+
+}
diff --git a/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/Gui.java b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/Gui.java
new file mode 100644
index 0000000..0e502c4
--- /dev/null
+++ b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/Gui.java
@@ -0,0 +1,65 @@
+package org.getmonero.i2p.zero.gui;
+
+import java.awt.Taskbar;
+import java.awt.image.BufferedImage;
+import java.util.List;
+
+import javafx.application.Application;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Parent;
+import javafx.scene.Scene;
+import javafx.scene.image.Image;
+import javafx.stage.Stage;
+
+import javax.imageio.ImageIO;
+
+public class Gui extends Application {
+
+
+ public static Gui instance;
+ private Controller controller;
+ private boolean isStopping = false;
+
+ @Override
+ public void start(Stage primaryStage) throws Exception{
+
+ instance = this;
+ String osName = System.getProperty("os.name");
+ if(osName.startsWith("Mac")) {
+ Taskbar taskbar = Taskbar.getTaskbar();
+ BufferedImage image = ImageIO.read(getClass().getResource("icon.png"));
+ taskbar.setIconImage(image);
+ }
+ else primaryStage.getIcons().add(new Image(getClass().getResourceAsStream("icon.png")));
+
+ FXMLLoader loader = new FXMLLoader(getClass().getResource("gui.fxml"));
+ Parent root = loader.load();
+ controller = loader.getController();
+
+ primaryStage.setTitle("I2P-zero");
+ primaryStage.setMinWidth(360);
+ primaryStage.setMinHeight(370);
+ Scene scene = new Scene(root);
+ scene.getStylesheets().add("org/getmonero/i2p/zero/gui/gui.css");
+ primaryStage.setScene(scene);
+ primaryStage.show();
+
+ }
+
+ @Override
+ public void stop() throws Exception {
+ isStopping = true;
+ }
+
+ public boolean isStopping() {
+ return isStopping;
+ }
+
+ public Controller getController() {
+ return controller;
+ }
+
+ public static void main(String[] args) {
+ launch(args);
+ }
+}
diff --git a/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/OpenSans-Regular.ttf b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/OpenSans-Regular.ttf
new file mode 100755
index 0000000..2e31d02
Binary files /dev/null and b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/OpenSans-Regular.ttf differ
diff --git a/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/addTunnel.fxml b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/addTunnel.fxml
new file mode 100644
index 0000000..6d16521
--- /dev/null
+++ b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/addTunnel.fxml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/gui.css b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/gui.css
new file mode 100644
index 0000000..65dcd1c
--- /dev/null
+++ b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/gui.css
@@ -0,0 +1,44 @@
+@font-face {
+ font-family: 'Open Sans Regular';
+ src: url('OpenSans-Regular.ttf');
+}
+
+.tab .tab-label {
+ -fx-font-size: 12px;
+ -fx-font-family: 'Open Sans Regular';
+}
+
+.text-area {
+ -fx-font-size: 13px;
+ -fx-font-family: 'Open Sans Regular';
+}
+
+.radio-button {
+ -fx-font-size: 13px;
+ -fx-font-family: 'Open Sans Regular';
+}
+
+.label {
+ -fx-font-size: 13px;
+ -fx-font-family: 'Open Sans Regular';
+}
+
+.button {
+ -fx-font-size: 13px;
+ -fx-font-family: 'Open Sans Regular';
+}
+
+.text-field {
+ -fx-font-size: 13px;
+ -fx-font-family: 'Open Sans Regular';
+}
+
+.slider {
+ -fx-font-size: 12px;
+ -fx-font-family: 'Open Sans Regular';
+}
+
+.table-view {
+ -fx-font-size: 13px;
+ -fx-font-family: 'Open Sans Regular';
+}
diff --git a/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/gui.fxml b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/gui.fxml
new file mode 100644
index 0000000..1d9264b
--- /dev/null
+++ b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/gui.fxml
@@ -0,0 +1,161 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/i2p-zero.png b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/i2p-zero.png
new file mode 100644
index 0000000..c69b02d
Binary files /dev/null and b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/i2p-zero.png differ
diff --git a/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/icon.png b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/icon.png
new file mode 100644
index 0000000..481ff76
Binary files /dev/null and b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/icon.png differ
diff --git a/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/toggle-off.png b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/toggle-off.png
new file mode 100644
index 0000000..f73c242
Binary files /dev/null and b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/toggle-off.png differ
diff --git a/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/toggle-on.png b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/toggle-on.png
new file mode 100644
index 0000000..f7523f9
Binary files /dev/null and b/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/toggle-on.png differ
diff --git a/org.getmonero.i2p.zero/org.getmonero.i2p.zero.iml b/org.getmonero.i2p.zero/org.getmonero.i2p.zero.iml
new file mode 100644
index 0000000..0c99e01
--- /dev/null
+++ b/org.getmonero.i2p.zero/org.getmonero.i2p.zero.iml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/module-info.java b/org.getmonero.i2p.zero/src/module-info.java
similarity index 83%
rename from src/module-info.java
rename to org.getmonero.i2p.zero/src/module-info.java
index 36bf536..04f69f2 100644
--- a/src/module-info.java
+++ b/org.getmonero.i2p.zero/src/module-info.java
@@ -6,4 +6,5 @@ module org.getmonero.i2p.zero {
requires sam;
requires i2ptunnel;
requires jdk.crypto.ec;
+ exports org.getmonero.i2p.zero;
}
\ No newline at end of file
diff --git a/src/org/getmonero/i2p/zero/Main.java b/org.getmonero.i2p.zero/src/org/getmonero/i2p/zero/Main.java
similarity index 62%
rename from src/org/getmonero/i2p/zero/Main.java
rename to org.getmonero.i2p.zero/src/org/getmonero/i2p/zero/Main.java
index 5218e56..cf73c7d 100644
--- a/src/org/getmonero/i2p/zero/Main.java
+++ b/org.getmonero.i2p.zero/src/org/getmonero/i2p/zero/Main.java
@@ -4,8 +4,6 @@ import java.io.File;
import java.util.Properties;
import java.util.stream.Collectors;
-import net.i2p.router.Router;
-
public class Main {
public static void main(String[] args) {
@@ -37,37 +35,7 @@ public class Main {
System.out.println("Options set: "
+ p.entrySet().stream().map(e->"--"+e.getKey()+"="+e.getValue()).collect(Collectors.joining(" ")));
- Router router = new Router(p);
-
- new Thread(()->{
- router.setKillVMOnEnd(true);
- router.runRouter();
- }).start();
-
- new Thread(()->{
- try {
- while(true) {
- if(router.isAlive()) {
- break;
- }
- else {
- Thread.sleep(1000);
- System.out.println("Waiting for I2P router to start...");
- }
- }
-
- new Thread(new TunnelControl(router, new File(new File(p.getProperty("i2p.dir.config")), "tunnel"))).start();
-
- }
- catch (Exception e) {
- e.printStackTrace();
- }
- }).start();
-
- Runtime.getRuntime().addShutdownHook(new Thread(()->{
- System.out.println("I2P router will shut down gracefully");
- router.shutdownGracefully();
- }));
+ new RouterWrapper(p).start();
}
diff --git a/org.getmonero.i2p.zero/src/org/getmonero/i2p/zero/RouterWrapper.java b/org.getmonero.i2p.zero/src/org/getmonero/i2p/zero/RouterWrapper.java
new file mode 100644
index 0000000..c2ecd0f
--- /dev/null
+++ b/org.getmonero.i2p.zero/src/org/getmonero/i2p/zero/RouterWrapper.java
@@ -0,0 +1,165 @@
+package org.getmonero.i2p.zero;
+
+import net.i2p.router.Router;
+import net.i2p.router.transport.FIFOBandwidthRefiller;
+
+import java.io.File;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Properties;
+
+public class RouterWrapper {
+
+ private Router router;
+ private boolean started = false;
+ private Properties routerProperties;
+ private TunnelControl tunnelControl;
+
+ public RouterWrapper(Properties p) {
+ this.routerProperties = p;
+ }
+
+ public boolean isStarted() {
+ return started;
+ }
+
+ public void start() {
+
+ new Thread(()-> {
+ if(started) return;
+ started = true;
+
+ router = new Router(routerProperties);
+
+ new Thread(()->{
+ router.setKillVMOnEnd(false);
+ router.runRouter();
+ }).start();
+
+ new Thread(()->{
+ try {
+ while(true) {
+ if(router.isAlive()) {
+ break;
+ }
+ else {
+ Thread.sleep(1000);
+ System.out.println("Waiting for I2P router to start...");
+ }
+ }
+
+ tunnelControl = new TunnelControl(router, new File(new File(routerProperties.getProperty("i2p.dir.config")), "tunnel"));
+ new Thread(tunnelControl).start();
+
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ }
+ }).start();
+
+ Runtime.getRuntime().addShutdownHook(new Thread(()->stop()));
+
+ }).start();
+
+ }
+
+ public void stop() {
+ if(!started) return;
+ started = false;
+ tunnelControl.stop();
+ System.out.println("I2P router will shut down gracefully");
+ router.shutdownGracefully();
+ }
+
+ long lastTriggerTimestamp = 0;
+ public void debouncedUpdateBandwidthLimitKBPerSec(int n) {
+ long triggerTime = new Date().getTime();
+ lastTriggerTimestamp = triggerTime;
+ new Thread(()->{
+ try { Thread.sleep(2000); } catch(InterruptedException e) {}
+ if(lastTriggerTimestamp==triggerTime) {
+ // nothing happened after we were triggered, so proceed
+ updateBandwidthLimitKBPerSec(n);
+ }
+ }).start();
+ }
+
+ public void updateBandwidthLimitKBPerSec(int n) {
+ routerProperties.put("i2np.inboundKBytesPerSecond", n);
+ routerProperties.put("i2np.outboundKBytesPerSecond", n);
+
+ var changes = new HashMap();
+
+ final int DEF_BURST_PCT = 10;
+ final int DEF_BURST_TIME = 20;
+
+ int inboundRate = n;
+ int outboundRate = n;
+
+ {
+ float rate = inboundRate / 1.024f;
+ float kb = DEF_BURST_TIME * rate;
+ changes.put(FIFOBandwidthRefiller.PROP_INBOUND_BURST_BANDWIDTH, Integer.toString(Math.round(rate)));
+ changes.put(FIFOBandwidthRefiller.PROP_INBOUND_BANDWIDTH_PEAK, Integer.toString(Math.round(kb)));
+ rate -= Math.min(rate * DEF_BURST_PCT / 100, 50);
+ changes.put(FIFOBandwidthRefiller.PROP_INBOUND_BANDWIDTH, Integer.toString(Math.round(rate)));
+ }
+ {
+ float rate = outboundRate / 1.024f;
+ float kb = DEF_BURST_TIME * rate;
+ changes.put(FIFOBandwidthRefiller.PROP_OUTBOUND_BURST_BANDWIDTH, Integer.toString(Math.round(rate)));
+ changes.put(FIFOBandwidthRefiller.PROP_OUTBOUND_BANDWIDTH_PEAK, Integer.toString(Math.round(kb)));
+ rate -= Math.min(rate * DEF_BURST_PCT / 100, 50);
+ changes.put(FIFOBandwidthRefiller.PROP_OUTBOUND_BANDWIDTH, Integer.toString(Math.round(rate)));
+ }
+
+ boolean saved = router.saveConfig(changes, null);
+ // this has to be after the save
+ router.getContext().bandwidthLimiter().reinitialize();
+ if(!saved) throw new RuntimeException("Error saving the new bandwidth limit");
+
+ }
+
+ public double get1sRateInKBps() {
+ try { return router.getContext().bandwidthLimiter().getReceiveBps()/1024d; } catch (Exception e) { return 0; }
+ }
+ public double get1sRateOutKBps() {
+ try { return router.getContext().bandwidthLimiter().getSendBps()/1024d; } catch (Exception e) { return 0; }
+ }
+ public double get5mRateInKBps() {
+ try {
+ return router.getContext().statManager().getRate("bw.recvRate").getRate(5*60*1000).getAverageValue()/1024d;
+ }
+ catch (Exception e) { return 0; }
+ }
+ public double get5mRateOutKBps() {
+ try {
+ return router.getContext().statManager().getRate("bw.sendRate").getRate(5*60*1000).getAverageValue()/1024d;
+ }
+ catch (Exception e) { return 0; }
+ }
+ public double getAvgRateInKBps() {
+ try {
+ return router.getContext().statManager().getRate("bw.recvRate").getLifetimeAverageValue()/1024d;
+ }
+ catch (Exception e) { return 0; }
+ }
+ public double getAvgRateOutKBps() {
+ try {
+ return router.getContext().statManager().getRate("bw.sendRate").getLifetimeAverageValue()/1024d;
+ }
+ catch (Exception e) { return 0; }
+ }
+ public double getTotalInMB() {
+ try { return router.getContext().bandwidthLimiter().getTotalAllocatedInboundBytes()/1048576d; } catch (Exception e) { return 0; }
+ }
+ public double getTotalOutMB() {
+ try { return router.getContext().bandwidthLimiter().getTotalAllocatedOutboundBytes()/1048576d; } catch (Exception e) { return 0; }
+ }
+
+ public TunnelControl getTunnelControl() {
+ return tunnelControl;
+ }
+
+
+}
diff --git a/org.getmonero.i2p.zero/src/org/getmonero/i2p/zero/TunnelControl.java b/org.getmonero.i2p.zero/src/org/getmonero/i2p/zero/TunnelControl.java
new file mode 100644
index 0000000..4288779
--- /dev/null
+++ b/org.getmonero.i2p.zero/src/org/getmonero/i2p/zero/TunnelControl.java
@@ -0,0 +1,277 @@
+package org.getmonero.i2p.zero;
+
+import net.i2p.I2PAppContext;
+import net.i2p.app.ClientAppManager;
+import net.i2p.app.ClientAppManagerImpl;
+import net.i2p.client.I2PClient;
+import net.i2p.client.I2PClientFactory;
+import net.i2p.data.Base64;
+import net.i2p.data.Destination;
+import net.i2p.i2ptunnel.I2PTunnel;
+import net.i2p.router.Router;
+import net.i2p.sam.SAMBridge;
+
+import java.io.*;
+import java.math.BigInteger;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+public class TunnelControl implements Runnable {
+
+ private List tunnels = new ArrayList<>();
+ private int clientPortSeq = 30000;
+ private Router router;
+ private boolean stopping = false;
+ private ServerSocket controlServerSocket;
+ private File tunnelControlTempDir;
+
+
+ public TunnelControl(Router router, File tunnelControlTempDir) {
+ this.router = router;
+ tunnelControlTempDir.delete();
+ tunnelControlTempDir.mkdir();
+ this.tunnelControlTempDir = tunnelControlTempDir;
+ }
+
+ public interface Tunnel {
+ public String getType();
+ public String getHost();
+ public String getPort();
+ public String getI2P();
+ public void destroy();
+ }
+
+ public static class ClientTunnel implements Tunnel {
+ public String dest;
+ public int port;
+ public I2PTunnel tunnel;
+ public ClientTunnel(String dest, int port) {
+ this.dest = dest;
+ this.port = port;
+ new Thread(()->{
+ tunnel = new I2PTunnel(new String[]{"-die", "-nocli", "-e", "config localhost 7654", "-e", "client " + port + " " + dest});
+ }).start();
+ }
+ public void destroy() {
+ new Thread(()->{
+ tunnel.runClose(new String[]{"forced", "all"}, tunnel);
+ }).start();
+ }
+
+ @Override public String getType() { return "client"; }
+ @Override public String getHost() { return "localhost"; }
+ @Override public String getPort() { return port+""; }
+ @Override public String getI2P() { return dest; }
+ }
+ public static class ServerTunnel implements Tunnel {
+ public String dest;
+ public String host;
+ public int port;
+ public I2PTunnel tunnel;
+ public KeyPair keyPair;
+ public ServerTunnel(String host, int port, KeyPair keyPair, File tunnelControlTempDir) throws Exception {
+ this.host = host;
+ this.port = port;
+ this.keyPair = keyPair;
+ this.dest = keyPair.b32Dest;
+
+ String uuid = new BigInteger(128, new Random()).toString(16);
+ String seckeyPath = tunnelControlTempDir.getAbsolutePath() + File.separator + "seckey."+uuid+".dat";
+
+ Files.write(Path.of(seckeyPath), Base64.decode(keyPair.seckey));
+
+ // listen using the I2P server keypair, and forward incoming connections to a destination and port
+ new Thread(()->{
+ tunnel = new I2PTunnel(new String[]{"-die", "-nocli", "-e", "server "+host+" "+port+" " + seckeyPath});
+ }).start();
+ }
+ public void destroy() {
+ new Thread(()->{
+ tunnel.runClose(new String[]{"forced", "all"}, tunnel);
+ }).start();
+ }
+ @Override public String getType() { return "server"; }
+ @Override public String getHost() { return host; }
+ @Override public String getPort() { return port+""; }
+ @Override public String getI2P() { return dest; }
+
+ }
+ public static class SocksTunnel implements Tunnel {
+ public int port;
+ public I2PTunnel tunnel;
+ public SocksTunnel(int port) {
+ this.port = port;
+ new Thread(()->{
+ tunnel = new I2PTunnel(new String[]{"-die", "-nocli", "-e", "sockstunnel " + port});
+ }).start();
+ }
+ public void destroy() {
+ new Thread(()->{
+ tunnel.runClose(new String[]{"forced", "all"}, tunnel);
+ }).start();
+ }
+ @Override public String getType() { return "socks"; }
+ @Override public String getHost() { return "localhost"; }
+ @Override public String getPort() { return port+""; }
+ @Override public String getI2P() { return "n/a"; }
+ }
+
+ public List getTunnels() {
+ return tunnels;
+ }
+
+ public KeyPair genKeyPair() throws Exception {
+ ByteArrayOutputStream seckey = new ByteArrayOutputStream();
+ ByteArrayOutputStream pubkey = new ByteArrayOutputStream();
+ I2PClient client = I2PClientFactory.createClient();
+ Destination d = client.createDestination(seckey);
+ d.writeBytes(pubkey);
+ String b32Dest = d.toBase32();
+ return new KeyPair(Base64.encode(seckey.toByteArray()), Base64.encode(pubkey.toByteArray()), b32Dest);
+ }
+
+ public static class KeyPair {
+ public String seckey;
+ public String pubkey;
+ public String b32Dest;
+ public KeyPair(String seckey, String pubkey, String b32Dest) {
+ this.seckey = seckey;
+ this.pubkey = pubkey;
+ this.b32Dest = b32Dest;
+ }
+ public KeyPair(String base64EncodedCommaDelimitedPair) throws Exception {
+ String[] a = base64EncodedCommaDelimitedPair.split(",");
+ this.seckey = a[0];
+ this.pubkey = a[1];
+ Destination d = new Destination();
+ d.readBytes(new ByteArrayInputStream(Base64.decode(this.seckey)));
+ this.b32Dest = d.toBase32();
+ }
+ }
+
+ @Override
+ public void run() {
+
+ // listen for socket connections to the tunnel controller.
+ // listen for the commands on port 30000:
+ // server.create // returns a newly created destination public key, which will listen for i2p connections and forward them to the specified host and port
+ // server.destroy // closes the tunnel listening for connections on the specified destination public key, and returns OK
+ // client.create // returns a newly created localhost port number, where connections will be sent over I2P to the destination public key
+ // client.destroy // closes the tunnel listening for connections on the specified port, and returns OK
+ // socks.create // creates a socks proxy listening on the specified port
+ // socks.destroy // closes the socks proxy listening on the specified port, and returns OK
+ //
+ // send a command with bash: exec 3<>/dev/tcp/localhost/30000; echo "server.create localhost 80" >&3; cat <&3
+ //
+
+ try {
+ controlServerSocket = new ServerSocket(clientPortSeq++, 0, InetAddress.getLoopbackAddress());
+ while (!stopping) {
+ try (var socket = controlServerSocket.accept()) {
+ var out = new PrintWriter(socket.getOutputStream(), true);
+ var in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
+
+ var args = in.readLine().split(" ");
+
+ switch(args[0]) {
+
+ case "server.create":
+ String destHost = args[1];
+ int destPort = Integer.parseInt(args[2]);
+ var tunnel = new ServerTunnel(destHost, destPort, genKeyPair(), getTunnelControlTempDir());
+ tunnels.add(tunnel);
+ out.println(tunnel.dest);
+ break;
+
+ case "server.destroy":
+ String dest = args[1];
+ new ArrayList<>(tunnels).stream().filter(t->t.getType().equals("server") && ((ServerTunnel) t).dest.equals(dest)).forEach(t->{
+ t.destroy();
+ tunnels.remove(t);
+ });
+ out.println("OK");
+ break;
+
+ case "client.create":
+ String destPubKey = args[1];
+ var clientTunnel = new ClientTunnel(destPubKey, clientPortSeq++);
+ tunnels.add(clientTunnel);
+ out.println(clientTunnel.port);
+ break;
+
+ case "client.destroy": {
+ int port = Integer.parseInt(args[1]);
+ new ArrayList<>(tunnels).stream().filter(t->t.getType().equals("client") && ((ClientTunnel) t).port == port).forEach(t->{
+ t.destroy();
+ tunnels.remove(t);
+ });
+ out.println("OK");
+ break;
+ }
+
+ case "socks.create": {
+ int port = Integer.parseInt(args[1]);
+ tunnels.add(new SocksTunnel(port));
+ out.println("OK");
+ break;
+ }
+
+ case "socks.destroy":
+ int port = Integer.parseInt(args[1]);
+ new ArrayList<>(tunnels).stream().filter(t->t.getType().equals("socks") && ((SocksTunnel) t).port == port).forEach(t->{
+ t.destroy();
+ tunnels.remove(t);
+ });
+ out.println("OK");
+ break;
+
+ case "sam.create":
+ String[] samArgs = new String[]{"sam.keys", "127.0.0.1", "7656", "i2cp.tcp.host=127.0.0.1", "i2cp.tcp.port=7654"};
+ I2PAppContext context = router.getContext();
+ ClientAppManager mgr = new ClientAppManagerImpl(context);
+ SAMBridge samBridge = new SAMBridge(context, mgr, samArgs);
+ samBridge.startup();
+ out.println("OK");
+ break;
+
+ }
+
+ }
+ catch (Exception e) {
+ if(!e.getMessage().contains("Socket closed")) e.printStackTrace();
+ }
+ }
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ }
+ finally {
+ if(controlServerSocket!=null) try {
+ controlServerSocket.close();
+ }
+ catch (Exception e) { e.printStackTrace(); }
+ }
+
+ }
+
+ public void stop() {
+ stopping = true;
+ try {
+ controlServerSocket.close();
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ public File getTunnelControlTempDir() {
+ return tunnelControlTempDir;
+ }
+
+
+}
diff --git a/resources/launch-gui.bat b/resources/launch-gui.bat
new file mode 100644
index 0000000..bb8293d
--- /dev/null
+++ b/resources/launch-gui.bat
@@ -0,0 +1,3 @@
+@echo off
+set DIR=%~dp0
+"%DIR%\java" -cp %DIR%\..\i2p.base\jbigi.jar -m org.getmonero.i2p.zero.gui --i2p.dir.base=%DIR%\..\i2p.base --i2p.dir.config=%DIR%\..\i2p.config %*
\ No newline at end of file
diff --git a/resources/launch-gui.sh b/resources/launch-gui.sh
new file mode 100755
index 0000000..f0761f7
--- /dev/null
+++ b/resources/launch-gui.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+if [ $(uname -s) = Darwin ]; then
+ basedir=$(dirname $(cd "$(dirname "$0")"; pwd -P))
+else
+ basedir=$(dirname $(dirname $(readlink -fm $0)))
+fi
+
+$basedir/bin/java -cp $basedir/i2p.base/jbigi.jar -m org.getmonero.i2p.zero.gui --i2p.dir.base=$basedir/i2p.base --i2p.dir.config=$basedir/i2p.config
diff --git a/src/org/getmonero/i2p/zero/TunnelControl.java b/src/org/getmonero/i2p/zero/TunnelControl.java
deleted file mode 100644
index eddd292..0000000
--- a/src/org/getmonero/i2p/zero/TunnelControl.java
+++ /dev/null
@@ -1,145 +0,0 @@
-package org.getmonero.i2p.zero;
-
-import net.i2p.I2PAppContext;
-import net.i2p.app.ClientAppManager;
-import net.i2p.app.ClientAppManagerImpl;
-import net.i2p.data.Base64;
-import net.i2p.data.Destination;
-import net.i2p.i2ptunnel.I2PTunnel;
-import net.i2p.router.Router;
-import net.i2p.sam.SAMBridge;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.InputStreamReader;
-import java.io.PrintWriter;
-import java.io.FileInputStream;
-import java.net.InetAddress;
-import java.net.ServerSocket;
-import java.nio.file.Files;
-import java.util.HashMap;
-import java.util.Map;
-
-public class TunnelControl implements Runnable {
-
- private Map clientTunnels = new HashMap<>();
- private Map serverTunnels = new HashMap<>();
- private Map socksTunnels = new HashMap<>();
- private int clientPortSeq = 30000;
- private int serverSeq = 0;
- private String tunnelConfigDirPrefix;
- private Router router;
- public TunnelControl(Router router, File tunnelConfigDir) {
- this.router = router;
- tunnelConfigDir.delete();
- tunnelConfigDir.mkdir();
- this.tunnelConfigDirPrefix = tunnelConfigDir.getAbsolutePath() + File.separator;
- }
-
- @Override
- public void run() {
-
- // listen for socket connections to the tunnel controller.
- // listen for the commands on port 30000:
- // server.create // returns a newly created destination public key, which will listen for i2p connections and forward them to the specified host and port
- // server.destroy // closes the tunnel listening for connections on the specified destination public key, and returns OK
- // client.create // returns a newly created localhost port number, where connections will be sent over I2P to the destination public key
- // client.destroy // closes the tunnel listening for connections on the specified port, and returns OK
- // socks.create // creates a socks proxy listening on the specified port
- // socks.destroy // closes the socks proxy listening on the specified port, and returns OK
- //
- // send a command with bash: exec 3<>/dev/tcp/localhost/30000; echo "server.create localhost 80" >&3; cat <&3
- //
- try (var listener = new ServerSocket(clientPortSeq++, 0, InetAddress.getLoopbackAddress())) {
- while (true) {
- try (var socket = listener.accept()) {
- var out = new PrintWriter(socket.getOutputStream(), true);
- var in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
-
- var args = in.readLine().split(" ");
-
- if(args[0].equals("server.create")) {
- String destHost = args[1];
- String destPort = args[2];
- String seckeyPath = tunnelConfigDirPrefix + "seckey."+serverSeq+".dat";
- String pubkeyPath = tunnelConfigDirPrefix + "pubkey."+serverSeq+".dat";
- serverSeq++;
- new I2PTunnel(new String[]{"-die", "-nocli", "-e", "genkeys " + seckeyPath + " " + pubkeyPath});
- String destPubKey = Base64.encode(Files.readAllBytes(new File(pubkeyPath).toPath()));
-
- // listen using the I2P server keypair, and forward incoming connections to a destination and port
- new Thread(()->{
- I2PTunnel t = new I2PTunnel(new String[]{"-die", "-nocli", "-e", "server "+destHost+" "+destPort+" " + seckeyPath});
- serverTunnels.put(destPubKey, t);
- }).start();
- try (var fis = new FileInputStream(seckeyPath)) {
- Destination d = new Destination();
- d.readBytes(fis);
- out.println(d.toBase32());
- }
- }
- else if(args[0].equals("server.destroy")) {
- String destPubKey = args[1];
- new Thread(()->{
- var t = serverTunnels.get(destPubKey);
- serverTunnels.remove(destPubKey);
- t.runClose(new String[]{"forced", "all"}, t);
- }).start();
- out.println("OK");
- }
- else if(args[0].equals("client.create")) {
- String destPubKey = args[1];
- int port = clientPortSeq++;
- new Thread(()->{
- var t = new I2PTunnel(new String[]{"-die", "-nocli", "-e", "config localhost 7654", "-e", "client " + port + " " + destPubKey});
- clientTunnels.put(port, t);
- }).start();
- out.println(port);
- }
- else if(args[0].equals("client.destroy")) {
- int port = Integer.parseInt(args[1]);
- new Thread(()->{
- var t = clientTunnels.get(port);
- clientTunnels.remove(port);
- t.runClose(new String[]{"forced", "all"}, t);
- }).start();
- out.println("OK");
- }
- else if(args[0].equals("socks.create")) {
- int port = Integer.parseInt(args[1]);
- new Thread(()->{
- var t = new I2PTunnel(new String[]{"-die", "-nocli", "-e", "sockstunnel " + port});
- socksTunnels.put(port, t);
- }).start();
- out.println("OK");
- }
- else if(args[0].equals("socks.destroy")) {
- int port = Integer.parseInt(args[1]);
- new Thread(()->{
- var t = socksTunnels.get(port);
- socksTunnels.remove(port);
- t.runClose(new String[]{"forced", "all"}, t);
- }).start();
- out.println("OK");
- }
- else if(args[0].equals("sam.create")) {
- String[] samArgs = new String[]{"sam.keys", "127.0.0.1", "7656", "i2cp.tcp.host=127.0.0.1", "i2cp.tcp.port=7654"};
- I2PAppContext context = router.getContext();
- ClientAppManager mgr = new ClientAppManagerImpl(context);
- SAMBridge samBridge = new SAMBridge(context, mgr, samArgs);
- samBridge.startup();
- out.println("OK");
- }
-
- }
- catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
- catch (Exception e) {
- e.printStackTrace();
- }
-
- }
-}