001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.preferences.remotecontrol; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.Color; 007import java.awt.Font; 008import java.awt.GridBagLayout; 009import java.awt.event.ActionEvent; 010import java.awt.event.ActionListener; 011import java.io.IOException; 012import java.security.GeneralSecurityException; 013import java.security.KeyStore; 014import java.security.KeyStoreException; 015import java.security.NoSuchAlgorithmException; 016import java.security.cert.CertificateException; 017import java.util.LinkedHashMap; 018import java.util.Map; 019import java.util.Map.Entry; 020 021import javax.swing.BorderFactory; 022import javax.swing.Box; 023import javax.swing.JButton; 024import javax.swing.JCheckBox; 025import javax.swing.JLabel; 026import javax.swing.JOptionPane; 027import javax.swing.JPanel; 028import javax.swing.JSeparator; 029 030import org.openstreetmap.josm.Main; 031import org.openstreetmap.josm.gui.preferences.DefaultTabPreferenceSetting; 032import org.openstreetmap.josm.gui.preferences.PreferenceSetting; 033import org.openstreetmap.josm.gui.preferences.PreferenceSettingFactory; 034import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane; 035import org.openstreetmap.josm.gui.util.GuiHelper; 036import org.openstreetmap.josm.gui.widgets.VerticallyScrollablePanel; 037import org.openstreetmap.josm.io.remotecontrol.PermissionPrefWithDefault; 038import org.openstreetmap.josm.io.remotecontrol.RemoteControl; 039import org.openstreetmap.josm.io.remotecontrol.RemoteControlHttpsServer; 040import org.openstreetmap.josm.io.remotecontrol.handler.RequestHandler; 041import org.openstreetmap.josm.tools.GBC; 042import org.openstreetmap.josm.tools.PlatformHookWindows; 043 044/** 045 * Preference settings for Remote Control. 046 * 047 * @author Frederik Ramm 048 */ 049public final class RemoteControlPreference extends DefaultTabPreferenceSetting { 050 051 /** 052 * Factory used to build a new instance of this preference setting 053 */ 054 public static class Factory implements PreferenceSettingFactory { 055 056 @Override 057 public PreferenceSetting createPreferenceSetting() { 058 return new RemoteControlPreference(); 059 } 060 } 061 062 private RemoteControlPreference() { 063 super(/* ICON(preferences/) */ "remotecontrol", tr("Remote Control"), tr("Settings for the remote control feature.")); 064 for (PermissionPrefWithDefault p : PermissionPrefWithDefault.getPermissionPrefs()) { 065 JCheckBox cb = new JCheckBox(p.preferenceText); 066 cb.setSelected(p.isAllowed()); 067 prefs.put(p, cb); 068 } 069 } 070 071 private final Map<PermissionPrefWithDefault, JCheckBox> prefs = new LinkedHashMap<>(); 072 private JCheckBox enableRemoteControl; 073 private JCheckBox enableHttpsSupport; 074 075 private JButton installCertificate; 076 private JButton uninstallCertificate; 077 078 private final JCheckBox loadInNewLayer = new JCheckBox(tr("Download objects to new layer")); 079 private final JCheckBox alwaysAskUserConfirm = new JCheckBox(tr("Confirm all Remote Control actions manually")); 080 081 @Override 082 public void addGui(final PreferenceTabbedPane gui) { 083 084 JPanel remote = new VerticallyScrollablePanel(new GridBagLayout()); 085 086 final JLabel descLabel = new JLabel("<html>" 087 + tr("Allows JOSM to be controlled from other applications, e.g. from a web browser.") 088 + "</html>"); 089 descLabel.setFont(descLabel.getFont().deriveFont(Font.PLAIN)); 090 remote.add(descLabel, GBC.eol().insets(5, 5, 0, 10).fill(GBC.HORIZONTAL)); 091 092 final JLabel portLabel = new JLabel("<html>" 093 + tr("JOSM will always listen at <b>port {0}</b> (http) and <b>port {1}</b> (https) on localhost." 094 + "<br>These ports are not configurable because they are referenced by external applications talking to JOSM.", 095 Main.pref.get("remote.control.port", "8111"), 096 Main.pref.get("remote.control.https.port", "8112")) + "</html>"); 097 portLabel.setFont(portLabel.getFont().deriveFont(Font.PLAIN)); 098 remote.add(portLabel, GBC.eol().insets(5, 5, 0, 10).fill(GBC.HORIZONTAL)); 099 100 enableRemoteControl = new JCheckBox(tr("Enable remote control"), RemoteControl.PROP_REMOTECONTROL_ENABLED.get()); 101 remote.add(enableRemoteControl, GBC.eol()); 102 103 final JPanel wrapper = new JPanel(new GridBagLayout()); 104 wrapper.setBorder(BorderFactory.createTitledBorder(BorderFactory.createLineBorder(Color.gray))); 105 106 remote.add(wrapper, GBC.eol().fill(GBC.HORIZONTAL).insets(5, 5, 5, 5)); 107 108 boolean https = RemoteControl.PROP_REMOTECONTROL_HTTPS_ENABLED.get(); 109 110 enableHttpsSupport = new JCheckBox(tr("Enable HTTPS support"), https); 111 wrapper.add(enableHttpsSupport, GBC.eol().fill(GBC.HORIZONTAL)); 112 113 // Certificate installation only available on Windows for now, see #10033 114 if (Main.isPlatformWindows()) { 115 installCertificate = new JButton(tr("Install...")); 116 uninstallCertificate = new JButton(tr("Uninstall...")); 117 installCertificate.setToolTipText(tr("Install JOSM localhost certificate to system/browser root keystores")); 118 uninstallCertificate.setToolTipText(tr("Uninstall JOSM localhost certificate from system/browser root keystores")); 119 wrapper.add(new JLabel(tr("Certificate:")), GBC.std().insets(15, 5, 0, 0)); 120 wrapper.add(installCertificate, GBC.std().insets(5, 5, 0, 0)); 121 wrapper.add(uninstallCertificate, GBC.eol().insets(5, 5, 0, 0)); 122 enableHttpsSupport.addActionListener(new ActionListener() { 123 @Override 124 public void actionPerformed(ActionEvent e) { 125 installCertificate.setEnabled(enableHttpsSupport.isSelected()); 126 } 127 }); 128 installCertificate.addActionListener(new ActionListener() { 129 @Override 130 public void actionPerformed(ActionEvent e) { 131 try { 132 boolean changed = RemoteControlHttpsServer.setupPlatform( 133 RemoteControlHttpsServer.loadJosmKeystore()); 134 String msg = changed ? 135 tr("Certificate has been successfully installed.") : 136 tr("Certificate is already installed. Nothing to do."); 137 Main.info(msg); 138 JOptionPane.showMessageDialog(wrapper, msg); 139 } catch (IOException | GeneralSecurityException ex) { 140 Main.error(ex); 141 } 142 } 143 }); 144 uninstallCertificate.addActionListener(new ActionListener() { 145 @Override 146 public void actionPerformed(ActionEvent e) { 147 try { 148 String msg; 149 KeyStore ks = PlatformHookWindows.getRootKeystore(); 150 if (ks.containsAlias(RemoteControlHttpsServer.ENTRY_ALIAS)) { 151 Main.info(tr("Removing certificate {0} from root keystore.", RemoteControlHttpsServer.ENTRY_ALIAS)); 152 ks.deleteEntry(RemoteControlHttpsServer.ENTRY_ALIAS); 153 msg = tr("Certificate has been successfully uninstalled."); 154 } else { 155 msg = tr("Certificate is not installed. Nothing to do."); 156 } 157 Main.info(msg); 158 JOptionPane.showMessageDialog(wrapper, msg); 159 } catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException ex) { 160 Main.error(ex); 161 } 162 } 163 }); 164 installCertificate.setEnabled(https); 165 } 166 167 wrapper.add(new JSeparator(), GBC.eop().fill(GBC.HORIZONTAL).insets(15, 5, 15, 5)); 168 169 wrapper.add(new JLabel(tr("Permitted actions:")), GBC.eol().insets(5, 0, 0, 0)); 170 for (JCheckBox p : prefs.values()) { 171 wrapper.add(p, GBC.eol().insets(15, 5, 0, 0).fill(GBC.HORIZONTAL)); 172 } 173 174 wrapper.add(new JSeparator(), GBC.eop().fill(GBC.HORIZONTAL).insets(15, 5, 15, 5)); 175 wrapper.add(loadInNewLayer, GBC.eol().fill(GBC.HORIZONTAL)); 176 wrapper.add(alwaysAskUserConfirm, GBC.eol().fill(GBC.HORIZONTAL)); 177 178 remote.add(Box.createVerticalGlue(), GBC.eol().fill(GBC.VERTICAL)); 179 180 loadInNewLayer.setSelected(Main.pref.getBoolean(RequestHandler.loadInNewLayerKey, RequestHandler.loadInNewLayerDefault)); 181 alwaysAskUserConfirm.setSelected(Main.pref.getBoolean(RequestHandler.globalConfirmationKey, RequestHandler.globalConfirmationDefault)); 182 183 ActionListener remoteControlEnabled = new ActionListener() { 184 185 @Override 186 public void actionPerformed(ActionEvent e) { 187 GuiHelper.setEnabledRec(wrapper, enableRemoteControl.isSelected()); 188 // 'setEnabled(false)' does not work for JLabel with html text, so do it manually 189 // FIXME: use QuadStateCheckBox to make checkboxes unset when disabled 190 if (installCertificate != null && uninstallCertificate != null) { 191 // Install certificate button is enabled if HTTPS is also enabled 192 installCertificate.setEnabled(enableRemoteControl.isSelected() && enableHttpsSupport.isSelected()); 193 // Uninstall certificate button is always enabled 194 uninstallCertificate.setEnabled(true); 195 } 196 } 197 }; 198 enableRemoteControl.addActionListener(remoteControlEnabled); 199 remoteControlEnabled.actionPerformed(null); 200 createPreferenceTabWithScrollPane(gui, remote); 201 } 202 203 @Override 204 public boolean ok() { 205 boolean enabled = enableRemoteControl.isSelected(); 206 boolean httpsEnabled = enableHttpsSupport.isSelected(); 207 boolean changed = RemoteControl.PROP_REMOTECONTROL_ENABLED.put(enabled); 208 boolean httpsChanged = RemoteControl.PROP_REMOTECONTROL_HTTPS_ENABLED.put(httpsEnabled); 209 if (enabled) { 210 for (Entry<PermissionPrefWithDefault, JCheckBox> p : prefs.entrySet()) { 211 Main.pref.put(p.getKey().pref, p.getValue().isSelected()); 212 } 213 Main.pref.put(RequestHandler.loadInNewLayerKey, loadInNewLayer.isSelected()); 214 Main.pref.put(RequestHandler.globalConfirmationKey, alwaysAskUserConfirm.isSelected()); 215 } 216 if (changed) { 217 if (enabled) { 218 RemoteControl.start(); 219 } else { 220 RemoteControl.stop(); 221 } 222 } else if (httpsChanged) { 223 if (httpsEnabled) { 224 RemoteControlHttpsServer.restartRemoteControlHttpsServer(); 225 } else { 226 RemoteControlHttpsServer.stopRemoteControlHttpsServer(); 227 } 228 } 229 return false; 230 } 231}