org.jetbrains.idea.svn.SvnAuthenticationManager.java Source code

Java tutorial

Introduction

Here is the source code for org.jetbrains.idea.svn.SvnAuthenticationManager.java

Source

/*
 * 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;

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.*;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.CalledInAwt;
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.auth.ProviderType;
import org.jetbrains.idea.svn.auth.SvnAuthenticationInteraction;
import org.jetbrains.idea.svn.auth.SvnAuthenticationListener;
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("#org.jetbrains.idea.svn.SvnAuthenticationManager");
    // 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, String kind, String realm, SVNErrorMessage message,
            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());

            if (StringUtil.isEmptyOrSpaces(groupName)) {
                serversFile.setPropertyValue("global", SvnServerFileKeys.SSL_CLIENT_CERT_FILE,
                        svnsslAuthentication.getCertificateFile().getPath(), true);
                //serversFile.setPropertyValue("global", SvnServerFileKeys.SSL_CLIENT_CERT_PASSWORD, null, true);
            } else {
                serversFile.setPropertyValue(groupName, SvnServerFileKeys.SSL_CLIENT_CERT_FILE,
                        svnsslAuthentication.getCertificateFile().getPath(), true);
                //serversFile.setPropertyValue(groupName, SvnServerFileKeys.SSL_CLIENT_CERT_PASSWORD, null, 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 ((proxyHost == null) || "".equals(proxyHost.trim())) {
            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(),
                                            "" + ((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 static final String ourPrompt = "Proxy authentication";
        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 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 ModalityState getCurrent() {
        if (ApplicationManager.getApplication().isDispatchThread()) {
            return ModalityState.current();
        }
        final ProgressIndicator pi = ProgressManager.getInstance().getProgressIndicator();
        if (pi == null) {
            return ModalityState.defaultModalityState();
        }
        return pi.getModalityState();
    }

    /**
     * Shows a yes/no question whether user wants to store his password in plain text and returns his answer.
     * @param title   title of the questioning dialog.
     * @param message questioning message to be displayed.
     * @return true if user agrees to store his password in plaintext, false if he doesn't.
     */
    @CalledInAwt
    private boolean askToStoreUnencrypted(String title, String message) {
        final int answer = Messages.showYesNoDialog(myProject, message, title, Messages.getQuestionIcon());
        return answer == Messages.YES;
    }

    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;
        }
    }

    private static boolean isLion() {
        return SystemInfo.isMac && SystemInfo.isMacOSSnowLeopard && !SystemInfo.OS_VERSION.startsWith("10.6");
    }

    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;
        }
    }
}