blob: e844374cf3411bf44a8049882f96616b62ec8541 [file] [log] [blame]
/*
* Copyright 2000-2013 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 org.jetbrains.idea.svn.auth;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.MessageType;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Getter;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.ThrowableComputable;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.changes.committed.AbstractCalledLater;
import com.intellij.openapi.vcs.ui.VcsBalloonProblemNotifier;
import com.intellij.util.EventDispatcher;
import com.intellij.util.SystemProperties;
import com.intellij.util.messages.Topic;
import com.intellij.util.net.HttpConfigurable;
import com.intellij.util.proxy.CommonProxy;
import com.intellij.util.ui.UIUtil;
import com.trilead.ssh2.auth.AgentProxy;
import org.intellij.lang.annotations.MagicConstant;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.idea.svn.*;
import org.jetbrains.idea.svn.config.ProxyGroup;
import org.jetbrains.idea.svn.config.SvnServerFileKeys;
import org.tmatesoft.svn.core.SVNErrorMessage;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNProperties;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.auth.*;
import org.tmatesoft.svn.core.internal.wc.*;
import org.tmatesoft.svn.core.io.SVNRepository;
import java.io.File;
import java.io.IOException;
import java.net.*;
import java.util.*;
/**
* @author alex
*/
public class SvnAuthenticationManager extends DefaultSVNAuthenticationManager implements SvnAuthenticationListener, ISVNAuthenticationManagerExt {
private static final Logger LOG = Logger.getInstance(SvnAuthenticationManager.class);
// while Mac storage not working for IDEA, we use this key to check whether to prompt abt plaintext or just store
public static final String SVN_SSH = "svn+ssh";
public static final String HTTP = "http";
public static final String HTTPS = "https";
public static final String HTTP_PROXY_HOST = "http-proxy-host";
public static final String HTTP_PROXY_PORT = "http-proxy-port";
public static final String HTTP_PROXY_USERNAME = "http-proxy-username";
public static final String HTTP_PROXY_PASSWORD = "http-proxy-password";
private Project myProject;
private File myConfigDirectory;
private ISVNAuthenticationProvider myRuntimeCacheProvider;
private PersistentAuthenticationProviderProxy myPersistentAuthenticationProviderProxy;
private SvnConfiguration myConfig;
private static final ThreadLocal<Boolean> ourJustEntered = new ThreadLocal<Boolean>();
private SvnAuthenticationInteraction myInteraction;
private EventDispatcher<SvnAuthenticationListener> myListener;
private IdeaSVNHostOptionsProvider myLocalHostOptionsProvider;
private final ThreadLocalSavePermissions mySavePermissions;
private final Map<Thread, String> myKeyAlgorithm;
private boolean myArtificialSaving;
private ISVNAuthenticationProvider myProvider;
public static final Topic<ISVNAuthenticationProviderListener> AUTHENTICATION_PROVIDER_LISTENER =
new Topic<ISVNAuthenticationProviderListener>("AUTHENTICATION_PROVIDER_LISTENER", ISVNAuthenticationProviderListener.class);
private final static ThreadLocal<ISVNAuthenticationProvider> ourThreadLocalProvider = new ThreadLocal<ISVNAuthenticationProvider>();
public SvnAuthenticationManager(final Project project, final File configDirectory) {
super(configDirectory, true, null, null);
myProject = project;
myConfigDirectory = configDirectory;
myKeyAlgorithm = new HashMap<Thread, String>();
ensureListenerCreated();
mySavePermissions = new ThreadLocalSavePermissions();
myConfig = SvnConfiguration.getInstance(myProject);
if (myPersistentAuthenticationProviderProxy != null) {
myPersistentAuthenticationProviderProxy.setProject(myProject);
}
myInteraction = new MySvnAuthenticationInteraction(myProject);
Disposer.register(project, new Disposable() {
@Override
public void dispose() {
myProject = null;
if (myPersistentAuthenticationProviderProxy != null) {
myPersistentAuthenticationProviderProxy.myProject = null;
((MyKeyringMasterKeyProvider) myPersistentAuthenticationProviderProxy.myISVNGnomeKeyringPasswordProvider).myProject = null;
myPersistentAuthenticationProviderProxy = null;
}
if (myInteraction instanceof MySvnAuthenticationInteraction) {
((MySvnAuthenticationInteraction) myInteraction).myProject = null;
}
if (myConfig != null) {
myConfig.clear();
myConfig = null;
}
myInteraction = null;
}
});
// This is not the same instance as DefaultSVNAuthenticationManager.myProviders[1], but currently
// DefaultSVNAuthenticationManager.CacheAuthenticationProvider uses only its outer class state - so we utilize necessary logic with
// this new instance.
myRuntimeCacheProvider = createRuntimeAuthenticationProvider();
}
public SVNAuthentication requestFromCache(String kind,
SVNURL url,
String realm,
SVNErrorMessage errorMessage,
SVNAuthentication previousAuth,
boolean authMayBeStored) {
return myRuntimeCacheProvider.requestClientAuthentication(kind, url, realm, errorMessage, previousAuth, authMayBeStored);
}
public String getDefaultUsername(String kind, SVNURL url) {
String result = SystemProperties.getUserName();
// USERNAME authentication is also requested in SVNSSHConnector.open()
if (ISVNAuthenticationManager.SSH.equals(kind) ||
(ISVNAuthenticationManager.USERNAME.equals(kind) && SVN_SSH.equals(url.getProtocol()))) {
result = url != null && !StringUtil.isEmpty(url.getUserInfo()) ? url.getUserInfo() : getDefaultOptions().getDefaultSSHUserName();
}
return result;
}
@Override
protected SVNSSHAuthentication getDefaultSSHAuthentication(SVNURL url) {
String userName = getDefaultUsername(ISVNAuthenticationManager.SSH, url);
// This is fully copied from base class - DefaultSVNAuthenticationManager - as there are no setters in Authentication classes
// and there is no url parameter if overriding getDefaultOptions()
String password = getDefaultOptions().getDefaultSSHPassword();
String keyFile = getDefaultOptions().getDefaultSSHKeyFile();
int port = getDefaultOptions().getDefaultSSHPortNumber();
String passphrase = getDefaultOptions().getDefaultSSHPassphrase();
if (userName != null && password != null) {
return new SVNSSHAuthentication(userName, password, port, getHostOptionsProvider().getHostOptions(url).isAuthStorageEnabled(), url,
false);
}
else if (userName != null && keyFile != null) {
return new SVNSSHAuthentication(userName, new File(keyFile), passphrase, port,
getHostOptionsProvider().getHostOptions(url).isAuthStorageEnabled(), url, false);
}
return null;
}
private class AuthenticationProviderProxy implements ISVNAuthenticationProvider {
private final ISVNAuthenticationProvider myDelegate;
private AuthenticationProviderProxy(ISVNAuthenticationProvider delegate) {
myDelegate = delegate;
}
@Override
public SVNAuthentication requestClientAuthentication(String kind,
SVNURL url,
String realm,
SVNErrorMessage errorMessage,
SVNAuthentication previousAuth, boolean authMayBeStored) {
final SVNAuthentication authentication =
myDelegate.requestClientAuthentication(kind, url, realm, errorMessage, previousAuth, authMayBeStored);
if (myProject != null && ! myProject.isDisposed()) {
myProject.getMessageBus().syncPublisher(AUTHENTICATION_PROVIDER_LISTENER)
.requestClientAuthentication(kind, url, realm, errorMessage, previousAuth, authMayBeStored, authentication);
}
return authentication;
}
@Override
public int acceptServerAuthentication(SVNURL url,
String realm,
Object certificate,
boolean resultMayBeStored) {
final int result = myDelegate.acceptServerAuthentication(url, realm, certificate, resultMayBeStored);
if (myProject != null && ! myProject.isDisposed()) {
myProject.getMessageBus().syncPublisher(AUTHENTICATION_PROVIDER_LISTENER)
.acceptServerAuthentication(url, realm, certificate, resultMayBeStored, result);
}
return result;
}
}
public interface ISVNAuthenticationProviderListener {
void requestClientAuthentication(String kind, SVNURL url, String realm, SVNErrorMessage errorMessage,
SVNAuthentication previousAuth, boolean authMayBeStored, SVNAuthentication authentication);
void acceptServerAuthentication(SVNURL url, String realm, Object certificate, boolean resultMayBeStored, @MagicConstant int acceptResult);
}
@Override
public void setAuthenticationProvider(ISVNAuthenticationProvider provider) {
ISVNAuthenticationProvider useProvider = provider;
if (! (provider instanceof AuthenticationProviderProxy)) {
useProvider = new AuthenticationProviderProxy(provider);
}
myProvider = useProvider;
super.setAuthenticationProvider(myProvider);
}
public ISVNAuthenticationProvider getProvider() {
final ISVNAuthenticationProvider threadProvider = ourThreadLocalProvider.get();
if (threadProvider != null) return threadProvider;
return myProvider;
}
/**
* Gets authentication provider without looking into thread local storage for providers.
*
* TODO:
* Thread local storage is used "for some interaction with SVNKit" and is not always cleared correctly. So some threads contain
* "passive provider" in thread local storage - and getProvider() returns this "passive provider". This occurs, for instance when
* RemoteRevisionsCache is refreshed in background - after its execution, corresponding thread has "passive provider" in thread local
* storage.
*
* As a result authentication fails in such cases (at least for command line implementation). To fix this, command line implementation is
* updated not to check thread local storage at all.
*
* @return
*/
public ISVNAuthenticationProvider getInnerProvider() {
return myProvider;
}
@Override
public ISVNAuthenticationStorage getRuntimeAuthStorage() {
return super.getRuntimeAuthStorage();
}
// since set to null during dispose and we have background processes
private SvnConfiguration getConfig() {
if (myConfig == null) throw new ProcessCanceledException();
return myConfig;
}
public void setArtificialSaving(boolean artificialSaving) {
myArtificialSaving = artificialSaving;
}
private void ensureListenerCreated() {
if (myListener == null) {
myListener = EventDispatcher.create(SvnAuthenticationListener.class);
}
}
@Override
public IdeaSVNHostOptionsProvider getHostOptionsProvider() {
if (myLocalHostOptionsProvider == null) {
myLocalHostOptionsProvider = new IdeaSVNHostOptionsProvider();
}
return myLocalHostOptionsProvider;
}
public void addListener(final SvnAuthenticationListener listener) {
myListener.addListener(listener);
}
@Override
public void actualSaveWillBeTried(ProviderType type, SVNURL url, String realm, String kind) {
myListener.getMulticaster().actualSaveWillBeTried(type, url, realm, kind);
}
@Override
public void saveAttemptStarted(ProviderType type, SVNURL url, String realm, String kind) {
myListener.getMulticaster().saveAttemptStarted(type, url, realm, kind);
}
@Override
public void saveAttemptFinished(ProviderType type, SVNURL url, String realm, String kind) {
myListener.getMulticaster().saveAttemptFinished(type, url, realm, kind);
}
@Override
public void acknowledge(boolean accepted, String kind, String realm, SVNErrorMessage message, SVNAuthentication authentication) {
myListener.getMulticaster().acknowledge(accepted, kind, realm, message, authentication);
}
@Override
public void requested(ProviderType type, SVNURL url, String realm, String kind, boolean canceled) {
if (ProviderType.interactive.equals(type) && (! canceled)) {
ourJustEntered.set(true);
}
myListener.getMulticaster().requested(type, url, realm, kind, canceled);
}
@Override
protected ISVNAuthenticationProvider createCacheAuthenticationProvider(File authDir, String userName) {
// this is a hack due to the fact this method is called from super() constructor
myConfigDirectory = new File(authDir.getParent());
myPersistentAuthenticationProviderProxy = new PersistentAuthenticationProviderProxy(authDir, userName);
return myPersistentAuthenticationProviderProxy;
}
private class PersistentAuthenticationProviderProxy implements ISVNAuthenticationProvider, ISVNPersistentAuthenticationProvider {
private final ISVNAuthenticationProvider myDelegate;
private final ISVNGnomeKeyringPasswordProvider myISVNGnomeKeyringPasswordProvider;
private final File myAuthDir;
private Project myProject;
private PersistentAuthenticationProviderProxy(File authDir, String userName) {
myISVNGnomeKeyringPasswordProvider = new MyKeyringMasterKeyProvider(myProject);
ISVNAuthenticationStorageOptions delegatingOptions = new ISVNAuthenticationStorageOptions() {
public boolean isNonInteractive() throws SVNException {
return getAuthenticationStorageOptions().isNonInteractive();
}
public ISVNAuthStoreHandler getAuthStoreHandler() throws SVNException {
return getAuthenticationStorageOptions().getAuthStoreHandler();
}
@Override
public ISVNGnomeKeyringPasswordProvider getGnomeKeyringPasswordProvider() {
return myISVNGnomeKeyringPasswordProvider;
}
@Override
public boolean isSSLPassphrasePromptSupported() {
return false;
}
};
ensureListenerCreated();
myDelegate = new DefaultSVNPersistentAuthenticationProvider(authDir, userName, delegatingOptions, getDefaultOptions(), getHostOptionsProvider()) {
@Override
protected IPasswordStorage[] createPasswordStorages(DefaultSVNOptions options) {
final IPasswordStorage[] passwordStorages = super.createPasswordStorages(options);
final IPasswordStorage[] proxied = new IPasswordStorage[passwordStorages.length];
for (int i = 0; i < passwordStorages.length; i++) {
final IPasswordStorage storage = passwordStorages[i];
proxied[i] = new ProxyPasswordStorageForDebug(storage, myListener);
}
return proxied;
}
};
myAuthDir = authDir;
}
public void setProject(Project project) {
myProject = project;
}
public SVNAuthentication requestClientAuthentication(final String kind, final SVNURL url, final String realm, final SVNErrorMessage errorMessage,
final SVNAuthentication previousAuth, final boolean authMayBeStored) {
try {
return wrapNativeCall(new ThrowableComputable<SVNAuthentication, SVNException>() {
@Override
public SVNAuthentication compute() throws SVNException {
final SVNAuthentication svnAuthentication =
myDelegate.requestClientAuthentication(kind, url, realm, errorMessage, previousAuth, authMayBeStored);
myListener.getMulticaster().requested(ProviderType.persistent, url, realm, kind, svnAuthentication == null);
return svnAuthentication;
}
});
}
catch (SVNException e) {
LOG.info(e);
throw new RuntimeException(e);
}
}
public int acceptServerAuthentication(final SVNURL url, final String realm, final Object certificate, final boolean resultMayBeStored) {
return ACCEPTED_TEMPORARY;
}
private void actualSavePermissions(String realm, SVNAuthentication auth) {
final String actualKind = auth.getKind();
File dir = new File(myAuthDir, actualKind);
String fileName = SVNFileUtil.computeChecksum(realm);
File authFile = new File(dir, fileName);
try {
((ISVNPersistentAuthenticationProvider) myDelegate).saveAuthentication(auth, actualKind, realm);
}
catch (SVNException e) {
if (myProject != null) {
ApplicationManager.getApplication().invokeLater(new VcsBalloonProblemNotifier(myProject,
"<b>Problem when storing Subversion credentials:</b>&nbsp;" + e.getMessage(), MessageType.ERROR));
}
}
finally {
// do not make password file readonly
setWriteable(authFile);
}
}
public void saveAuthentication(final SVNAuthentication auth, final String kind, final String realm) throws SVNException {
try {
wrapNativeCall(new ThrowableComputable<Void, SVNException>() {
@Override
public Void compute() throws SVNException {
final Boolean fromInteractive = ourJustEntered.get();
ourJustEntered.set(null);
if (! myArtificialSaving && ! Boolean.TRUE.equals(fromInteractive)) {
// not what user entered
return null;
}
myListener.getMulticaster().saveAttemptStarted(ProviderType.persistent, auth.getURL(), realm, auth.getKind());
((ISVNPersistentAuthenticationProvider) myDelegate).saveAuthentication(auth, kind, realm);
myListener.getMulticaster().saveAttemptFinished(ProviderType.persistent, auth.getURL(), realm, auth.getKind());
return null;
}
});
}
catch (SVNException e) {
LOG.info(e);
throw new RuntimeException(e);
}
}
@Override
public void saveFingerprints(final String realm, final byte[] fingerprints) {
try {
wrapNativeCall(new ThrowableComputable<Void, SVNException>() {
@Override
public Void compute() throws SVNException {
((ISVNPersistentAuthenticationProvider) myDelegate).saveFingerprints(realm, fingerprints);
return null;
}
});
}
catch (SVNException e) {
LOG.info(e);
throw new RuntimeException(e);
}
}
@Override
public byte[] loadFingerprints(final String realm) {
try {
return wrapNativeCall(new ThrowableComputable<byte[], SVNException>() {
@Override
public byte[] compute() throws SVNException {
return ((ISVNPersistentAuthenticationProvider) myDelegate).loadFingerprints(realm);
}
});
}
catch (SVNException e) {
LOG.info(e);
throw new RuntimeException(e);
}
}
private final static int maxAttempts = 10;
private void setWriteable(final File file) {
if (! file.exists()) return;
if (file.getParentFile() == null) {
return;
}
for (int i = 0; i < maxAttempts; i++) {
final File parent = file.getParentFile();
try {
final File tempFile = FileUtil.createTempFile(parent, "123", "1", true);
FileUtil.delete(tempFile);
if (! file.renameTo(tempFile)) continue;
if (! file.createNewFile()) continue;
FileUtil.copy(tempFile, file);
FileUtil.delete(tempFile);
return;
}
catch (IOException e) {
//
}
}
}
}
@Override
public void verifyHostKey(String hostName, int port, String keyAlgorithm, byte[] hostKey) throws SVNException {
myKeyAlgorithm.put(Thread.currentThread(), keyAlgorithm);
try {
super.verifyHostKey(hostName, port, keyAlgorithm, hostKey);
} finally {
myKeyAlgorithm.remove(Thread.currentThread());
}
}
@Nullable
public String getSSHKeyAlgorithm() {
return myKeyAlgorithm.get(Thread.currentThread());
}
@Override
public void acknowledgeConnectionSuccessful(SVNURL url, String method) {
CommonProxy.getInstance().removeNoProxy(url.getProtocol(), url.getHost(), url.getPort());
SSLExceptionsHelper.removeInfo();
ourThreadLocalProvider.remove();
}
@Override
public void acknowledgeAuthentication(boolean accepted,
String kind,
String realm,
SVNErrorMessage errorMessage,
SVNAuthentication authentication) throws SVNException {
acknowledgeAuthentication(accepted, kind, realm, errorMessage, authentication, null);
}
@Override
public void acknowledgeAuthentication(boolean accepted,
String kind,
String realm,
SVNErrorMessage errorMessage,
SVNAuthentication authentication,
SVNURL url) throws SVNException {
showSshAgentErrorIfAny(errorMessage, authentication);
SSLExceptionsHelper.removeInfo();
ourThreadLocalProvider.remove();
if (url != null) {
CommonProxy.getInstance().removeNoProxy(url.getProtocol(), url.getHost(), url.getPort());
}
boolean successSaving = false;
myListener.getMulticaster().acknowledge(accepted, kind, realm, errorMessage, authentication);
try {
final boolean authStorageEnabled = getHostOptionsProvider().getHostOptions(authentication.getURL()).isAuthStorageEnabled();
final SVNAuthentication proxy = ProxySvnAuthentication.proxy(authentication, authStorageEnabled, myArtificialSaving);
super.acknowledgeAuthentication(accepted, kind, realm, errorMessage, proxy);
successSaving = true;
} finally {
mySavePermissions.remove();
if (myArtificialSaving) {
myArtificialSaving = false;
throw new CredentialsSavedException(successSaving);
}
}
}
/**
* "Pageant is not running" error thrown in PageantConnector.query() method is caught and "eaten" in SVNKit logic.
* So for both cases "Pageant is not running" and "There are no valid keys in agent (both no keys at all and no valid keys for host)"
* we will get same "Credentials rejected by SSH server" error.
*/
private void showSshAgentErrorIfAny(@Nullable SVNErrorMessage errorMessage, @Nullable SVNAuthentication authentication) {
if (errorMessage != null && authentication instanceof SVNSSHAuthentication) {
AgentProxy agentProxy = ((SVNSSHAuthentication)authentication).getAgentProxy();
if (agentProxy != null) {
// TODO: Most likely this should be updated with new VcsNotifier api.
VcsBalloonProblemNotifier.showOverChangesView(myProject, errorMessage.getFullMessage(), MessageType.ERROR);
}
}
}
public void acknowledgeForSSL(boolean accepted, SVNAuthentication proxy) {
if (accepted && proxy instanceof SVNSSLAuthentication && (((SVNSSLAuthentication) proxy).getCertificateFile() != null)) {
final SVNSSLAuthentication svnsslAuthentication = (SVNSSLAuthentication)proxy;
final SVNURL url = svnsslAuthentication.getURL();
final IdeaSVNHostOptionsProvider provider = getHostOptionsProvider();
final SVNCompositeConfigFile serversFile = provider.getServersFile();
String groupName = getGroupName(serversFile.getProperties("groups"), url.getHost());
groupName = StringUtil.isEmptyOrSpaces(groupName) ? "global" : groupName;
serversFile
.setPropertyValue(groupName, SvnServerFileKeys.SSL_CLIENT_CERT_FILE, svnsslAuthentication.getCertificateFile().getPath(), true);
serversFile.save();
}
}
public ISVNProxyManager getProxyManager(SVNURL url) throws SVNException {
SSLExceptionsHelper.addInfo("Accessing URL: " + url.toString());
ourThreadLocalProvider.set(myProvider);
// in proxy creation, we need proxy information from common proxy. but then we should forbid common proxy to intercept
final ISVNProxyManager proxy = createProxy(url);
CommonProxy.getInstance().noProxy(url.getProtocol(), url.getHost(), url.getPort());
return proxy;
}
private ISVNProxyManager createProxy(SVNURL url) {
// this code taken from default manager (changed for system properties reading)
String host = url.getHost();
String proxyHost = getServersPropertyIdea(host, HTTP_PROXY_HOST);
if (StringUtil.isEmptyOrSpaces(proxyHost)) {
if (getConfig().isIsUseDefaultProxy()) {
// ! use common proxy if it is set
try {
final List<Proxy> proxies = HttpConfigurable.getInstance().getOnlyBySettingsSelector().select(new URI(url.toString()));
if (proxies != null && ! proxies.isEmpty()) {
for (Proxy proxy : proxies) {
if (HttpConfigurable.isRealProxy(proxy) && Proxy.Type.HTTP.equals(proxy.type())) {
final SocketAddress address = proxy.address();
if (address instanceof InetSocketAddress) {
return new MyPromptingProxyManager(((InetSocketAddress)address).getHostName(),
String.valueOf(((InetSocketAddress)address).getPort()), url.getProtocol());
}
}
}
}
}
catch (URISyntaxException e) {
LOG.info(e);
}
}
return null;
}
String proxyExceptions = getServersPropertyIdea(host, "http-proxy-exceptions");
String proxyExceptionsSeparator = ",";
if (proxyExceptions == null) {
proxyExceptions = System.getProperty("http.nonProxyHosts");
proxyExceptionsSeparator = "|";
}
if (proxyExceptions != null) {
for(StringTokenizer exceptions = new StringTokenizer(proxyExceptions, proxyExceptionsSeparator); exceptions.hasMoreTokens();) {
String exception = exceptions.nextToken().trim();
if (DefaultSVNOptions.matches(exception, host)) {
return null;
}
}
}
String proxyPort = getServersPropertyIdea(host, HTTP_PROXY_PORT);
String proxyUser = getServersPropertyIdea(host, HTTP_PROXY_USERNAME);
String proxyPassword = getServersPropertyIdea(host, HTTP_PROXY_PASSWORD);
return new MySimpleProxyManager(proxyHost, proxyPort, proxyUser, proxyPassword);
}
private static class MyPromptingProxyManager extends MySimpleProxyManager {
private final String myProtocol;
private MyPromptingProxyManager(final String host, final String port, String protocol) {
super(host, port, null, null);
myProtocol = protocol;
}
@Override
public String getProxyUserName() {
if (myProxyUser != null) {
return myProxyUser;
}
tryGetCredentials();
return myProxyUser;
}
private void tryGetCredentials() {
try {
final InetAddress ia = InetAddress.getByName(getProxyHost());
final PasswordAuthentication authentication =
Authenticator.requestPasswordAuthentication(getProxyHost(), ia, getProxyPort(), myProtocol, getProxyHost(), myProtocol,
null, Authenticator.RequestorType.PROXY);
if (authentication != null) {
myProxyUser = authentication.getUserName();
myProxyPassword = String.valueOf(authentication.getPassword());
}
}
catch (UnknownHostException e) {
//
}
}
@Override
public String getProxyPassword() {
if (myProxyPassword != null) {
return myProxyPassword;
}
tryGetCredentials();
return myProxyPassword;
}
}
private static class MySimpleProxyManager implements ISVNProxyManager {
protected String myProxyHost;
private final String myProxyPort;
protected String myProxyUser;
protected String myProxyPassword;
public MySimpleProxyManager(String host, String port, String user, String password) {
myProxyHost = host;
myProxyPort = port == null ? "3128" : port;
myProxyUser = user;
myProxyPassword = password;
}
public String getProxyHost() {
return myProxyHost;
}
public int getProxyPort() {
try {
return Integer.parseInt(myProxyPort);
} catch (NumberFormatException nfe) {
//
}
return 3128;
}
public String getProxyUserName() {
return myProxyUser;
}
public String getProxyPassword() {
return myProxyPassword;
}
public void acknowledgeProxyContext(boolean accepted, SVNErrorMessage errorMessage) {
}
}
// 30 seconds
private final static int DEFAULT_READ_TIMEOUT = 30 * 1000;
@Override
public int getReadTimeout(final SVNRepository repository) {
String protocol = repository.getLocation().getProtocol();
if (HTTP.equals(protocol) || HTTPS.equals(protocol)) {
String host = repository.getLocation().getHost();
String timeout = getServersPropertyIdea(host, "http-timeout");
if (timeout != null) {
try {
return Integer.parseInt(timeout)*1000;
} catch (NumberFormatException nfe) {
// use default
}
}
return DEFAULT_READ_TIMEOUT;
}
if (SVN_SSH.equals(protocol)) {
return (int)getConfig().getSshReadTimeout();
}
return 0;
}
@Override
public int getConnectTimeout(SVNRepository repository) {
String protocol = repository.getLocation().getProtocol();
if (SVN_SSH.equals(protocol)) {
return (int)getConfig().getSshConnectionTimeout();
}
final int connectTimeout = super.getConnectTimeout(repository);
if ((HTTP.equals(protocol) || HTTPS.equals(protocol)) && (connectTimeout <= 0)) {
return DEFAULT_READ_TIMEOUT;
}
return connectTimeout;
}
// taken from default manager as is
private String getServersPropertyIdea(String host, final String name) {
final SVNCompositeConfigFile serversFile = getHostOptionsProvider().getServersFile();
return getPropertyIdea(host, serversFile, name);
}
private static String getPropertyIdea(String host, SVNCompositeConfigFile serversFile, final String name) {
String groupName = getGroupName(serversFile.getProperties("groups"), host);
if (groupName != null) {
Map hostProps = serversFile.getProperties(groupName);
final String value = (String)hostProps.get(name);
if (value != null) {
return value;
}
}
Map globalProps = serversFile.getProperties("global");
return (String) globalProps.get(name);
}
public static boolean checkHostGroup(final String url, final String patterns, final String exceptions) {
final SVNURL svnurl;
try {
svnurl = SVNURL.parseURIEncoded(url);
}
catch (SVNException e) {
return false;
}
final String host = svnurl.getHost();
return matches(patterns, host) && (! matches(exceptions, host));
}
private static boolean matches(final String pattern, final String host) {
final StringTokenizer tokenizer = new StringTokenizer(pattern, ",");
while(tokenizer.hasMoreTokens()) {
String token = tokenizer.nextToken();
if (DefaultSVNOptions.matches(token, host)) {
return true;
}
}
return false;
}
@Nullable
public static String getGroupForHost(final String host, final IdeaSVNConfigFile serversFile) {
final Map<String,ProxyGroup> groups = serversFile.getAllGroups();
for (Map.Entry<String, ProxyGroup> entry : groups.entrySet()) {
if (matchesGroupPattern(host, entry.getValue().getPatterns())) return entry.getKey();
}
return null;
}
// taken from default manager as is
private static String getGroupName(Map groups, String host) {
for (Object o : groups.keySet()) {
final String name = (String) o;
final String pattern = (String) groups.get(name);
if (matchesGroupPattern(host, pattern)) return name;
}
return null;
}
private static boolean matchesGroupPattern(String host, String pattern) {
for(StringTokenizer tokens = new StringTokenizer(pattern, ","); tokens.hasMoreTokens();) {
String token = tokens.nextToken();
if (DefaultSVNOptions.matches(token, host)) {
return true;
}
}
return false;
}
// default = yes
private static boolean isTurned(final String value) {
return value == null || "yes".equalsIgnoreCase(value) || "on".equalsIgnoreCase(value) || "true".equalsIgnoreCase(value);
}
private static ModalityState getCurrent() {
if (ApplicationManager.getApplication().isDispatchThread()) {
return ModalityState.current();
}
final ProgressIndicator pi = ProgressManager.getInstance().getProgressIndicator();
if (pi == null) {
return ModalityState.defaultModalityState();
}
return pi.getModalityState();
}
public void setInteraction(SvnAuthenticationInteraction interaction) {
myInteraction = interaction;
}
private static class MySvnAuthenticationInteraction implements SvnAuthenticationInteraction {
private Project myProject;
private MySvnAuthenticationInteraction(Project project) {
myProject = project;
}
@Override
public void warnOnAuthStorageDisabled(SVNURL url) {
VcsBalloonProblemNotifier.showOverChangesView(myProject, "Cannot store credentials: forbidden by \"store-auth-creds=no\"", MessageType.ERROR);
}
@Override
public void warnOnPasswordStorageDisabled(SVNURL url) {
VcsBalloonProblemNotifier.showOverChangesView(myProject, "Cannot store password: forbidden by \"store-passwords=no\"", MessageType.ERROR);
}
@Override
public void warnOnSSLPassphraseStorageDisabled(SVNURL url) {
VcsBalloonProblemNotifier.showOverChangesView(myProject, "Cannot store passphrase: forbidden by \"store-ssl-client-cert-pp=no\"", MessageType.ERROR);
}
@Override
public boolean promptForPlaintextPasswordSaving(SVNURL url, String realm) {
final int answer = Messages.showYesNoDialog(myProject, String.format("Your password for authentication realm:\n" +
"%s\ncan only be stored to disk unencrypted. Would you like to store it in plaintext?", realm),
"Store the password in plaintext?", Messages.getQuestionIcon());
return answer == Messages.YES;
}
@Override
public boolean promptInAwt() {
return true;
}
@Override
public boolean promptForSSLPlaintextPassphraseSaving(SVNURL url, String realm, File certificateFile, String certificateName) {
final int answer = Messages.showYesNoDialog(myProject,
String.format("Your passphrase for " + certificateName + ":\n%s\ncan only be stored to disk unencrypted. Would you like to store it in plaintext?",
certificateFile.getPath()),
"Store the passphrase in plaintext?", Messages.getQuestionIcon());
return answer == Messages.YES;
}
@Override
public void dispose() {
myProject = null;
}
}
public class IdeaSVNHostOptionsProvider extends DefaultSVNHostOptionsProvider {
public IdeaSVNHostOptionsProvider() {
super(myConfigDirectory);
}
@Override
public SVNCompositeConfigFile getServersFile() {
return super.getServersFile();
}
@Override
public ISVNHostOptions getHostOptions(SVNURL url) {
return new IdeaSVNHostOptions(getServersFile(), url);
}
}
private static class ThreadLocalSavePermissions {
private final Map<Thread, Boolean> myPlainTextAllowed;
private ThreadLocalSavePermissions() {
myPlainTextAllowed = Collections.synchronizedMap(new HashMap<Thread, Boolean>());
}
public void put(final boolean value) {
myPlainTextAllowed.put(Thread.currentThread(), value);
}
public boolean have() {
return myPlainTextAllowed.containsKey(Thread.currentThread());
}
public void remove() {
myPlainTextAllowed.remove(Thread.currentThread());
}
public boolean allowed() {
return Boolean.TRUE.equals(myPlainTextAllowed.get(Thread.currentThread()));
}
}
private class IdeaSVNHostOptions extends DefaultSVNHostOptions {
private SVNCompositeConfigFile myConfigFile;
private final SVNURL myUrl;
private IdeaSVNHostOptions(SVNCompositeConfigFile serversFile, SVNURL url) {
super(serversFile, url);
myUrl = url;
}
@Override
public boolean isStorePlainTextPasswords(final String realm, SVNAuthentication auth) throws SVNException {
if (USERNAME.equals(auth.getKind())) return true;
final boolean superValue = super.isStorePlainTextPasswords(realm, auth);
final boolean value = mySavePermissions.allowed() || superValue;
if ((! value) && (! mySavePermissions.have())) {
promptAndSaveWhenWeLackEncryption(realm, auth, new Getter<Boolean>() {
@Override
public Boolean get() {
return myInteraction.promptForPlaintextPasswordSaving(myUrl, realm);
}
});
}
return value;
}
@Override
public boolean isStorePlainTextPassphrases(final String realm, final SVNAuthentication auth) throws SVNException {
if (USERNAME.equals(auth.getKind())) return true;
final boolean value = mySavePermissions.allowed() || super.isStorePlainTextPassphrases(realm, auth);
if ((! value) && (! mySavePermissions.have())) {
promptAndSaveWhenWeLackEncryption(realm, auth, new Getter<Boolean>() {
@Override
public Boolean get() {
File file = null;
String certificateName = null;
if (auth instanceof SVNSSLAuthentication) {
file = ((SVNSSLAuthentication) auth).getCertificateFile();
certificateName = "client certificate";
} else if (auth instanceof SVNSSHAuthentication) {
file = ((SVNSSHAuthentication) auth).getPrivateKeyFile();
certificateName = "private key file";
} else {
assert false;
}
return myInteraction.promptForSSLPlaintextPassphraseSaving(myUrl, realm,
file, certificateName);
}
});
}
return value;
}
@Override
public boolean isAuthStorageEnabled() {
final boolean value;
if (hasAuthStorageEnabledOption()) {
value = super.isAuthStorageEnabled();
} else {
value = isTurned(getConfigFile().getPropertyValue("auth", "store-auth-creds"));
}
if (! value) {
myInteraction.warnOnAuthStorageDisabled(myUrl);
}
return value;
}
@Override
public boolean isStorePasswords() {
final String storePasswords = getStorePasswords();
final boolean value;
if (storePasswords != null) {
value = isTurned(storePasswords);
} else {
final String configValue = getConfigFile().getPropertyValue("auth", "store-passwords");
value = isTurned(configValue);
}
if (! value) {
myInteraction.warnOnPasswordStorageDisabled(myUrl);
}
return value;
}
@Override
public boolean isStoreSSLClientCertificatePassphrases() {
final boolean value = super.isStoreSSLClientCertificatePassphrases();
if (! value) {
myInteraction.warnOnSSLPassphraseStorageDisabled(myUrl);
}
return value;
}
public String getStorePasswords() {
return getServersPropertyIdea(getHost(), "store-passwords");
}
private SVNCompositeConfigFile getConfigFile() {
if (myConfigFile == null) {
final File config = new File(myConfigDirectory, "config");
SVNConfigFile.createDefaultConfiguration(myConfigDirectory);
SVNConfigFile userConfig = new SVNConfigFile(config);
SVNConfigFile systemConfig = new SVNConfigFile(new File(SVNFileUtil.getSystemConfigurationDirectory(), "config"));
myConfigFile = new SVNCompositeConfigFile(systemConfig, userConfig);
}
return myConfigFile;
}
private void promptAndSaveWhenWeLackEncryption(final String realm, final SVNAuthentication auth, final Getter<Boolean> prompt) {
final Boolean[] saveOnce = new Boolean[1];
final Runnable actualSave = new Runnable() {
@Override
public void run() {
mySavePermissions.put(Boolean.TRUE.equals(saveOnce[0]));
try {
myPersistentAuthenticationProviderProxy.actualSavePermissions(realm, auth);
}
finally {
mySavePermissions.remove();
}
}
};
if (myInteraction.promptInAwt()) {
new AbstractCalledLater(myProject, getCurrent()) {
@Override
public void run() {
saveOnce[0] = Boolean.TRUE.equals(prompt.get());
ApplicationManager.getApplication().executeOnPooledThread(actualSave);
}
}.callMe();
} else {
saveOnce[0] = Boolean.TRUE.equals(prompt.get());
actualSave.run();
}
}
}
private static class ProxyPasswordStorageForDebug implements DefaultSVNPersistentAuthenticationProvider.IPasswordStorage {
private final DefaultSVNPersistentAuthenticationProvider.IPasswordStorage myDelegate;
private final EventDispatcher<SvnAuthenticationListener> myListener;
public ProxyPasswordStorageForDebug(final DefaultSVNPersistentAuthenticationProvider.IPasswordStorage delegate,
final EventDispatcher<SvnAuthenticationListener> listener) {
myDelegate = delegate;
myListener = listener;
}
@Override
public String getPassType() {
return myDelegate.getPassType();
}
@Override
public boolean savePassword(String realm, String password, SVNAuthentication auth, SVNProperties authParameters) throws SVNException {
final boolean saved = myDelegate.savePassword(realm, password, auth, authParameters);
if (saved) {
myListener.getMulticaster().actualSaveWillBeTried(ProviderType.persistent, auth.getURL(), realm, auth.getKind()
);
}
return saved;
}
@Override
public String readPassword(String realm, String userName, SVNProperties authParameters) throws SVNException {
return myDelegate.readPassword(realm, userName, authParameters);
}
@Override
public boolean savePassphrase(String realm, String passphrase, SVNAuthentication auth, SVNProperties authParameters, boolean force)
throws SVNException {
final boolean saved = myDelegate.savePassphrase(realm, passphrase, auth, authParameters, force);
if (saved) {
myListener.getMulticaster().actualSaveWillBeTried(ProviderType.persistent, auth.getURL(), realm, auth.getKind()
);
}
return saved;
}
@Override
public String readPassphrase(String realm, SVNProperties authParameters) throws SVNException {
return myDelegate.readPassphrase(realm, authParameters);
}
}
private <T> T wrapNativeCall(final ThrowableComputable<T, SVNException> runnable) throws SVNException {
try {
NativeLogReader.startTracking();
final T t = runnable.compute();
final List<NativeLogReader.CallInfo> logged = NativeLogReader.getLogged();
final StringBuilder sb = new StringBuilder();
for (NativeLogReader.CallInfo info : logged) {
final String message = SvnNativeCallsTranslator.getMessage(info);
if (message != null) {
if (sb.length() > 0) sb.append('\n');
sb.append(message);
}
}
if (sb.length() > 0) {
VcsBalloonProblemNotifier.showOverChangesView(myProject, sb.toString(), MessageType.ERROR);
LOG.info(sb.toString());
}
return t;
} finally {
NativeLogReader.clear();
NativeLogReader.endTracking();
}
}
private static class MyKeyringMasterKeyProvider implements ISVNGnomeKeyringPasswordProvider {
private Project myProject;
public MyKeyringMasterKeyProvider(Project project) {
myProject = project;
}
@Override
public String getKeyringPassword(final String keyringName) throws SVNException {
final String message = keyringName != null ? SvnBundle.message("gnome.keyring.prompt.named", keyringName)
: SvnBundle.message("gnome.keyring.prompt.nameless");
final Ref<String> result = Ref.create();
UIUtil.invokeAndWaitIfNeeded(new Runnable() {
@Override
public void run() {
result.set(Messages.showPasswordDialog(myProject, message, SvnBundle.message("subversion.name"), Messages.getQuestionIcon()));
}
});
return result.get();
}
}
public static class CredentialsSavedException extends RuntimeException {
private final boolean mySuccess;
public CredentialsSavedException(boolean success) {
mySuccess = success;
}
public boolean isSuccess() {
return mySuccess;
}
}
}