You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
i2p-zero/org.getmonero.i2p.zero.gui/src/org/getmonero/i2p/zero/gui/Controller.java

467 lines
18 KiB

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.ActionEvent;
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.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.*;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.FlowPane;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.stage.DirectoryChooser;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.getmonero.i2p.zero.RouterWrapper;
import org.getmonero.i2p.zero.TunnelControl;
import org.getmonero.i2p.zero.UpdateCheck;
import static org.getmonero.i2p.zero.TunnelControl.*;
import java.awt.*;
import java.io.File;
import java.net.URI;
import java.text.DecimalFormat;
import java.util.Optional;
import java.util.Properties;
import java.util.function.Supplier;
import java.util.stream.Stream;
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 TabPane tabPane;
@FXML private Tab bandwidthTab;
@FXML private Tab tunnelsTab;
@FXML private Tab eepSiteTab;
@FXML private Tab helpTab;
@FXML private Label statusLabel;
@FXML private Button tunnelAddButton;
@FXML private Button tunnelRemoveButton;
@FXML private Button tunnelEditButton;
@FXML private TableView<Tunnel> tunnelsTableView;
@FXML private TableColumn typeCol;
@FXML private TableColumn stateCol;
@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;
@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;
@FXML private TextArea helpTextArea;
DecimalFormat format2dp = new DecimalFormat("0.00");
private boolean masterState = true;
private boolean shuttingDown = false;
private final ObservableList<Tunnel> tunnelTableList = FXCollections.observableArrayList();
private boolean eepSiteTabInitialized = false;
private Stage getStage() {
return (Stage) rootBorderPane.getScene().getWindow();
}
public static class DialogRefs {
public Scene scene;
public Stage stage;
public AddTunnelController controller;
public DialogRefs(Scene scene, Stage stage, AddTunnelController controller) {
this.scene = scene;
this.stage = stage;
this.controller = controller;
}
}
@FXML private void initialize() {
// set up copy-cell-to-clipboard functionality
tunnelsTableView.setOnKeyPressed(new TableKeyEventHandler());
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"));
portCol.setCellValueFactory(new PropertyValueFactory<Tunnel,String>("port"));
i2PCol.setCellValueFactory(new PropertyValueFactory<Tunnel,String>("I2P"));
DoubleBinding usedWidth = typeCol.widthProperty().add(stateCol.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.setVisible(false);
tunnelEditButton.setVisible(false);
}
else {
tunnelRemoveButton.setVisible(true);
tunnelEditButton.setVisible(true);
}
});
tunnelRemoveButton.setOnAction(e->{
Tunnel t = tunnelsTableView.getSelectionModel().getSelectedItem();
getRouterWrapper().getTunnelControl().getTunnelList().removeTunnel(t);
t.destroy(false);
tunnelRemoveButton.setDisable(true);
});
Supplier<DialogRefs> showAddTunnelDialog = ()->{
try {
Stage dialogStage = new Stage();
dialogStage.initModality(Modality.WINDOW_MODAL);
dialogStage.initOwner(getStage());
dialogStage.setResizable(false);
dialogStage.setTitle("New tunnel");
FXMLLoader loader = new FXMLLoader(getClass().getResource("addTunnel.fxml"));
Scene dialogScene = new Scene(loader.load());
dialogScene.getStylesheets().add("org/getmonero/i2p/zero/gui/gui.css");
dialogStage.setScene(dialogScene);
return new DialogRefs(dialogScene, dialogStage, loader.getController());
} catch (Exception e) {
throw new RuntimeException(e);
}
};
tunnelAddButton.setOnAction(event->{
DialogRefs dialogRefs = showAddTunnelDialog.get();
dialogRefs.stage.show();
});
tunnelEditButton.setOnAction(event->{
Tunnel existingTunnel = tunnelsTableView.getSelectionModel().getSelectedItem();
if(existingTunnel.getType().equals("eepsite")) {
tabPane.getSelectionModel().select(eepSiteTab);
}
else {
DialogRefs dialogRefs = showAddTunnelDialog.get();
dialogRefs.stage.setTitle("Edit tunnel");
dialogRefs.controller.setExistingTunnel(existingTunnel);
dialogRefs.controller.setExistingTunnelDestroyer(()->{
getRouterWrapper().getTunnelControl().getTunnelList().removeTunnel(existingTunnel);
existingTunnel.destroy(false);
});
dialogRefs.stage.show();
}
});
EventHandler<Event> tabSelectionEventHandler = e->{
Stage stage = getStage();
if(bandwidthTab.isSelected()) {
stage.setWidth(360);
stage.setHeight(370);
}
else if(tunnelsTab.isSelected()) {
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) -> {
eepSiteEnableCheckbox.setSelected(false);
eepSiteAddrField.setText("");
String key = newValue;
if(key!=null && !key.isEmpty()) {
try {
KeyPair keyPair = new TunnelControl.KeyPair(key);
eepSiteAddrField.setText("http://" + keyPair.b32Dest);
getEepSiteTunnel().dest = keyPair.b32Dest;
getEepSiteTunnel().keyPair = keyPair;
save();
}
catch (Exception e2) {
// ignore exception. user may be part way through entering string
}
}
});
eepSiteGenButton.setOnAction(ev->{
getEepSiteTunnel().keyPair = KeyPair.gen();
save();
eepSiteSecretKeyField.setText(getEepSiteTunnel().keyPair.toString());
});
eepSiteLocalPortField.textProperty().addListener((ov, oldValue, newValue)->{
eepSiteEnableCheckbox.setSelected(false);
getEepSiteTunnel().port = Integer.parseInt(newValue);
save();
});
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(false);
}
}
});
// 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();
});
}
}
};
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");
routerWrapper.debouncedUpdateBandwidthLimitKBPerSec((int) Math.round(1024d*newValue.doubleValue()/8d));
});
masterToggle.setOnMouseClicked(e->{
if(shuttingDown) return;
masterState = !masterState;
bandwidthDisabledOverlay.setVisible(!masterState);
if(masterState) {
masterToggle.setImage(new Image("org/getmonero/i2p/zero/gui/toggle-on.png"));
statusLabel.setVisible(true);
routerWrapper.start(()->{});
listenForTunnelChanges();
tunnelAddButton.setDisable(false);
}
else {
shuttingDown = true;
masterToggle.setImage(new Image("org/getmonero/i2p/zero/gui/toggle-off.png"));
tunnelTableList.clear();
tunnelAddButton.setDisable(true);
statusLabel.setText("Shutting down...");
routerWrapper.stop(true);
}
});
startRouter();
bandwidthSlider.setValue(getRouterWrapper().loadBandwidthLimitKBps()*8d/1024d);
listenForTunnelChanges();
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(formatTransferAmount(routerWrapper.getTotalInMB())+" ");
bandwidthOut1s.setText(format2dp.format(routerWrapper.get1sRateOutKBps()) + " KBps");
bandwidthOut5m.setText(format2dp.format(routerWrapper.get5mRateOutKBps()) + " KBps");
bandwidthOutAll.setText(format2dp.format(routerWrapper.getAvgRateOutKBps()) + " KBps");
totalTransferredOut.setText(formatTransferAmount(routerWrapper.getTotalOutMB())+" ");
statusLabel.setText("Status: " + routerWrapper.getReachability().getMessage());
tunnelsTableView.refresh();
});
}
try { Thread.sleep(1000); } catch (InterruptedException e) {}
}
});
bandwidthUpdateThread.start();
}
private String formatTransferAmount(double amt) {
String unit = "MB";
if(amt>=1000) { amt/= 1000d; unit = "GB"; }
if(amt>=1000) { amt/= 1000d; unit = "TB"; }
if(amt>=1000) { amt/= 1000d; unit = "PB"; }
return format2dp.format(amt) + " " + unit;
}
private void listenForTunnelChanges() {
new Thread(()->{
while(getRouterWrapper()==null || getRouterWrapper().getTunnelControl()==null) {
try { Thread.sleep(100); } catch (InterruptedException e) {}
}
var tunnelList = getRouterWrapper().getTunnelControl().getTunnelList();
tunnelList.addChangeListener(tunnels->{
Platform.runLater(() -> {
tunnelTableList.clear();
tunnels.stream().filter(Tunnel::getEnabled).forEach(tunnelTableList::add);
});
});
Platform.runLater(() -> getRouterWrapper().getTunnelControl().getTunnelList().fireChangeEvent());
}).start();
}
public RouterWrapper getRouterWrapper() {
return routerWrapper;
}
private int getBandwidthLimitKBPerSec() {
return (int)Math.round(bandwidthSlider.getValue()*1024d/8d);
}
private void startRouter() {
var params = Gui.instance.getParameters().getNamed();
Properties routerProperties = new Properties();
routerProperties.put("router.sharePercentage", 80);
params.entrySet().stream().forEach(e->routerProperties.put(e.getKey(), e.getValue()));
routerWrapper = new RouterWrapper(routerProperties, ()-> {
Platform.runLater(()->{
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle("Update available");
alert.setHeaderText(null);
FlowPane fp = new FlowPane();
String updateUrl = "https://github.com/i2p-zero/i2p-zero";
var link = new Hyperlink(updateUrl);
TextFlow textFlow = new TextFlow(
new Text("A new version of I2P-zero is available at"),
link,
new Text("\nPlease keep your software up-to-date, as it will enhance your privacy and keep you safe from vulnerabilities")
);
textFlow.setPrefWidth(300);
link.setOnAction((ActionEvent event) -> {
try {
Desktop.getDesktop().browse(new URI(updateUrl));
}
catch (Exception e) {
e.printStackTrace();
}
});
fp.getChildren().addAll(textFlow);
alert.getDialogPane().contentProperty().set(fp);
alert.show();
});
});
routerWrapper.start(()->{
Platform.runLater(() -> {
helpTextArea.setText("You are running I2P-zero version " + UpdateCheck.currentVersion + "\n\n"
+ "For best performance, please open port " + routerWrapper.routerExternalPort + " on your firewall for incoming UDP and TCP connections. This port has been randomly assigned to you. For privacy reasons, please do not share this port with others.\n\n"
+ helpTextArea.getText());
});
});
}
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();
}
public static class TableKeyEventHandler implements EventHandler<KeyEvent> {
KeyCodeCombination ctrlC = new KeyCodeCombination(KeyCode.C, KeyCombination.CONTROL_ANY);
KeyCodeCombination cmdD = new KeyCodeCombination(KeyCode.C, KeyCombination.META_ANY);
public void handle(final KeyEvent keyEvent) {
if (ctrlC.match(keyEvent) || cmdD.match(keyEvent)) {
if (keyEvent.getSource() instanceof TableView) {
copySelectionToClipboard((TableView<?>) keyEvent.getSource());
keyEvent.consume();
}
}
}
}
public static void copySelectionToClipboard(TableView<?> table) {
var cellPositions = table.getSelectionModel().getSelectedCells();
if(cellPositions.size()>0) {
var cellPosition = cellPositions.get(0);
var cell = table.getColumns().get(cellPosition.getColumn()).getCellData(cellPosition.getRow());
if(cell!=null) {
var clipboardContent = new ClipboardContent();
clipboardContent.putString(cell.toString());
Clipboard.getSystemClipboard().setContent(clipboardContent);
}
}
}
}