001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.preferences.server;
003
004import static java.awt.GridBagConstraints.HORIZONTAL;
005import static org.openstreetmap.josm.tools.I18n.tr;
006import static org.openstreetmap.josm.tools.I18n.trc;
007
008import java.awt.Component;
009import java.awt.Dimension;
010import java.awt.GridBagConstraints;
011import java.awt.GridBagLayout;
012import java.awt.Insets;
013import java.awt.event.ItemEvent;
014import java.awt.event.ItemListener;
015import java.net.Authenticator.RequestorType;
016import java.net.PasswordAuthentication;
017import java.net.ProxySelector;
018import java.util.Arrays;
019import java.util.EnumMap;
020import java.util.Map;
021import java.util.Optional;
022
023import javax.swing.BorderFactory;
024import javax.swing.Box;
025import javax.swing.ButtonGroup;
026import javax.swing.JLabel;
027import javax.swing.JPanel;
028import javax.swing.JRadioButton;
029
030import org.openstreetmap.josm.gui.help.HelpUtil;
031import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
032import org.openstreetmap.josm.gui.widgets.JosmPasswordField;
033import org.openstreetmap.josm.gui.widgets.JosmTextField;
034import org.openstreetmap.josm.gui.widgets.VerticallyScrollablePanel;
035import org.openstreetmap.josm.io.DefaultProxySelector;
036import org.openstreetmap.josm.io.ProxyPolicy;
037import org.openstreetmap.josm.io.auth.CredentialsAgent;
038import org.openstreetmap.josm.io.auth.CredentialsAgentException;
039import org.openstreetmap.josm.io.auth.CredentialsManager;
040import org.openstreetmap.josm.spi.preferences.Config;
041import org.openstreetmap.josm.spi.preferences.IPreferences;
042import org.openstreetmap.josm.tools.GBC;
043import org.openstreetmap.josm.tools.Logging;
044
045/**
046 * Component allowing input of proxy settings.
047 */
048public class ProxyPreferencesPanel extends VerticallyScrollablePanel {
049
050    static final class AutoSizePanel extends JPanel {
051        AutoSizePanel() {
052            super(new GridBagLayout());
053        }
054
055        @Override
056        public Dimension getMinimumSize() {
057            return getPreferredSize();
058        }
059    }
060
061    private transient Map<ProxyPolicy, JRadioButton> rbProxyPolicy;
062    private final JosmTextField tfProxyHttpHost = new JosmTextField();
063    private final JosmTextField tfProxyHttpPort = new JosmTextField(5);
064    private final JosmTextField tfProxySocksHost = new JosmTextField(20);
065    private final JosmTextField tfProxySocksPort = new JosmTextField(5);
066    private final JosmTextField tfProxyHttpUser = new JosmTextField(20);
067    private final JosmPasswordField tfProxyHttpPassword = new JosmPasswordField(20);
068
069    private JPanel pnlHttpProxyConfigurationPanel;
070    private JPanel pnlSocksProxyConfigurationPanel;
071
072    /**
073     * Builds the panel for the HTTP proxy configuration
074     *
075     * @return panel with HTTP proxy configuration
076     */
077    protected final JPanel buildHttpProxyConfigurationPanel() {
078        JPanel pnl = new AutoSizePanel();
079        GridBagConstraints gc = new GridBagConstraints();
080
081        gc.anchor = GridBagConstraints.LINE_START;
082        gc.insets = new Insets(5, 5, 0, 0);
083        gc.fill = HORIZONTAL;
084        gc.weightx = 0.0;
085        pnl.add(new JLabel(tr("Host:")), gc);
086
087        gc.gridx = 1;
088        gc.weightx = 1.0;
089        pnl.add(tfProxyHttpHost, gc);
090
091        gc.gridy = 1;
092        gc.gridx = 0;
093        gc.fill = GridBagConstraints.NONE;
094        gc.weightx = 0.0;
095        pnl.add(new JLabel(trc("server", "Port:")), gc);
096
097        gc.gridx = 1;
098        gc.weightx = 1.0;
099        pnl.add(tfProxyHttpPort, gc);
100        tfProxyHttpPort.setMinimumSize(tfProxyHttpPort.getPreferredSize());
101
102        gc.gridy = 2;
103        gc.gridx = 0;
104        gc.gridwidth = 2;
105        gc.fill = HORIZONTAL;
106        gc.weightx = 1.0;
107        pnl.add(new JMultilineLabel(tr("Please enter a username and a password if your proxy requires authentication.")), gc);
108
109        gc.gridy = 3;
110        gc.gridx = 0;
111        gc.gridwidth = 1;
112        gc.fill = GridBagConstraints.NONE;
113        gc.weightx = 0.0;
114        pnl.add(new JLabel(tr("User:")), gc);
115
116        gc.gridy = 3;
117        gc.gridx = 1;
118        gc.weightx = 1.0;
119        pnl.add(tfProxyHttpUser, gc);
120        tfProxyHttpUser.setMinimumSize(tfProxyHttpUser.getPreferredSize());
121
122        gc.gridy = 4;
123        gc.gridx = 0;
124        gc.weightx = 0.0;
125        pnl.add(new JLabel(tr("Password:")), gc);
126
127        gc.gridx = 1;
128        gc.weightx = 1.0;
129        pnl.add(tfProxyHttpPassword, gc);
130        tfProxyHttpPassword.setMinimumSize(tfProxyHttpPassword.getPreferredSize());
131
132        // add an extra spacer, otherwise the layout is broken
133        gc.gridy = 5;
134        gc.gridx = 0;
135        gc.gridwidth = 2;
136        gc.fill = GridBagConstraints.BOTH;
137        gc.weightx = 1.0;
138        gc.weighty = 1.0;
139        pnl.add(new JPanel(), gc);
140        return pnl;
141    }
142
143    /**
144     * Builds the panel for the SOCKS proxy configuration
145     *
146     * @return panel with SOCKS proxy configuration
147     */
148    protected final JPanel buildSocksProxyConfigurationPanel() {
149        JPanel pnl = new AutoSizePanel();
150        GridBagConstraints gc = new GridBagConstraints();
151        gc.anchor = GridBagConstraints.LINE_START;
152        gc.insets = new Insets(5, 5, 0, 0);
153        gc.fill = HORIZONTAL;
154        gc.weightx = 0.0;
155        pnl.add(new JLabel(tr("Host:")), gc);
156
157        gc.gridx = 1;
158        gc.weightx = 1.0;
159        pnl.add(tfProxySocksHost, gc);
160
161        gc.gridy = 1;
162        gc.gridx = 0;
163        gc.weightx = 0.0;
164        gc.fill = GridBagConstraints.NONE;
165        pnl.add(new JLabel(trc("server", "Port:")), gc);
166
167        gc.gridx = 1;
168        gc.weightx = 1.0;
169        pnl.add(tfProxySocksPort, gc);
170        tfProxySocksPort.setMinimumSize(tfProxySocksPort.getPreferredSize());
171
172        // add an extra spacer, otherwise the layout is broken
173        gc.gridy = 2;
174        gc.gridx = 0;
175        gc.gridwidth = 2;
176        gc.fill = GridBagConstraints.BOTH;
177        gc.weightx = 1.0;
178        gc.weighty = 1.0;
179        pnl.add(new JPanel(), gc);
180        return pnl;
181    }
182
183    protected final JPanel buildProxySettingsPanel() {
184        JPanel pnl = new JPanel(new GridBagLayout());
185
186        ButtonGroup bgProxyPolicy = new ButtonGroup();
187        rbProxyPolicy = new EnumMap<>(ProxyPolicy.class);
188        ProxyPolicyChangeListener policyChangeListener = new ProxyPolicyChangeListener();
189        for (ProxyPolicy pp: ProxyPolicy.values()) {
190            rbProxyPolicy.put(pp, new JRadioButton());
191            bgProxyPolicy.add(rbProxyPolicy.get(pp));
192            rbProxyPolicy.get(pp).addItemListener(policyChangeListener);
193        }
194
195        // radio button "No proxy"
196        pnl.add(newRadioButton(ProxyPolicy.NO_PROXY, tr("No proxy")), GBC.eop().anchor(GBC.NORTHWEST));
197
198        // radio button "System settings"
199        pnl.add(newRadioButton(ProxyPolicy.USE_SYSTEM_SETTINGS, tr("Use standard system settings")), GBC.eol());
200        if (!DefaultProxySelector.willJvmRetrieveSystemProxies()) {
201            String msg = tr("Use standard system settings (disabled. Start JOSM with <tt>-Djava.net.useSystemProxies=true</tt> to enable)");
202            pnl.add(new JMultilineLabel("<html>" + msg + "</html>"), GBC.eop().fill(HORIZONTAL));
203        }
204
205        // radio button http proxy
206        pnl.add(newRadioButton(ProxyPolicy.USE_HTTP_PROXY, tr("Manually configure a HTTP proxy")), GBC.eol());
207
208        // the panel with the http proxy configuration parameters
209        pnlHttpProxyConfigurationPanel = buildHttpProxyConfigurationPanel();
210        pnl.add(pnlHttpProxyConfigurationPanel, GBC.eop().fill(HORIZONTAL));
211
212        // radio button SOCKS proxy
213        pnl.add(newRadioButton(ProxyPolicy.USE_SOCKS_PROXY, tr("Use a SOCKS proxy")), GBC.eol());
214
215        // the panel with the SOCKS configuration parameters
216        pnlSocksProxyConfigurationPanel = buildSocksProxyConfigurationPanel();
217        pnl.add(pnlSocksProxyConfigurationPanel, GBC.eop().fill(HORIZONTAL));
218
219        pnl.add(Box.createVerticalGlue(), GBC.eol().fill());
220
221        return pnl;
222    }
223
224    private JRadioButton newRadioButton(ProxyPolicy policy, String text) {
225        JRadioButton radioButton = rbProxyPolicy.get(policy);
226        radioButton.setText(text);
227        return radioButton;
228    }
229
230    /**
231     * Initializes the panel with the values from the preferences
232     */
233    public final void initFromPreferences() {
234        IPreferences pref = Config.getPref();
235        ProxyPolicy pp = Optional.ofNullable(ProxyPolicy.fromName(pref.get(DefaultProxySelector.PROXY_POLICY, null)))
236                .orElse(ProxyPolicy.NO_PROXY);
237        rbProxyPolicy.get(pp).setSelected(true);
238        tfProxyHttpHost.setText(pref.get(DefaultProxySelector.PROXY_HTTP_HOST, ""));
239        tfProxyHttpPort.setText(pref.get(DefaultProxySelector.PROXY_HTTP_PORT, ""));
240        tfProxySocksHost.setText(pref.get(DefaultProxySelector.PROXY_SOCKS_HOST, ""));
241        tfProxySocksPort.setText(pref.get(DefaultProxySelector.PROXY_SOCKS_PORT, ""));
242
243        if (pp == ProxyPolicy.USE_SYSTEM_SETTINGS && !DefaultProxySelector.willJvmRetrieveSystemProxies()) {
244            Logging.warn(tr("JOSM is configured to use proxies from the system setting, but the JVM is not configured to retrieve them. " +
245                         "Resetting preferences to ''No proxy''"));
246            pp = ProxyPolicy.NO_PROXY;
247            rbProxyPolicy.get(pp).setSelected(true);
248        }
249
250        // save the proxy user and the proxy password to a credentials store managed by
251        // the credentials manager
252        CredentialsAgent cm = CredentialsManager.getInstance();
253        try {
254            PasswordAuthentication pa = cm.lookup(RequestorType.PROXY, tfProxyHttpHost.getText());
255            if (pa == null) {
256                tfProxyHttpUser.setText("");
257                tfProxyHttpPassword.setText("");
258            } else {
259                tfProxyHttpUser.setText(pa.getUserName() == null ? "" : pa.getUserName());
260                tfProxyHttpPassword.setText(pa.getPassword() == null ? "" : String.valueOf(pa.getPassword()));
261            }
262        } catch (CredentialsAgentException e) {
263            Logging.error(e);
264            tfProxyHttpUser.setText("");
265            tfProxyHttpPassword.setText("");
266        }
267    }
268
269    protected final void updateEnabledState() {
270        boolean isHttpProxy = rbProxyPolicy.get(ProxyPolicy.USE_HTTP_PROXY).isSelected();
271        for (Component c: pnlHttpProxyConfigurationPanel.getComponents()) {
272            c.setEnabled(isHttpProxy);
273        }
274
275        boolean isSocksProxy = rbProxyPolicy.get(ProxyPolicy.USE_SOCKS_PROXY).isSelected();
276        for (Component c: pnlSocksProxyConfigurationPanel.getComponents()) {
277            c.setEnabled(isSocksProxy);
278        }
279
280        rbProxyPolicy.get(ProxyPolicy.USE_SYSTEM_SETTINGS).setEnabled(DefaultProxySelector.willJvmRetrieveSystemProxies());
281    }
282
283    class ProxyPolicyChangeListener implements ItemListener {
284        @Override
285        public void itemStateChanged(ItemEvent e) {
286            updateEnabledState();
287        }
288    }
289
290    /**
291     * Constructs a new {@code ProxyPreferencesPanel}.
292     */
293    public ProxyPreferencesPanel() {
294        setLayout(new GridBagLayout());
295        setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
296        add(buildProxySettingsPanel(), GBC.eop().anchor(GridBagConstraints.NORTHWEST).fill(GridBagConstraints.BOTH));
297
298        initFromPreferences();
299        updateEnabledState();
300
301        HelpUtil.setHelpContext(this, HelpUtil.ht("/Preferences/Connection#ProxySettings"));
302    }
303
304    /**
305     * Saves the current values to the preferences
306     */
307    public void saveToPreferences() {
308        ProxyPolicy policy = Arrays.stream(ProxyPolicy.values())
309                .filter(pp -> rbProxyPolicy.get(pp).isSelected())
310                .findFirst().orElse(null);
311        IPreferences pref = Config.getPref();
312        pref.put(DefaultProxySelector.PROXY_POLICY, Optional.ofNullable(policy).orElse(ProxyPolicy.NO_PROXY).getName());
313        pref.put(DefaultProxySelector.PROXY_HTTP_HOST, tfProxyHttpHost.getText());
314        pref.put(DefaultProxySelector.PROXY_HTTP_PORT, tfProxyHttpPort.getText());
315        pref.put(DefaultProxySelector.PROXY_SOCKS_HOST, tfProxySocksHost.getText());
316        pref.put(DefaultProxySelector.PROXY_SOCKS_PORT, tfProxySocksPort.getText());
317
318        // update the proxy selector
319        ProxySelector selector = ProxySelector.getDefault();
320        if (selector instanceof DefaultProxySelector) {
321            ((DefaultProxySelector) selector).initFromPreferences();
322        }
323
324        CredentialsAgent cm = CredentialsManager.getInstance();
325        try {
326            PasswordAuthentication pa = new PasswordAuthentication(
327                    tfProxyHttpUser.getText().trim(),
328                    tfProxyHttpPassword.getPassword()
329            );
330            cm.store(RequestorType.PROXY, tfProxyHttpHost.getText(), pa);
331        } catch (CredentialsAgentException e) {
332            Logging.error(e);
333        }
334    }
335}