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