blob: 4aa63cd39f014ffe9f61d1e73be63ed966baff89 [file] [log] [blame]
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.util.proxy;
import com.intellij.CommonBundle;
import com.intellij.openapi.application.ApplicationNamesInfo;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.net.NetUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.net.*;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
public class CommonProxy extends ProxySelector {
private final static CommonProxy ourInstance = new CommonProxy();
private final CommonAuthenticator myAuthenticator;
private final static ThreadLocal<Boolean> ourReenterDefence = new ThreadLocal<Boolean>();
public final static List<Proxy> NO_PROXY_LIST = Collections.singletonList(Proxy.NO_PROXY);
private final static long ourErrorInterval = TimeUnit.MINUTES.toMillis(3);
private static volatile int ourNotificationCount;
private volatile static long ourErrorTime = 0;
private volatile static ProxySelector ourWrong;
private static final AtomicReference<Map<String, String>> ourProps = new AtomicReference<Map<String, String>>();
static {
ProxySelector.setDefault(ourInstance);
}
private static final Logger LOG = Logger.getInstance("#com.intellij.util.proxy.CommonProxy");
private final Object myLock;
private final Set<Pair<HostInfo, Thread>> myNoProxy;
private final Map<String, ProxySelector> myCustom;
private final Map<String, NonStaticAuthenticator> myCustomAuth;
private final Set<Pair<HostInfo, Thread>> myNoAuthentication;
public static CommonProxy getInstance() {
return ourInstance;
}
private CommonProxy() {
myLock = new Object();
myNoProxy = new HashSet<Pair<HostInfo, Thread>>();
myCustom = new HashMap<String, ProxySelector>();
myCustomAuth = new HashMap<String, NonStaticAuthenticator>();
myAuthenticator = new CommonAuthenticator();
ensureAuthenticator();
myNoAuthentication = new HashSet<Pair<HostInfo, Thread>>();
}
public static void isInstalledAssertion() {
final ProxySelector aDefault = ProxySelector.getDefault();
if (ourInstance != aDefault) {
// to report only once
if (ourWrong != aDefault || itsTime()) {
LOG.error("ProxySelector.setDefault() was changed to [" + aDefault.toString() + "] - other than com.intellij.util.proxy.CommonProxy.ourInstance.\n" +
"This will make some " + ApplicationNamesInfo.getInstance().getProductName() + " network calls fail.\n" +
"Instead, methods of com.intellij.util.proxy.CommonProxy should be used for proxying.");
ourWrong = aDefault;
}
ProxySelector.setDefault(ourInstance);
ourInstance.ensureAuthenticator();
}
assertSystemPropertiesSet();
}
private static boolean itsTime() {
final boolean b = System.currentTimeMillis() - ourErrorTime > ourErrorInterval && ourNotificationCount < 5;
if (b) {
ourErrorTime = System.currentTimeMillis();
++ ourNotificationCount;
}
return b;
}
private static void assertSystemPropertiesSet() {
final Map<String, String> props = getOldStyleProperties();
final Map<String, String> was = ourProps.get();
if (Comparing.equal(was, props) && ! itsTime()) return;
ourProps.set(props);
final String message = getMessageFromProps(props);
if (message != null) {
// we only intend to somehow report possible misconfiguration
// will not show to the user since on Mac OS this setting is typical
LOG.info(message);
}
}
@Nullable
public static String getMessageFromProps(Map<String, String> props) {
String message = null;
for (Map.Entry<String, String> entry : props.entrySet()) {
if (! StringUtil.isEmptyOrSpaces(entry.getValue())) {
message = CommonBundle.message("label.old.way.jvm.property.used", entry.getKey(), entry.getValue());
break;
}
}
return message;
}
public static Map<String, String> getOldStyleProperties() {
final Map<String, String> props = new HashMap<String, String>();
props.put(JavaProxyProperty.HTTP_HOST, System.getProperty(JavaProxyProperty.HTTP_HOST));
props.put(JavaProxyProperty.HTTPS_HOST, System.getProperty(JavaProxyProperty.HTTPS_HOST));
props.put(JavaProxyProperty.SOCKS_HOST, System.getProperty(JavaProxyProperty.SOCKS_HOST));
return props;
}
public void ensureAuthenticator() {
Authenticator.setDefault(myAuthenticator);
}
public void noProxy(@NotNull final String protocol, @NotNull final String host, final int port) {
synchronized (myLock) {
LOG.debug("no proxy added: " + protocol + "://" + host + ":" + port);
myNoProxy.add(Pair.create(new HostInfo(protocol, host, port), Thread.currentThread()));
}
}
public void removeNoProxy(@NotNull final String protocol, @NotNull final String host, final int port) {
synchronized (myLock) {
LOG.debug("no proxy removed: " + protocol + "://" + host + ":" + port);
myNoProxy.remove(Pair.create(new HostInfo(protocol, host, port), Thread.currentThread()));
}
}
public void noAuthentication(@NotNull final String protocol, @NotNull final String host, final int port) {
synchronized (myLock) {
LOG.debug("no proxy added: " + protocol + "://" + host + ":" + port);
myNoProxy.add(Pair.create(new HostInfo(protocol, host, port), Thread.currentThread()));
}
}
public void removeNoAuthentication(@NotNull final String protocol, @NotNull final String host, final int port) {
synchronized (myLock) {
LOG.debug("no proxy removed: " + protocol + "://" + host + ":" + port);
myNoProxy.remove(Pair.create(new HostInfo(protocol, host, port), Thread.currentThread()));
}
}
public void setCustom(@NotNull final String key, @NotNull final ProxySelector proxySelector) {
synchronized (myLock) {
LOG.debug("custom set: " + key + ", " + proxySelector.toString());
myCustom.put(key, proxySelector);
}
}
public void setCustomAuth(@NotNull final String key, final NonStaticAuthenticator authenticator) {
synchronized (myLock) {
LOG.debug("custom auth set: " + key + ", " + authenticator.toString());
myCustomAuth.put(key, authenticator);
}
}
public void removeCustomAuth(@NotNull final String key) {
synchronized (myLock) {
LOG.debug("custom auth removed: " + key);
myCustomAuth.remove(key);
}
}
public void removeCustom(@NotNull final String key) {
synchronized (myLock) {
LOG.debug("custom set: " + key);
myCustom.remove(key);
}
}
public List<Proxy> select(@NotNull URL url) {
return select(createUri(url));
}
@Override
public List<Proxy> select(@Nullable URI uri) {
isInstalledAssertion();
if (uri == null) {
return NO_PROXY_LIST;
}
LOG.debug("CommonProxy.select called for " + uri.toString());
if (Boolean.TRUE.equals(ourReenterDefence.get())) {
return NO_PROXY_LIST;
}
try {
ourReenterDefence.set(Boolean.TRUE);
String host = StringUtil.notNullize(uri.getHost());
if (NetUtils.isLocalhost(host)) {
return NO_PROXY_LIST;
}
final HostInfo info = new HostInfo(uri.getScheme(), host, correctPortByProtocol(uri));
final Map<String, ProxySelector> copy;
synchronized (myLock) {
if (myNoProxy.contains(Pair.create(info, Thread.currentThread()))) {
LOG.debug("CommonProxy.select returns no proxy (in no proxy list) for " + uri.toString());
return NO_PROXY_LIST;
}
copy = new HashMap<String, ProxySelector>(myCustom);
}
for (Map.Entry<String, ProxySelector> entry : copy.entrySet()) {
final List<Proxy> proxies = entry.getValue().select(uri);
if (!ContainerUtil.isEmpty(proxies)) {
LOG.debug("CommonProxy.select returns custom proxy for " + uri.toString() + ", " + proxies.toString());
return proxies;
}
}
return NO_PROXY_LIST;
}
finally {
ourReenterDefence.remove();
}
}
private static int correctPortByProtocol(@NotNull URI uri) {
if (uri.getPort() == -1) {
if ("http".equals(uri.getScheme())) {
return ProtocolDefaultPorts.HTTP;
} else if ("https".equals(uri.getScheme())) {
return ProtocolDefaultPorts.SSL;
}
}
return uri.getPort();
}
@Override
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
LOG.info("connect failed to " + uri.toString() + ", sa: " + sa.toString(), ioe);
final Map<String, ProxySelector> copy;
synchronized (myLock) {
copy = new HashMap<String, ProxySelector>(myCustom);
}
for (Map.Entry<String, ProxySelector> entry : copy.entrySet()) {
entry.getValue().connectFailed(uri, sa, ioe);
}
}
private class CommonAuthenticator extends Authenticator {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
final String siteStr = getRequestingSite() == null ? null : getRequestingSite().toString();
LOG.debug("CommonAuthenticator.getPasswordAuthentication called for " + siteStr);
final String host = getHostNameReliably(getRequestingHost(), getRequestingSite(), getRequestingURL());
final int port = getRequestingPort();
final Map<String, NonStaticAuthenticator> copy;
synchronized (myLock) {
// for hosts defined as no proxy we will NOT pass authentication to not provoke credentials
final HostInfo hostInfo = new HostInfo(getRequestingProtocol(), host, port);
final Pair<HostInfo, Thread> pair = Pair.create(hostInfo, Thread.currentThread());
if (myNoProxy.contains(pair)) {
LOG.debug("CommonAuthenticator.getPasswordAuthentication found host in no proxies set (" + siteStr + ")");
return null;
}
if (myNoAuthentication.contains(pair)) {
LOG.debug("CommonAuthenticator.getPasswordAuthentication found host in no authentication set (" + siteStr + ")");
return null;
}
copy = new HashMap<String, NonStaticAuthenticator>(myCustomAuth);
}
if (! copy.isEmpty()) {
for (Map.Entry<String, NonStaticAuthenticator> entry : copy.entrySet()) {
final NonStaticAuthenticator authenticator = entry.getValue();
prepareAuthenticator(authenticator);
final PasswordAuthentication authentication = authenticator.getPasswordAuthentication();
if (authentication != null) {
LOG.debug("CommonAuthenticator.getPasswordAuthentication found custom authenticator for " + siteStr + ", " + entry.getKey() +
", " + authenticator.toString());
logAuthentication(authentication);
return authentication;
}
}
}
return null;
}
private void prepareAuthenticator(NonStaticAuthenticator authenticator) {
authenticator.setRequestingHost(getRequestingHost());
authenticator.setRequestingSite(getRequestingSite());
authenticator.setRequestingPort(getRequestingPort());
authenticator.setRequestingProtocol(getRequestingProtocol());//http
authenticator.setRequestingPrompt(getRequestingPrompt());
authenticator.setRequestingScheme(getRequestingScheme());//ntlm
authenticator.setRequestingURL(getRequestingURL());
authenticator.setRequestorType(getRequestorType());
}
private void logAuthentication(PasswordAuthentication authentication) {
if (authentication == null) {
LOG.debug("CommonAuthenticator.getPasswordAuthentication returned null");
} else {
LOG.debug("CommonAuthenticator.getPasswordAuthentication returned authentication pair with login: " + authentication.getUserName());
}
}
}
public static String getHostNameReliably(final String requestingHost, final InetAddress site, final URL requestingUrl) {
String host = requestingHost;
if (host == null) {
if (site != null) {
host = site.getHostName();
}
else if (requestingUrl != null) {
host = requestingUrl.getHost();
}
}
host = host == null ? "" : host;
return host;
}
private static URI createUri(final URL url) {
return VfsUtil.toUri(url.toString());
}
public static class HostInfo {
public String myProtocol;
@NotNull
public String myHost;
public int myPort;
public HostInfo() {
}
public HostInfo(@Nullable String protocol, @NotNull String host, int port) {
myPort = port;
myHost = host;
myProtocol = protocol;
}
public String getProtocol() {
return myProtocol;
}
public String getHost() {
return myHost;
}
public int getPort() {
return myPort;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
HostInfo info = (HostInfo)o;
return myPort == info.myPort && myHost.equals(info.myHost) && Comparing.equal(myProtocol, info.myProtocol);
}
@Override
public int hashCode() {
int result = myProtocol != null ? myProtocol.hashCode() : 0;
result = 31 * result + myHost.hashCode();
result = 31 * result + myPort;
return result;
}
}
}