001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.io;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.io.IOException;
007import java.net.InetSocketAddress;
008import java.net.Proxy;
009import java.net.Proxy.Type;
010import java.net.ProxySelector;
011import java.net.SocketAddress;
012import java.net.URI;
013import java.util.Arrays;
014import java.util.Collections;
015import java.util.HashSet;
016import java.util.List;
017import java.util.Set;
018import java.util.TreeSet;
019
020import org.openstreetmap.josm.Main;
021import org.openstreetmap.josm.gui.preferences.server.ProxyPreferencesPanel;
022import org.openstreetmap.josm.gui.preferences.server.ProxyPreferencesPanel.ProxyPolicy;
023
024/**
025 * This is the default proxy selector used in JOSM.
026 *
027 */
028public class DefaultProxySelector extends ProxySelector {
029
030    private static final List<Proxy> NO_PROXY_LIST = Collections.singletonList(Proxy.NO_PROXY);
031
032    private static final String IPV4_LOOPBACK = "127.0.0.1";
033    private static final String IPV6_LOOPBACK = "::1";
034
035    /**
036     * The {@link ProxySelector} provided by the JDK will retrieve proxy information
037     * from the system settings, if the system property <tt>java.net.useSystemProxies</tt>
038     * is defined <strong>at startup</strong>. It has no effect if the property is set
039     * later by the application.
040     *
041     * We therefore read the property at class loading time and remember it's value.
042     */
043    private static boolean JVM_WILL_USE_SYSTEM_PROXIES;
044    static {
045        String v = System.getProperty("java.net.useSystemProxies");
046        if (v != null && v.equals(Boolean.TRUE.toString())) {
047            JVM_WILL_USE_SYSTEM_PROXIES = true;
048        }
049    }
050
051    /**
052     * The {@link ProxySelector} provided by the JDK will retrieve proxy information
053     * from the system settings, if the system property <tt>java.net.useSystemProxies</tt>
054     * is defined <strong>at startup</strong>. If the property is set later by the application,
055     * this has no effect.
056     *
057     * @return true, if <tt>java.net.useSystemProxies</tt> was set to true at class initialization time
058     *
059     */
060    public static boolean willJvmRetrieveSystemProxies() {
061        return JVM_WILL_USE_SYSTEM_PROXIES;
062    }
063
064    private ProxyPolicy proxyPolicy;
065    private InetSocketAddress httpProxySocketAddress;
066    private InetSocketAddress socksProxySocketAddress;
067    private final ProxySelector delegate;
068
069    private final Set<String> errorResources = new HashSet<>();
070    private final Set<String> errorMessages = new HashSet<>();
071    private Set<String> proxyExceptions;
072
073    /**
074     * A typical example is:
075     * <pre>
076     *    PropertySelector delegate = PropertySelector.getDefault();
077     *    PropertySelector.setDefault(new DefaultPropertySelector(delegate));
078     * </pre>
079     *
080     * @param delegate the proxy selector to delegate to if system settings are used. Usually
081     * this is the proxy selector found by ProxySelector.getDefault() before this proxy
082     * selector is installed
083     */
084    public DefaultProxySelector(ProxySelector delegate) {
085        this.delegate = delegate;
086        initFromPreferences();
087    }
088
089    protected int parseProxyPortValue(String property, String value) {
090        if (value == null) return 0;
091        int port = 0;
092        try {
093            port = Integer.parseInt(value);
094        } catch (NumberFormatException e) {
095            Main.error(tr("Unexpected format for port number in preference ''{0}''. Got ''{1}''.", property, value));
096            Main.error(tr("The proxy will not be used."));
097            return 0;
098        }
099        if (port <= 0 || port > 65535) {
100            Main.error(tr("Illegal port number in preference ''{0}''. Got {1}.", property, port));
101            Main.error(tr("The proxy will not be used."));
102            return 0;
103        }
104        return port;
105    }
106
107    /**
108     * Initializes the proxy selector from the setting in the preferences.
109     *
110     */
111    public final void initFromPreferences() {
112        String value = Main.pref.get(ProxyPreferencesPanel.PROXY_POLICY);
113        if (value.isEmpty()) {
114            proxyPolicy = ProxyPolicy.NO_PROXY;
115        } else {
116            proxyPolicy = ProxyPolicy.fromName(value);
117            if (proxyPolicy == null) {
118                Main.warn(tr("Unexpected value for preference ''{0}'' found. Got ''{1}''. Will use no proxy.",
119                        ProxyPreferencesPanel.PROXY_POLICY, value));
120                proxyPolicy = ProxyPolicy.NO_PROXY;
121            }
122        }
123        String host = Main.pref.get(ProxyPreferencesPanel.PROXY_HTTP_HOST, null);
124        int port = parseProxyPortValue(ProxyPreferencesPanel.PROXY_HTTP_PORT, Main.pref.get(ProxyPreferencesPanel.PROXY_HTTP_PORT, null));
125        httpProxySocketAddress = null;
126        if (proxyPolicy.equals(ProxyPolicy.USE_HTTP_PROXY)) {
127            if (host != null && !host.trim().isEmpty() && port > 0) {
128                httpProxySocketAddress = new InetSocketAddress(host, port);
129            } else {
130                Main.warn(tr("Unexpected parameters for HTTP proxy. Got host ''{0}'' and port ''{1}''.", host, port));
131                Main.warn(tr("The proxy will not be used."));
132            }
133        }
134
135        host = Main.pref.get(ProxyPreferencesPanel.PROXY_SOCKS_HOST, null);
136        port = parseProxyPortValue(ProxyPreferencesPanel.PROXY_SOCKS_PORT, Main.pref.get(ProxyPreferencesPanel.PROXY_SOCKS_PORT, null));
137        socksProxySocketAddress = null;
138        if (proxyPolicy.equals(ProxyPolicy.USE_SOCKS_PROXY)) {
139            if (host != null && !host.trim().isEmpty() && port > 0) {
140                socksProxySocketAddress = new InetSocketAddress(host, port);
141            } else {
142                Main.warn(tr("Unexpected parameters for SOCKS proxy. Got host ''{0}'' and port ''{1}''.", host, port));
143                Main.warn(tr("The proxy will not be used."));
144            }
145        }
146        proxyExceptions = new HashSet<>(
147            Main.pref.getCollection(ProxyPreferencesPanel.PROXY_EXCEPTIONS,
148                    Arrays.asList(new String[]{"localhost", IPV4_LOOPBACK, IPV6_LOOPBACK}))
149        );
150    }
151
152    @Override
153    public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
154        // Just log something. The network stack will also throw an exception which will be caught somewhere else
155        Main.error(tr("Connection to proxy ''{0}'' for URI ''{1}'' failed. Exception was: {2}", sa.toString(), uri.toString(), ioe.toString()));
156        // Remember errors to give a friendly user message asking to review proxy configuration
157        errorResources.add(uri.toString());
158        errorMessages.add(ioe.toString());
159    }
160
161    /**
162     * Returns the set of current proxy resources that failed to be retrieved.
163     * @return the set of current proxy resources that failed to be retrieved
164     * @since 6523
165     */
166    public final Set<String> getErrorResources() {
167        return new TreeSet<>(errorResources);
168    }
169
170    /**
171     * Returns the set of current proxy error messages.
172     * @return the set of current proxy error messages
173     * @since 6523
174     */
175    public final Set<String> getErrorMessages() {
176        return new TreeSet<>(errorMessages);
177    }
178
179    /**
180     * Clear the sets of failed resources and error messages.
181     * @since 6523
182     */
183    public final void clearErrors() {
184        errorResources.clear();
185        errorMessages.clear();
186    }
187
188    /**
189     * Determines if proxy errors have occured.
190     * @return {@code true} if errors have occured, {@code false} otherwise.
191     * @since 6523
192     */
193    public final boolean hasErrors() {
194        return !errorResources.isEmpty();
195    }
196
197    @Override
198    public List<Proxy> select(URI uri) {
199        if (uri != null && proxyExceptions.contains(uri.getHost())) {
200            return NO_PROXY_LIST;
201        }
202        switch(proxyPolicy) {
203        case USE_SYSTEM_SETTINGS:
204            if (!JVM_WILL_USE_SYSTEM_PROXIES) {
205                Main.warn(tr("The JVM is not configured to lookup proxies from the system settings. "+
206                        "The property ''java.net.useSystemProxies'' was missing at startup time.  Will not use a proxy."));
207                return NO_PROXY_LIST;
208            }
209            // delegate to the former proxy selector
210            return delegate.select(uri);
211        case NO_PROXY:
212            return NO_PROXY_LIST;
213        case USE_HTTP_PROXY:
214            if (httpProxySocketAddress == null)
215                return NO_PROXY_LIST;
216            return Collections.singletonList(new Proxy(Type.HTTP, httpProxySocketAddress));
217        case USE_SOCKS_PROXY:
218            if (socksProxySocketAddress == null)
219                return NO_PROXY_LIST;
220            return Collections.singletonList(new Proxy(Type.SOCKS, socksProxySocketAddress));
221        }
222        // should not happen
223        return null;
224    }
225}