produce fat JAR for non-module dependencies, to prevent split-package warnings and classpath issues

added ability to create HTTP proxy tunnel
quit immediately it we detect i2p-zero is already running (by checking for tunnelcontrol port 30000 in use)
after attempted shutdown, force shutdown after 2 seconds. this is because if there is a shutdown prior to tunnels having opened, there could be a delay of up to 20 seconds before those opening tunnels exit their sleep and start responding
added GUI tab to set up a personal eepsite (via embedded jetty)
pull/10/head
knaccc 5 years ago
parent 1c3c4ce35f
commit 65a4f8b659

@ -83,18 +83,13 @@ I2P router launched.
Press Ctrl-C to gracefully shut down the router (or send the SIGINT signal to the process).
```
## Check that the I2P router is running and that it is listening for SAM connections
`fuser 7656/tcp`
## Tunnel control
Note that it may take a short while for new tunnels to be set up.
Call the `dist/linux/router/bin/tunnel-control.sh` script as follows to create and destroy tunnels:
#### Get the router reachability status. Returns a string such as "Testing", "Tirewalled", "Running", "Error"
#### Get the router reachability status. Returns a string such as "Testing", "Firewalled", "Running", "Error"
`tunnel-control.sh router.reachability`
@ -113,6 +108,7 @@ specified host and port. Note that the base 32 I2P destination address determini
`tunnel-control.sh server.state <base 32 I2P address>`
`tunnel-control.sh client.state <local port>`
`tunnel-control.sh http.state <local port>`
`tunnel-control.sh socks.state <local port>`
@ -130,6 +126,14 @@ specified host and port. Note that the base 32 I2P destination address determini
`tunnel-control.sh client.destroy <local port>`
#### Create an http proxy (for accessing .i2p web sites), listening on the specified port
`tunnel-control.sh http.create <local port>`
#### Destroy the http proxy listening on the specified port
`tunnel-control.sh http.destroy <local port>`
#### Create a socks tunnel, listening on the specified port
`tunnel-control.sh socks.create <local port>`

@ -6,6 +6,8 @@ else
basedir=$(dirname $(dirname $(readlink -fm $0)))
fi
rm -fr "$basedir/target" "$basedir/dist"
# retrieve the I2P Java sources, OpenJDK and the Ant build tool
"$basedir"/bin/import-packages.sh

@ -8,21 +8,23 @@ 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/org.getmonero.i2p.zero/classes $(find org.getmonero.i2p.zero/src -name '*.java')
$JAVA_HOME/bin/javac --module-path import/lib:import/javafx-sdks/linux/javafx-sdk-$JAVAFX_VERSION/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
echo "*** Compiling CLI"
$JAVA_HOME/bin/javac --module-path target/modules/combined.jar -d target/classes/org.getmonero.i2p.zero $(find org.getmonero.i2p.zero/src -name '*.java')
# 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/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 .
echo "*** Packaging CLI 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/org.getmonero.i2p.zero .
echo "*** Compiling GUI"
$JAVA_HOME/bin/javac --module-path target/org.getmonero.i2p.zero.jar:target/modules/combined.jar:import/javafx-sdks/linux/javafx-sdk-$JAVAFX_VERSION/lib -d target/classes/org.getmonero.i2p.zero.gui $(find org.getmonero.i2p.zero.gui/src -name '*.java')
cp -r org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/*.{css,png,fxml,ttf} target/classes/org.getmonero.i2p.zero.gui/org/getmonero/i2p/zero/gui/
echo "*** Packaging GUI as a modular jar"
$JAVA_HOME/bin/jar --create --file target/org.getmonero.i2p.zero.gui.jar --main-class org.getmonero.i2p.zero.gui.Gui -C target/classes/org.getmonero.i2p.zero.gui .
hh
rm -fr "$basedir/dist"
for i in linux mac win linux-gui mac-gui win-gui; do mkdir -p "$basedir/dist/$i"; done
@ -39,8 +41,8 @@ for i in linux mac win; do
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 --compress 2 --no-header-files --no-man-pages
"$JAVA_HOME"/bin/jlink --module-path "${JAVA_HOME_VARIANT}/jmods":import/javafx-jmods/$i/javafx-jmods-${JAVAFX_VERSION}: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 --compress 2 --no-header-files --no-man-pages
"$JAVA_HOME"/bin/jlink --module-path "${JAVA_HOME_VARIANT}/jmods":target/modules:target/org.getmonero.i2p.zero.jar --add-modules combined,org.getmonero.i2p.zero --output dist/$i/router --compress 2 --no-header-files --no-man-pages
"$JAVA_HOME"/bin/jlink --module-path "${JAVA_HOME_VARIANT}/jmods":import/javafx-jmods/$i/javafx-jmods-${JAVAFX_VERSION}:target/modules:target/org.getmonero.i2p.zero.jar:target/org.getmonero.i2p.zero.gui.jar --add-modules combined,org.getmonero.i2p.zero,org.getmonero.i2p.zero.gui,javafx.controls,javafx.fxml,java.desktop --output dist/$i-gui/router --compress 2 --no-header-files --no-man-pages
done
for i in linux mac linux-gui mac-gui; do

@ -8,20 +8,37 @@ fi
source "$basedir/bin/java-config.sh"
cp "$basedir"/import/jetty-lib/*.jar "$basedir/import/lib/"
rm -fr "$basedir/target/lib-combined"
rm -fr "$basedir/target/lib-combined-tmp"
mkdir -p "$basedir/target/lib-combined"
mkdir -p "$basedir/target/lib-combined-tmp"
jarPaths=`find "$basedir/import/lib" -name '*.jar'`
combinedJarPath="$basedir/target/lib-combined/combined.jar"
for jarPath in $jarPaths; do unzip -quo $jarPath -d "$basedir/target/lib-combined-tmp"; done
jar cf "$combinedJarPath" -C "$basedir/target/lib-combined-tmp" .
rm -fr "$basedir/target/module-info"
mkdir -p "$basedir/target/module-info"
rm -fr "$basedir/target/modules"
mkdir -p "$basedir/target/modules"
rm -f "$basedir/target/modules"/*
for jarPath in $jarPaths; do
moduleName=$(basename "${jarPath%.*}")
echo "*** Determining dependencies for $moduleName"
"$JAVA_HOME"/bin/jdeps --module-path "$basedir/import/lib" --add-modules=ALL-MODULE-PATH --generate-module-info "$basedir/target/module-info" "$jarPath"
done
for jarPath in $jarPaths; do
moduleName=$(basename "${jarPath%.*}")
echo "*** Creating new modular jar for $moduleName"
"$JAVA_HOME"/bin/javac --module-path "$basedir/import/lib" --patch-module $moduleName=$jarPath $basedir/target/module-info/$moduleName/module-info.java
cp $jarPath "$basedir/target/modules/"
"$JAVA_HOME"/bin/jar uf "$basedir/target/modules/${moduleName}.jar" -C "$basedir/target/module-info/$moduleName" module-info.class
done
echo "*** Determining dependencies for $combinedJarPath"
"$JAVA_HOME"/bin/jdeps --add-modules=ALL-MODULE-PATH --generate-module-info "$basedir/target/module-info" "$combinedJarPath"
if [ $(uname -s) = Darwin ]; then
sed -i '' -e '$ d' "$basedir/target/module-info/combined/module-info.java"
else
sed -i '$ d' "$basedir/target/module-info/combined/module-info.java"
fi
echo 'uses org.eclipse.jetty.http.HttpFieldPreEncoder; }' >> "$basedir/target/module-info/combined/module-info.java"
echo "*** Creating new combined modular jar"
"$JAVA_HOME"/bin/javac --module-path "$combinedJarPath/combined" --patch-module combined=$combinedJarPath $basedir/target/module-info/combined/module-info.java
cp $combinedJarPath "$basedir/target/modules/"
"$JAVA_HOME"/bin/jar uf "$basedir/target/modules/combined.jar" -C "$basedir/target/module-info/combined" module-info.class

@ -37,6 +37,18 @@ 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/jetty-lib" ]; then
mkdir -p jetty-lib
wget --directory-prefix=jetty-lib http://central.maven.org/maven2/org/eclipse/jetty/jetty-server/9.4.14.v20181114/jetty-server-9.4.14.v20181114.jar
wget --directory-prefix=jetty-lib http://central.maven.org/maven2/javax/servlet/javax.servlet-api/3.1.0/javax.servlet-api-3.1.0.jar
wget --directory-prefix=jetty-lib http://central.maven.org/maven2/org/eclipse/jetty/jetty-util/9.4.14.v20181114/jetty-util-9.4.14.v20181114.jar
wget --directory-prefix=jetty-lib http://central.maven.org/maven2/org/eclipse/jetty/jetty-http/9.4.14.v20181114/jetty-http-9.4.14.v20181114.jar
wget --directory-prefix=jetty-lib http://central.maven.org/maven2/org/eclipse/jetty/jetty-io/9.4.14.v20181114/jetty-io-9.4.14.v20181114.jar
wget --directory-prefix=jetty-lib http://central.maven.org/maven2/org/slf4j/slf4j-api/1.7.25/slf4j-api-1.7.25.jar
wget --directory-prefix=jetty-lib http://central.maven.org/maven2/org/slf4j/slf4j-simple/1.7.25/slf4j-simple-1.7.25.jar
wget --directory-prefix=jetty-lib http://central.maven.org/maven2/org/eclipse/jetty/jetty-jmx/9.4.14.v20181114/jetty-jmx-9.4.14.v20181114.jar
fi
if [ ! -d "$basedir/import/javafx-sdks" ]; then
mkdir -p javafx-sdks
mkdir -p javafx-sdks/linux javafx-sdks/mac javafx-sdks/win

@ -7,11 +7,10 @@
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="module" module-name="org.getmonero.i2p.zero" />
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../import/javafx-sdks/linux/javafx-sdk-11.0.2/lib/javafx-swt.jar!/" />
<root url="jar://$MODULE_DIR$/../import/javafx-sdks/mac/javafx-sdk-11.0.2/lib/javafx-swt.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
@ -20,7 +19,7 @@
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../import/javafx-sdks/linux/javafx-sdk-11.0.2/lib/javafx.base.jar!/" />
<root url="jar://$MODULE_DIR$/../import/javafx-sdks/mac/javafx-sdk-11.0.2/lib/javafx.base.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
@ -29,7 +28,7 @@
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../import/javafx-sdks/linux/javafx-sdk-11.0.2/lib/javafx.controls.jar!/" />
<root url="jar://$MODULE_DIR$/../import/javafx-sdks/mac/javafx-sdk-11.0.2/lib/javafx.controls.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
@ -38,7 +37,7 @@
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../import/javafx-sdks/linux/javafx-sdk-11.0.2/lib/javafx.fxml.jar!/" />
<root url="jar://$MODULE_DIR$/../import/javafx-sdks/mac/javafx-sdk-11.0.2/lib/javafx.fxml.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
@ -47,7 +46,7 @@
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../import/javafx-sdks/linux/javafx-sdk-11.0.2/lib/javafx.graphics.jar!/" />
<root url="jar://$MODULE_DIR$/../import/javafx-sdks/mac/javafx-sdk-11.0.2/lib/javafx.graphics.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
@ -56,7 +55,7 @@
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../import/javafx-sdks/linux/javafx-sdk-11.0.2/lib/javafx.media.jar!/" />
<root url="jar://$MODULE_DIR$/../import/javafx-sdks/mac/javafx-sdk-11.0.2/lib/javafx.media.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
@ -65,7 +64,7 @@
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../import/javafx-sdks/linux/javafx-sdk-11.0.2/lib/javafx.swing.jar!/" />
<root url="jar://$MODULE_DIR$/../import/javafx-sdks/mac/javafx-sdk-11.0.2/lib/javafx.swing.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
@ -74,11 +73,12 @@
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../import/javafx-sdks/linux/javafx-sdk-11.0.2/lib/javafx.web.jar!/" />
<root url="jar://$MODULE_DIR$/../import/javafx-sdks/mac/javafx-sdk-11.0.2/lib/javafx.web.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
<orderEntry type="module" module-name="org.getmonero.i2p.zero" />
</component>
</module>

@ -12,12 +12,14 @@ public class AddTunnelController {
@FXML Pane clientTunnelConfigPane;
@FXML Pane serverTunnelConfigPane;
@FXML Pane httpProxyConfigPane;
@FXML Pane socksProxyConfigPane;
@FXML Button addButton;
@FXML Button cancelButton;
@FXML ToggleGroup tunnelType;
@FXML RadioButton clientTunnelRadioButton;
@FXML RadioButton serverTunnelRadioButton;
@FXML RadioButton httpProxyRadioButton;
@FXML RadioButton socksProxyRadioButton;
@FXML TextField clientDestAddrField;
@FXML TextField clientPortField;
@ -26,6 +28,7 @@ public class AddTunnelController {
@FXML TextField serverKeyField;
@FXML TextField serverAddrField;
@FXML TextField socksPortField;
@FXML TextField httpProxyPortField;
private void updateAddButtonState() {
@ -38,6 +41,9 @@ public class AddTunnelController {
else if(tunnelType.getSelectedToggle().equals(socksProxyRadioButton)) {
addButton.setDisable(Stream.of(socksPortField).anyMatch(f->f.getText().isBlank()));
}
else if(tunnelType.getSelectedToggle().equals(httpProxyRadioButton)) {
addButton.setDisable(Stream.of(httpProxyPortField).anyMatch(f->f.getText().isBlank()));
}
}
@FXML
@ -45,6 +51,7 @@ public class AddTunnelController {
serverTunnelConfigPane.setVisible(false);
socksProxyConfigPane.setVisible(false);
httpProxyConfigPane.setVisible(false);
addButton.setOnAction(ev->{
try {
@ -52,13 +59,16 @@ public class AddTunnelController {
var tunnelControl = controller.getRouterWrapper().getTunnelControl();
var tunnelList = tunnelControl.getTunnelList();
if (tunnelType.getSelectedToggle().equals(clientTunnelRadioButton)) {
Tunnel t = new TunnelControl.ClientTunnel(clientDestAddrField.getText(), Integer.parseInt(clientPortField.getText()));
Tunnel t = new TunnelControl.ClientTunnel(clientDestAddrField.getText(), Integer.parseInt(clientPortField.getText())).start();
tunnelList.addTunnel(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());
Tunnel t = new TunnelControl.ServerTunnel(serverHostField.getText(), Integer.parseInt(serverPortField.getText()), new TunnelControl.KeyPair(serverKeyField.getText()), tunnelControl.getTunnelControlTempDir()).start();
tunnelList.addTunnel(t);
} else if (tunnelType.getSelectedToggle().equals(socksProxyRadioButton)) {
Tunnel t = new TunnelControl.SocksTunnel(Integer.parseInt(socksPortField.getText()));
Tunnel t = new TunnelControl.SocksTunnel(Integer.parseInt(socksPortField.getText())).start();
tunnelList.addTunnel(t);
} else if (tunnelType.getSelectedToggle().equals(httpProxyRadioButton)) {
Tunnel t = new TunnelControl.HttpClientTunnel(Integer.parseInt(httpProxyPortField.getText())).start();
tunnelList.addTunnel(t);
}
clientTunnelConfigPane.getScene().getWindow().hide();
@ -80,17 +90,18 @@ public class AddTunnelController {
tunnelType.selectedToggleProperty().addListener((ov, oldToggle, newToggle)-> {
try {
var tunnelControl = Gui.instance.getController().getRouterWrapper().getTunnelControl();
clientTunnelConfigPane.setVisible(false);
serverTunnelConfigPane.setVisible(false);
httpProxyConfigPane.setVisible(false);
socksProxyConfigPane.setVisible(false);
if (newToggle.equals(clientTunnelRadioButton)) clientTunnelConfigPane.setVisible(true);
if (newToggle.equals(serverTunnelRadioButton)) {
var keyPair = TunnelControl.KeyPair.gen();
serverKeyField.setText(keyPair.seckey + "," + keyPair.pubkey);
serverKeyField.setText(keyPair.toString());
serverAddrField.setText(keyPair.b32Dest);
serverTunnelConfigPane.setVisible(true);
}
if (newToggle.equals(httpProxyRadioButton)) httpProxyConfigPane.setVisible(true);
if (newToggle.equals(socksProxyRadioButton)) socksProxyConfigPane.setVisible(true);
updateAddButtonState();
}
@ -106,13 +117,11 @@ public class AddTunnelController {
String key = newValue;
if(key!=null && !key.isEmpty()) {
try {
TunnelControl.KeyPair keyPair = new TunnelControl.KeyPair(key);
serverAddrField.setText(keyPair.b32Dest);
serverAddrField.setText(new TunnelControl.KeyPair(key).b32Dest);
}
catch (Exception e) {
// ignore exception. user may be part way through entering string
}
}
});

@ -15,27 +15,32 @@ import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.BorderPane;
import javafx.stage.DirectoryChooser;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.getmonero.i2p.zero.RouterWrapper;
import static org.getmonero.i2p.zero.TunnelControl.Tunnel;
import org.getmonero.i2p.zero.TunnelControl;
import static org.getmonero.i2p.zero.TunnelControl.*;
import java.io.File;
import java.text.DecimalFormat;
import java.util.List;
import java.util.Optional;
import java.util.Properties;
import java.util.stream.Stream;
public class Controller {
private RouterWrapper routerWrapper;
@FXML
private BorderPane rootBorderPane;
@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 eepSiteTab;
@FXML private Tab helpTab;
@FXML private Label statusLabel;
@FXML private Button tunnelAddButton;
@ -54,17 +59,32 @@ public class Controller {
@FXML private Label bandwidthOut5m;
@FXML private Label bandwidthOutAll;
@FXML private Label totalTransferredOut;
@FXML private CheckBox eepSiteEnableCheckbox;
@FXML private TextField eepSiteAddrField;
@FXML private TextField eepSiteSecretKeyField;
@FXML private Button eepSiteGenButton;
@FXML private TextField eepSiteContentDirField;
@FXML private Button eepSiteContentDirChooseButton;
@FXML private TextField eepSiteLogsDirField;
@FXML private Button eepSiteLogsDirChooseButton;
@FXML private CheckBox eepSiteEnableLogsCheckbox;
@FXML private CheckBox eepSiteAllowDirBrowsingCheckbox;
@FXML private TextField eepSiteLocalPortField;
DecimalFormat format2dp = new DecimalFormat("0.00");
private boolean masterState = true;
private final ObservableList<Tunnel> tunnelTableList = FXCollections.observableArrayList();
private boolean eepSiteTabInitialized = false;
private Stage getStage() {
return (Stage) rootBorderPane.getScene().getWindow();
}
@FXML private void initialize() {
DirectoryChooser directoryChooser = new DirectoryChooser();
typeCol.setCellValueFactory(new PropertyValueFactory<Tunnel,String>("type"));
stateCol.setCellValueFactory(new PropertyValueFactory<Tunnel,String>("state"));
hostCol.setCellValueFactory(new PropertyValueFactory<Tunnel,String>("host"));
@ -115,12 +135,99 @@ public class Controller {
stage.setHeight(370);
}
else if(tunnelsTab.isSelected()) {
stage.setWidth(900);
stage.setWidth(830);
}
else if(eepSiteTab.isSelected()) {
stage.setWidth(830);
stage.setHeight(500);
if(!eepSiteTabInitialized) {
EepSiteTunnel eepSiteTunnel = getEepSiteTunnel();
eepSiteSecretKeyField.setText(eepSiteTunnel.keyPair.toString());
eepSiteAddrField.setText("http://" + eepSiteTunnel.keyPair.b32Dest);
eepSiteContentDirField.setText(eepSiteTunnel.contentDir);
eepSiteLogsDirField.setText(eepSiteTunnel.logsDir);
eepSiteEnableCheckbox.setSelected(eepSiteTunnel.enabled);
eepSiteEnableLogsCheckbox.setSelected(eepSiteTunnel.enableLogs);
eepSiteAllowDirBrowsingCheckbox.setSelected(eepSiteTunnel.allowDirectoryBrowsing);
eepSiteLocalPortField.setText(eepSiteTunnel.port + "");
eepSiteTabInitialized = true;
// modifying eepSiteSecretKeyField/eepSiteLocalPortField will require the eepsite tunnel to be destroyed
// and later recreated whenever the user is finished editing settings and ticks the enabled box again
eepSiteSecretKeyField.textProperty().addListener((observable, oldValue, newValue) -> {
eepSiteAddrField.setText("");
String key = newValue;
if(key!=null && !key.isEmpty()) {
try {
eepSiteAddrField.setText("http://" + new TunnelControl.KeyPair(key).b32Dest);
}
catch (Exception e2) {
// ignore exception. user may be part way through entering string
}
eepSiteEnableCheckbox.setSelected(false);
}
});
eepSiteGenButton.setOnAction(ev->{
getEepSiteTunnel().keyPair = KeyPair.gen();
eepSiteSecretKeyField.setText(getEepSiteTunnel().keyPair.toString());
});
eepSiteLocalPortField.textProperty().addListener((ov, oldValue, newValue)->{
eepSiteEnableCheckbox.setSelected(false);
getEepSiteTunnel().port = Integer.parseInt(newValue);
save();
eepSiteEnableCheckbox.setSelected(false);
});
eepSiteEnableCheckbox.selectedProperty().addListener((ov, oldValue, newValue)->{
getEepSiteTunnel().enabled = newValue;
if(oldValue!=newValue) {
save();
getRouterWrapper().getTunnelControl().getTunnelList().fireChangeEvent();
if(getEepSiteTunnel().enabled) {
getEepSiteTunnel().start();
}
else {
getEepSiteTunnel().destroy();
}
}
});
// changing eepSiteContentDirChooseButton/eepSiteLogsDirChooseButton/eepSiteEnableLogsCheckbox/eepSiteAllowDirBrowsingCheckbox only requires a jetty restart
eepSiteContentDirChooseButton.setOnAction(ev->{
File selectedDirectory = directoryChooser.showDialog(getStage());
if (selectedDirectory!=null) {
eepSiteContentDirField.setText(selectedDirectory.getAbsolutePath());
getEepSiteTunnel().contentDir = selectedDirectory.getAbsolutePath();
save();
if(getEepSiteTunnel().enabled) restartJetty();
}
});
eepSiteLogsDirChooseButton.setOnAction(ev->{
File selectedDirectory = directoryChooser.showDialog(getStage());
if (selectedDirectory!=null) {
eepSiteLogsDirField.setText(selectedDirectory.getAbsolutePath());
getEepSiteTunnel().logsDir = selectedDirectory.getAbsolutePath();
save();
if(getEepSiteTunnel().enabled) restartJetty();
}
});
eepSiteEnableLogsCheckbox.selectedProperty().addListener((ov, oldValue, newValue)->{
getEepSiteTunnel().enableLogs = newValue;
save();
if(getEepSiteTunnel().enabled) restartJetty();
});
eepSiteAllowDirBrowsingCheckbox.selectedProperty().addListener((ov, oldValue, newValue)->{
getEepSiteTunnel().allowDirectoryBrowsing = newValue;
save();
if(getEepSiteTunnel().enabled) restartJetty();
});
}
}
};
bandwidthTab.setOnSelectionChanged(tabSelectionEventHandler);
tunnelsTab.setOnSelectionChanged(tabSelectionEventHandler);
helpTab.setOnSelectionChanged(tabSelectionEventHandler);
Stream.of(bandwidthTab, tunnelsTab, eepSiteTab, helpTab).forEach(tab->tab.setOnSelectionChanged(tabSelectionEventHandler));
bandwidthSlider.valueProperty().addListener((observableValue, oldValue, newValue)-> {
maxBandwidthLabel.setText(String.format("%.1f", newValue.floatValue()) + " Mbps");
@ -155,7 +262,7 @@ public class Controller {
var tunnelList = getRouterWrapper().getTunnelControl().getTunnelList();
tunnelList.addChangeListener(tunnels->{
tunnelTableList.clear();
tunnelTableList.addAll(tunnels);
tunnels.stream().filter(Tunnel::getEnabled).forEach(tunnelTableList::add);
});
}).start();
@ -184,8 +291,6 @@ public class Controller {
});
bandwidthUpdateThread.start();
Runtime.getRuntime().addShutdownHook(new Thread(()->bandwidthUpdateThread.interrupt()));
}
public RouterWrapper getRouterWrapper() {
@ -210,5 +315,18 @@ public class Controller {
}
private void restartJetty() {
Optional<Tunnel> eepSiteTunnelOptional = getRouterWrapper().getTunnelControl().getTunnelList().getTunnelsCopyStream().filter(t->t.getType().equals("eepsite")).findFirst();
EepSiteTunnel eepSiteTunnel = (EepSiteTunnel) eepSiteTunnelOptional.get();
eepSiteTunnel.stopJetty();
new Thread(()->eepSiteTunnel.startJetty()).start();
}
private void save() {
getRouterWrapper().getTunnelControl().getTunnelList().save();
}
private EepSiteTunnel getEepSiteTunnel() {
return getRouterWrapper().getTunnelControl().getEepSiteTunnel();
}
}

@ -7,8 +7,10 @@ import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.image.Image;
import javafx.stage.Stage;
import org.getmonero.i2p.zero.TunnelControl;
import javax.imageio.ImageIO;
@ -22,6 +24,15 @@ public class Gui extends Application {
@Override
public void start(Stage primaryStage) throws Exception{
if(TunnelControl.isPortInUse()) {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setTitle("Error");
alert.setHeaderText(null);
alert.setContentText("I2P-zero is already running");
alert.showAndWait();
System.exit(1);
}
instance = this;
String osName = System.getProperty("os.name");
if(osName.startsWith("Mac")) {
@ -36,6 +47,8 @@ public class Gui extends Application {
controller = loader.getController();
primaryStage.setTitle("I2P-zero");
primaryStage.setWidth(360);
primaryStage.setHeight(340);
primaryStage.setMinWidth(360);
primaryStage.setMinHeight(370);
Scene scene = new Scene(root);

@ -8,7 +8,7 @@
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.Pane?>
<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="241.0" prefWidth="770.0" stylesheets="@gui.css" xmlns="http://javafx.com/javafx/10.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.getmonero.i2p.zero.gui.AddTunnelController">
<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="267.0" prefWidth="770.0" stylesheets="@gui.css" xmlns="http://javafx.com/javafx/10.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.getmonero.i2p.zero.gui.AddTunnelController">
<children>
<Label layoutX="18.0" layoutY="9.0" prefHeight="27.0" prefWidth="120.0" text="Add a tunnel">
</Label>
@ -18,10 +18,10 @@
</toggleGroup>
</RadioButton>
<RadioButton fx:id="serverTunnelRadioButton" layoutX="19.0" layoutY="73.0" mnemonicParsing="false" text="Server tunnel - create an I2P destination address that will forward requests to the specified host and port" toggleGroup="$tunnelType" />
<RadioButton fx:id="socksProxyRadioButton" layoutX="19.0" layoutY="97.0" mnemonicParsing="false" text="SOCKS proxy - applications (like web browsers) that support SOCKS can use your proxy to connect to I2P destinations" toggleGroup="$tunnelType" />
<Button fx:id="addButton" defaultButton="true" disable="true" layoutX="712.0" layoutY="197.0" mnemonicParsing="false" text="Add" />
<Button fx:id="cancelButton" cancelButton="true" layoutX="647.0" layoutY="197.0" mnemonicParsing="false" text="Cancel" />
<Pane fx:id="clientTunnelConfigPane" layoutX="27.0" layoutY="133.0" prefHeight="67.0" prefWidth="523.0">
<RadioButton fx:id="httpProxyRadioButton" layoutX="19.0" layoutY="97.0" mnemonicParsing="false" text="HTTP proxy - create a proxy that your web browser can use to access I2P web sites" toggleGroup="$tunnelType" />
<Button fx:id="addButton" defaultButton="true" disable="true" layoutX="712.0" layoutY="221.0" mnemonicParsing="false" text="Add" />
<Button fx:id="cancelButton" cancelButton="true" layoutX="647.0" layoutY="221.0" mnemonicParsing="false" text="Cancel" />
<Pane fx:id="clientTunnelConfigPane" layoutX="40.0" layoutY="156.0" prefHeight="67.0" prefWidth="523.0" >
<children>
<TextField fx:id="clientDestAddrField" layoutX="165.0" prefHeight="27.0" prefWidth="353.0" />
<Label layoutX="1.0" layoutY="4.0" text="I2P destination address" />
@ -29,7 +29,7 @@
<Label layoutX="1.0" layoutY="37.0" text="Local port" />
</children>
</Pane>
<Pane fx:id="serverTunnelConfigPane" layoutX="27.0" layoutY="132.0" prefHeight="97.0" prefWidth="523.0" visible="false">
<Pane fx:id="serverTunnelConfigPane" layoutX="40.0" layoutY="155.0" prefHeight="97.0" prefWidth="597.0">
<children>
<TextField fx:id="serverHostField" layoutX="135.0" text="localhost" />
<Label layoutX="1.0" layoutY="5.0" text="Destination host" />
@ -41,11 +41,18 @@
<Label layoutX="1.0" layoutY="71.0" text="Server public address" />
</children>
</Pane>
<Pane fx:id="socksProxyConfigPane" layoutX="27.0" layoutY="132.0" prefHeight="39.0" prefWidth="523.0" visible="false">
<Pane fx:id="socksProxyConfigPane" layoutX="40.0" layoutY="155.0" prefHeight="39.0" prefWidth="523.0" visible="false">
<children>
<TextField fx:id="socksPortField" layoutX="124.0" prefHeight="27.0" prefWidth="77.0" />
<Label layoutX="1.0" layoutY="5.0" text="Local SOCKS port" />
</children>
</Pane>
<RadioButton fx:id="socksProxyRadioButton" layoutX="19.0" layoutY="120.0" mnemonicParsing="false" text="SOCKS proxy - applications that support SOCKS can use your proxy to connect to I2P destinations" toggleGroup="$tunnelType" />
<Pane fx:id="httpProxyConfigPane" layoutX="40.0" layoutY="155.0" prefHeight="39.0" prefWidth="523.0" visible="false">
<children>
<TextField fx:id="httpProxyPortField" layoutX="149.0" prefHeight="27.0" prefWidth="77.0" />
<Label layoutY="5.0" text="Local HTTP proxy port" />
</children>
</Pane>
</children>
</AnchorPane>

@ -2,6 +2,7 @@
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Slider?>
<?import javafx.scene.control.Tab?>
@ -9,6 +10,7 @@
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.control.TextArea?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.AnchorPane?>
@ -16,7 +18,7 @@
<?import javafx.scene.layout.Pane?>
<?import javafx.scene.layout.StackPane?>
<BorderPane fx:id="rootBorderPane" minHeight="340.0" minWidth="360.0" prefHeight="340.0" prefWidth="360.0" stylesheets="@gui.css" xmlns="http://javafx.com/javafx/10.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.getmonero.i2p.zero.gui.Controller">
<BorderPane fx:id="rootBorderPane" minHeight="340.0" minWidth="360.0" prefHeight="500.0" prefWidth="900.0" stylesheets="@gui.css" xmlns="http://javafx.com/javafx/10.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.getmonero.i2p.zero.gui.Controller">
<center>
<TabPane>
<tabs>
@ -113,11 +115,40 @@
</BorderPane>
</content>
</Tab>
<Tab fx:id="eepSiteTab" closable="false" text="EepSite">
<content>
<BorderPane layoutX="86.0" layoutY="25.0" minHeight="-Infinity" minWidth="-Infinity">
<center>
<AnchorPane maxHeight="340.0" maxWidth="784.0" minHeight="340.0" minWidth="784.0" prefHeight="340.0" prefWidth="784.0">
<children>
<CheckBox fx:id="eepSiteEnableCheckbox" layoutY="38.0" mnemonicParsing="false" text="Enable your EepSite" />
<TextField fx:id="eepSiteAddrField" editable="false" layoutX="174.0" layoutY="76.0" prefHeight="28.0" prefWidth="490.0" />
<Label layoutY="81.0" text="Your I2P EepSite address" />
<Label text="Run your own website, accessible to the outside world only via I2P" />
<Label layoutX="2.0" layoutY="118.0" text="Server secret key" />
<TextField fx:id="eepSiteSecretKeyField" layoutX="174.0" layoutY="113.0" prefHeight="28.0" prefWidth="490.0" />
<Button fx:id="eepSiteGenButton" layoutX="672.0" layoutY="76.0" mnemonicParsing="false" text="Generate new" />
<TextField fx:id="eepSiteContentDirField" editable="false" layoutX="174.0" layoutY="169.0" prefHeight="28.0" prefWidth="490.0" />
<Label layoutX="2.0" layoutY="174.0" text="Website content directory" />
<Button fx:id="eepSiteContentDirChooseButton" layoutX="672.0" layoutY="169.0" mnemonicParsing="false" text="Choose..." />
<TextField fx:id="eepSiteLogsDirField" editable="false" layoutX="174.0" layoutY="235.0" prefHeight="28.0" prefWidth="490.0" />
<Label layoutX="2.0" layoutY="240.0" text="Website logs directory" />
<Button fx:id="eepSiteLogsDirChooseButton" layoutX="672.0" layoutY="235.0" mnemonicParsing="false" text="Choose..." />
<CheckBox fx:id="eepSiteEnableLogsCheckbox" layoutX="174.0" layoutY="268.0" mnemonicParsing="false" text="Enable logs" />
<CheckBox fx:id="eepSiteAllowDirBrowsingCheckbox" layoutX="174.0" layoutY="203.0" mnemonicParsing="false" text="Allow directory browsing" />
<Label layoutX="2.0" layoutY="308.0" text="Local web server port" />
<TextField fx:id="eepSiteLocalPortField" layoutX="174.0" layoutY="303.0" prefHeight="28.0" prefWidth="78.0" text="8080" />
</children>
</AnchorPane>
</center>
</BorderPane>
</content>
</Tab>
<Tab fx:id="helpTab" closable="false" text="Help">
<content>
<BorderPane>
<center>
<TextArea fx:id="helpTextArea" editable="false" text="I2P hides your IP address when you connect to other I2P destinations. &#10;&#10;For example, if you are using a Monero wallet: When your Monero wallet needs to announce a transaction, none of the other Monero I2P nodes that it announces the transaction to will be able to know your IP address.&#10;&#10;You can create your own I2P destination addresses using the Tunnels tab. Connections received to these I2P addresses will be forwarded to the destination of your choice (such as a local web server). You can also create your own client tunnels, which will allow existing software to easily communicate with remote I2P destinations.&#10;&#10;I2P achives privacy by routing your traffic through a series of other I2P nodes. Each node between you and your final destination will not be able to tell whether the next node in the chain is your final destination.&#10;&#10;The nodes you connect to will not know whether you are originating a tunnel, or whether you're simply acting as a link in the chain of somebody else's tunnel.&#10;&#10;I2P automatically encrypts all traffic, so no node will be able to know what kind of traffic it is forwarding on behalf of others. &#10;&#10;For more information, visit https://geti2p.net" wrapText="true" BorderPane.alignment="CENTER">
<TextArea fx:id="helpTextArea" editable="false" text="I2P hides your IP address when you connect to other I2P destinations.&#10;&#10;Note that when you first start I2P, it will take a minute to warm up and establish any pre-configured tunnels.&#10;&#10;To browse I2P web sites, in the tunnels tab ensure you have your HTTP proxy running, and configure your web browser to it (usually in your browser's preferences/proxy settings). You can then access I2P websites such as http://direct.i2p&#10;&#10;You can also easily set up your own I2P website (also known as an EepSite).&#10;&#10;I2P-zero can also be used to let other applications, such as a cryptocurrency wallet, to access the I2P network. For example, if you are using a Monero wallet, your transactions will be announced over I2P instead of over the regular internet. This means that none of the other Monero I2P nodes will be able to know your IP address.&#10;&#10;I2P achives privacy by routing your traffic through a chain of other I2P nodes. None of the nodes between you and your final destination will be able to tell whether the next node in the chain is your final destination or just another link in the chain.&#10;&#10;The nodes you connect to will not know whether you are originating a tunnel, or whether you're simply acting as a link in the chain of somebody else's tunnel. &#10;&#10;I2P automatically encrypts all traffic, so no node will be able to know what kind of traffic it is forwarding on behalf of others. This means that when you participate in the I2P network, it is impossible for you to spy on the traffic of other I2P users.&#10;&#10;For more information, visit https://geti2p.net" wrapText="true" BorderPane.alignment="CENTER">
<BorderPane.margin>
<Insets />
</BorderPane.margin>

@ -10,61 +10,7 @@
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../target/modules/i2p.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../target/modules/i2ptunnel.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../target/modules/mstreaming.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../target/modules/router.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../target/modules/sam.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../target/modules/streaming.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../import/javax.json-api-1.1.4.jar!/" />
<root url="jar://$MODULE_DIR$/../target/modules/combined.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />

@ -1,10 +1,5 @@
module org.getmonero.i2p.zero {
requires i2p;
requires mstreaming;
requires streaming;
requires router;
requires sam;
requires i2ptunnel;
requires combined;
requires jdk.crypto.ec;
exports org.getmonero.i2p.zero;
}

@ -7,6 +7,11 @@ public class Main {
public static void main(String[] args) {
if(TunnelControl.isPortInUse()) {
System.out.println("I2P-zero already running");
System.exit(1);
}
System.out.println("I2P router launched.\n" +
"Press Ctrl-C to gracefully shut down the router (or send the SIGINT signal to the process).");
@ -29,7 +34,10 @@ public class Main {
System.out.println("Options set: "
+ p.entrySet().stream().map(e->"--"+e.getKey()+"="+e.getValue()).collect(Collectors.joining(" ")));
new RouterWrapper(p).start();
RouterWrapper routerWrapper = new RouterWrapper(p);
routerWrapper.start();
Runtime.getRuntime().addShutdownHook(new Thread(()->routerWrapper.stop()));
}

@ -94,8 +94,6 @@ public class RouterWrapper {
}
}).start();
Runtime.getRuntime().addShutdownHook(new Thread(()->stop()));
}).start();
}
@ -106,6 +104,12 @@ public class RouterWrapper {
tunnelControl.stop();
System.out.println("I2P router will shut down gracefully");
router.shutdownGracefully();
// don't wait more than 2 seconds for shutdown. If tunnels are still opening, they can pause for up to 20 seconds, which is too long
new Thread(()->{
try { Thread.sleep(2000); } catch (InterruptedException e) {}
System.exit(0);
}).start();
}
long lastTriggerTimestamp = 0;

@ -9,6 +9,9 @@ import net.i2p.data.Base64;
import net.i2p.data.Destination;
import net.i2p.i2ptunnel.I2PTunnel;
import net.i2p.sam.SAMBridge;
import org.eclipse.jetty.server.*;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.server.handler.ResourceHandler;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
@ -22,6 +25,7 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import java.util.stream.Stream;
@ -34,6 +38,8 @@ public class TunnelControl implements Runnable {
private File tunnelControlTempDir;
private TunnelList tunnelList;
private static final int TUNNEL_CONTROL_LISTENING_PORT = 30000;
public static class TunnelList {
private File tunnelControlConfigDir;
private File tunnelControlTempDir;
@ -48,7 +54,7 @@ public class TunnelControl implements Runnable {
public void addChangeListener(ChangeListener<List<Tunnel>> listener) {
changeListeners.add(listener);
}
private void fireChangeEvent() {
public void fireChangeEvent() {
for(var listener : changeListeners) listener.onChange(tunnels);
}
@ -79,12 +85,18 @@ public class TunnelControl implements Runnable {
case "server":
tunnels.add(new ServerTunnel((String) obj.get("host"), Integer.parseInt((String) obj.get("port")), new KeyPair((String) obj.get("keypair")), tunnelControlTempDir));
break;
case "eepsite":
tunnels.add(new EepSiteTunnel((Boolean) obj.get("enabled"), new KeyPair((String) obj.get("keypair")), (String) obj.get("contentDir"), (String) obj.get("logsDir"), (Boolean) obj.get("allowDirectoryBrowsing"), (Boolean) obj.get("enableLogs"), Integer.parseInt((String) obj.get("port")), tunnelControlTempDir));
break;
case "client":
tunnels.add(new ClientTunnel((String) obj.get("dest"), Integer.parseInt((String) obj.get("port"))));
break;
case "socks":
tunnels.add(new SocksTunnel(Integer.parseInt((String) obj.get("port"))));
break;
case "http":
tunnels.add(new HttpClientTunnel(Integer.parseInt((String) obj.get("port"))));
break;
}
});
fireChangeEvent();
@ -106,10 +118,19 @@ public class TunnelControl implements Runnable {
tunnelsArray.add(entry);
switch (t.getType()) {
case "server":
case "eepsite":
entry.put("host", t.getHost());
entry.put("port", t.getPort());
entry.put("dest", t.getI2P());
if(includeKeyPairs) entry.put("keypair", ((ServerTunnel) t).keyPair.toString());
if(t.getType().equals("eepsite")) {
EepSiteTunnel eepSiteTunnel = (EepSiteTunnel) t;
entry.put("enabled", eepSiteTunnel.enabled);
entry.put("contentDir", eepSiteTunnel.contentDir);
entry.put("logsDir", eepSiteTunnel.logsDir);
entry.put("allowDirectoryBrowsing", eepSiteTunnel.allowDirectoryBrowsing);
entry.put("enableLogs", eepSiteTunnel.enableLogs);
}
break;
case "client":
@ -118,6 +139,7 @@ public class TunnelControl implements Runnable {
break;
case "socks":
case "http":
entry.put("port", t.getPort());
break;
}
@ -150,100 +172,237 @@ public class TunnelControl implements Runnable {
tunnelList = new TunnelList(tunnelControlConfigDir, tunnelControlTempDir);
}
public interface Tunnel {
public String getType();
public String getHost();
public String getPort();
public String getI2P();
public String getState();
public void destroy();
public static abstract class Tunnel {
public volatile I2PTunnel tunnel;
public boolean enabled = true;
public abstract String getType();
public abstract String getHost();
public abstract String getPort();
public abstract String getI2P();
public abstract Tunnel start();
public String getState() {
return tunnel==null ? "opening" : "open";
}
public boolean getEnabled() { return enabled; }
public void destroy() {
new Thread(()->{
// subsequent line commented out, because the tunnels will sleep for 20 seconds at a time, which is too long
// while(tunnel==null) { try { Thread.sleep(100); } catch (InterruptedException e) {} } // wait for tunnel to be established before closing it
if(tunnel!=null) tunnel.runClose(new String[]{"forced", "all"}, tunnel);
}).start();
}
}
public static class ClientTunnel implements Tunnel {
public static class ClientTunnel extends Tunnel {
public String dest;
public int port;
public I2PTunnel tunnel;
public ClientTunnel(String dest, int port) {
this.dest = dest;
this.port = port;
}
@Override
public Tunnel start() {
new Thread(()->{
tunnel = new I2PTunnel(new String[]{"-die", "-nocli", "-e", "config localhost 7654", "-e", "client " + port + " " + dest});
}).start();
return this;
}
public void destroy() {
@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 HttpClientTunnel extends Tunnel {
public int port;
public HttpClientTunnel(int port) {
this.port = port;
}
@Override
public Tunnel start() {
new Thread(()->{
while(tunnel==null) { try { Thread.sleep(100); } catch (InterruptedException e) {} } // wait for tunnel to be established before closing it
tunnel.runClose(new String[]{"forced", "all"}, tunnel);
tunnel = new I2PTunnel(new String[]{"-die", "-nocli", "-e", "config localhost 7654", "-e", "httpclient " + port});
}).start();
return this;
}
@Override public String getType() { return "client"; }
@Override public String getType() { return "http"; }
@Override public String getHost() { return "localhost"; }
@Override public String getPort() { return port+""; }
@Override public String getI2P() { return dest; }
@Override public String getState() { return tunnel==null ? "opening" : "open"; }
@Override public String getI2P() { return "n/a"; }
}
public static class ServerTunnel implements Tunnel {
public static class ServerTunnel<T extends ServerTunnel> extends Tunnel {
public String dest;
public String host;
public int port;
public volatile I2PTunnel tunnel;
public KeyPair keyPair;
private File tunnelControlTempDir;
public ServerTunnel(String host, int port, KeyPair keyPair, File tunnelControlTempDir) {
try {
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));
new File(seckeyPath).deleteOnExit(); // clean up temporary file that was only required because new I2PTunnel() requires it to be written to disk
// 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();
this.tunnelControlTempDir = tunnelControlTempDir;
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
public void destroy() {
new Thread(()->{
while(tunnel==null) { try { Thread.sleep(100); } catch (InterruptedException e) {} } // wait for tunnel to be established before closing it
tunnel.runClose(new String[]{"forced", "all"}, tunnel);
@Override
public Tunnel start() {
new Thread(() -> {
try {
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));
new File(seckeyPath).deleteOnExit(); // clean up temporary file that was only required because new I2PTunnel() requires it to be written to disk
// listen using the I2P server keypair, and forward incoming connections to a destination and port
tunnel = new I2PTunnel(new String[]{"-die", "-nocli", "-e", "server " + host + " " + port + " " + seckeyPath});
}
catch(Exception e) {
throw new RuntimeException(e);
}
}).start();
return this;
}
@Override public String getType() { return "server"; }
@Override public String getHost() { return host; }
@Override public String getPort() { return port+""; }
@Override public String getI2P() { return dest; }
@Override public String getState() { return tunnel==null ? "opening" : "open"; }
}
public static class SocksTunnel implements Tunnel {
public static class EepSiteTunnel extends ServerTunnel {
public Server server;
public String contentDir;
public String logsDir;
public Boolean allowDirectoryBrowsing;
public Boolean enableLogs;
public EepSiteTunnel(boolean enabled, KeyPair keyPair, String contentDirStr, String logsDirStr, boolean allowDirectoryBrowsing, boolean enableLogs, int port, File tunnelControlTempDir) {
super("localhost", port, keyPair, tunnelControlTempDir);
this.enabled = enabled;
this.contentDir = contentDirStr;
this.logsDir = logsDirStr;
this.allowDirectoryBrowsing = allowDirectoryBrowsing;
this.enableLogs = enableLogs;
this.port = port;
this.keyPair = keyPair;
}
@Override
public void destroy() {
try {
server.stop();
super.destroy();
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
public void stopJetty() {
if(server==null) return;
try {
server.stop();
server = null;
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
public void startJetty() {
try {
File contentDirFile = new File(contentDir);
File logsDirFile = new File(logsDir);
contentDirFile.mkdirs();
logsDirFile.mkdirs();
server = new Server();
server.setStopAtShutdown(true);
HttpConfiguration httpConfig = new HttpConfiguration();
httpConfig.setSendServerVersion(false);
httpConfig.setSendDateHeader(false);
httpConfig.setSendXPoweredBy(false);
httpConfig.setSendServerVersion(false);
HandlerList handlers = new HandlerList();
ResourceHandler resourceHandler = new ResourceHandler();
resourceHandler.setDirectoriesListed(allowDirectoryBrowsing);
resourceHandler.setWelcomeFiles(new String[]{"index.html"});
resourceHandler.setResourceBase(contentDirFile.getAbsolutePath());
handlers.addHandler(resourceHandler);
server.setHandler(handlers);
if(enableLogs) {
NCSARequestLog requestLog = new NCSARequestLog(logsDirFile.getAbsolutePath() + File.separator + "eepsite-yyyy_mm_dd.request.log");
requestLog.setAppend(true);
requestLog.setExtended(false);
requestLog.setLogTimeZone("UTC");
requestLog.setLogLatency(true);
requestLog.setRetainDays(0);
server.setRequestLog(requestLog);
}
ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(httpConfig));
http.setPort(port);
http.setIdleTimeout(TUNNEL_CONTROL_LISTENING_PORT);
http.setHost("localhost");
server.addConnector(http);
server.start();
server.join();
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public Tunnel start() {
if(!enabled) return this;
new Thread(()->startJetty()).start();
return super.start();
}
@Override public String getType() { return "eepsite"; }
}
public static class SocksTunnel extends 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() {
@Override
public Tunnel start() {
new Thread(()->{
while(tunnel==null) { try { Thread.sleep(100); } catch (InterruptedException e) {} } // wait for tunnel to be established before closing it
tunnel.runClose(new String[]{"forced", "all"}, tunnel);
tunnel = new I2PTunnel(new String[]{"-die", "-nocli", "-e", "sockstunnel " + port});
}).start();
return this;
}
@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"; }
@Override public String getState() { return tunnel==null ? "opening" : "open"; }
}
public TunnelList getTunnelList() {
@ -278,14 +437,19 @@ public class TunnelControl implements Runnable {
throw new RuntimeException(e);
}
}
public static KeyPair gen() 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 KeyPair gen() {
try {
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);
}
catch (Exception e){
throw new RuntimeException(e);
}
}
public static KeyPair read(String path) throws Exception {
return new KeyPair(Files.readString(Paths.get(path)));
@ -300,6 +464,7 @@ public class TunnelControl implements Runnable {
public void run() {
tunnelList.load();
for(var t : tunnelList.tunnels) if(t.getEnabled()) t.start();
try {
controlServerSocket = new ServerSocket(30000, 0, InetAddress.getLoopbackAddress());
@ -334,6 +499,7 @@ public class TunnelControl implements Runnable {
keyPair = KeyPair.gen();
}
var tunnel = new ServerTunnel(destHost, destPort, keyPair, getTunnelControlTempDir());
tunnel.start();
tunnelList.addTunnel(tunnel);
out.println(tunnel.dest);
break;
@ -352,7 +518,7 @@ public class TunnelControl implements Runnable {
case "server.state": {
String dest = args[1];
tunnelList.getTunnelsCopyStream().filter(t -> t.getType().equals("server") && ((ServerTunnel) t).dest.equals(dest)).forEach(t -> {
out.println(((ServerTunnel) t).tunnel==null ? "opening" : "open");
out.println((t.getState()));
});
break;
}
@ -361,6 +527,7 @@ public class TunnelControl implements Runnable {
String destPubKey = args[1];
int port = Integer.parseInt(args[2]);
var clientTunnel = new ClientTunnel(destPubKey, port);
clientTunnel.start();
tunnelList.addTunnel(clientTunnel);
out.println(clientTunnel.port);
break;
@ -379,14 +546,14 @@ public class TunnelControl implements Runnable {
case "client.state": {
int port = Integer.parseInt(args[1]);
tunnelList.getTunnelsCopyStream().filter(t->t.getType().equals("client") && ((ClientTunnel) t).port == port).forEach(t->{
out.println(((ClientTunnel) t).tunnel==null ? "opening" : "open");
out.println((t.getState()));
});
break;
}
case "socks.create": {
int port = Integer.parseInt(args[1]);
tunnelList.addTunnel(new SocksTunnel(port));
tunnelList.addTunnel(new SocksTunnel(port).start());
out.println("OK");
break;
}
@ -403,7 +570,31 @@ public class TunnelControl implements Runnable {
case "socks.state": {
int port = Integer.parseInt(args[1]);
tunnelList.getTunnelsCopyStream().filter(t -> t.getType().equals("socks") && ((SocksTunnel) t).port == port).forEach(t -> {
out.println(((SocksTunnel) t).tunnel==null ? "opening" : "open");
out.println((t.getState()));
});
break;
}
case "http.create": {
int port = Integer.parseInt(args[1]);
tunnelList.addTunnel(new HttpClientTunnel(port).start());
out.println("OK");
break;
}
case "http.destroy": {
int port = Integer.parseInt(args[1]);
tunnelList.getTunnelsCopyStream().filter(t -> t.getType().equals("http") && ((SocksTunnel) t).port == port).forEach(t -> {
t.destroy();
tunnelList.removeTunnel(t);
});
out.println("OK");
break;
}
case "http.state": {
int port = Integer.parseInt(args[1]);
tunnelList.getTunnelsCopyStream().filter(t -> t.getType().equals("http") && ((HttpClientTunnel) t).port == port).forEach(t -> {
out.println((t.getState()));
});
break;
}
@ -460,6 +651,7 @@ public class TunnelControl implements Runnable {
public void stop() {
stopping = true;
try {
getTunnelList().tunnels.forEach(TunnelControl.Tunnel::destroy);
controlServerSocket.close();
}
catch (Exception e) {
@ -467,9 +659,29 @@ public class TunnelControl implements Runnable {
}
}
public EepSiteTunnel getEepSiteTunnel() {
Optional<Tunnel> eepSiteTunnelOptional = getTunnelList().getTunnelsCopyStream().filter(t->t.getType().equals("eepsite")).findFirst();
if(eepSiteTunnelOptional.isPresent()) return (EepSiteTunnel) eepSiteTunnelOptional.get();
else {
EepSiteTunnel eepSiteTunnel = new EepSiteTunnel(false, KeyPair.gen(),
System.getProperty("user.home") + File.separator + ".i2p-zero" + File.separator + "eepsite" + File.separator + "content",
System.getProperty("user.home") + File.separator + ".i2p-zero" + File.separator + "eepsite" + File.separator + "logs",
true, true, 8080, getTunnelControlTempDir());
getTunnelList().addTunnel(eepSiteTunnel);
return eepSiteTunnel;
}
}
public File getTunnelControlTempDir() {
return tunnelControlTempDir;
}
public static boolean isPortInUse() {
try (var socket = new ServerSocket(TUNNEL_CONTROL_LISTENING_PORT, 0, InetAddress.getLoopbackAddress())) {
return false;
} catch (IOException e) {
return true;
}
}
}

Loading…
Cancel
Save