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