| /* |
| * Copyright 2000-2014 JetBrains s.r.o. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.jetbrains.idea.svn.auth; |
| |
| import com.intellij.notification.NotificationType; |
| import com.intellij.openapi.application.Application; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.application.ModalityState; |
| import com.intellij.openapi.diagnostic.Logger; |
| 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.ui.popup.Balloon; |
| import com.intellij.openapi.ui.popup.JBPopupFactory; |
| import com.intellij.openapi.util.NamedRunnable; |
| import com.intellij.openapi.util.Ref; |
| import com.intellij.openapi.util.io.FileUtil; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.openapi.vcs.impl.GenericNotifierImpl; |
| import com.intellij.openapi.vcs.ui.VcsBalloonProblemNotifier; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.openapi.wm.IdeFrame; |
| import com.intellij.openapi.wm.ex.WindowManagerEx; |
| import com.intellij.ui.awt.RelativePoint; |
| import com.intellij.util.Consumer; |
| import com.intellij.util.ThreeState; |
| import com.intellij.util.net.HttpConfigurable; |
| import com.intellij.util.proxy.CommonProxy; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| import org.jetbrains.idea.svn.*; |
| import org.jetbrains.idea.svn.api.ClientFactory; |
| import org.jetbrains.idea.svn.commandLine.SvnBindException; |
| import org.jetbrains.idea.svn.info.Info; |
| import org.jetbrains.idea.svn.info.InfoClient; |
| import org.tmatesoft.svn.core.SVNAuthenticationException; |
| import org.tmatesoft.svn.core.SVNCancelException; |
| import org.tmatesoft.svn.core.SVNException; |
| import org.tmatesoft.svn.core.SVNURL; |
| import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager; |
| import org.tmatesoft.svn.core.auth.SVNAuthentication; |
| import org.tmatesoft.svn.core.internal.util.SVNURLUtil; |
| import org.tmatesoft.svn.core.wc.SVNRevision; |
| |
| import javax.swing.*; |
| import java.awt.*; |
| import java.io.File; |
| import java.io.FilenameFilter; |
| import java.net.*; |
| import java.util.*; |
| import java.util.List; |
| import java.util.Timer; |
| |
| public class SvnAuthenticationNotifier extends GenericNotifierImpl<SvnAuthenticationNotifier.AuthenticationRequest, SVNURL> { |
| private static final Logger LOG = Logger.getInstance(SvnAuthenticationNotifier.class); |
| |
| private static final List<String> ourAuthKinds = Arrays.asList(ISVNAuthenticationManager.PASSWORD, ISVNAuthenticationManager.SSH, |
| ISVNAuthenticationManager.SSL, ISVNAuthenticationManager.USERNAME, "svn.ssl.server", "svn.ssh.server"); |
| |
| private final SvnVcs myVcs; |
| private final RootsToWorkingCopies myRootsToWorkingCopies; |
| private final Map<SVNURL, Boolean> myCopiesPassiveResults; |
| private Timer myTimer; |
| private volatile boolean myVerificationInProgress; |
| |
| public SvnAuthenticationNotifier(final SvnVcs svnVcs) { |
| super(svnVcs.getProject(), svnVcs.getDisplayName(), "Not Logged In to Subversion", NotificationType.ERROR); |
| myVcs = svnVcs; |
| myRootsToWorkingCopies = myVcs.getRootsToWorkingCopies(); |
| myCopiesPassiveResults = Collections.synchronizedMap(new HashMap<SVNURL, Boolean>()); |
| myVerificationInProgress = false; |
| } |
| |
| public void init() { |
| if (myTimer != null) { |
| stop(); |
| } |
| myTimer = new Timer("SVN authentication timer"); |
| // every 10 minutes |
| myTimer.schedule(new TimerTask() { |
| @Override |
| public void run() { |
| myCopiesPassiveResults.clear(); |
| } |
| }, 10000, 10 * 60 * 1000); |
| } |
| |
| public void stop() { |
| myTimer.cancel(); |
| myTimer = null; |
| } |
| |
| @Override |
| protected boolean ask(final AuthenticationRequest obj, String description) { |
| if (myVerificationInProgress) { |
| return showAlreadyChecking(); |
| } |
| myVerificationInProgress = true; |
| |
| final Ref<Boolean> resultRef = new Ref<Boolean>(); |
| |
| final Runnable checker = new Runnable() { |
| @Override |
| public void run() { |
| try { |
| final boolean result = |
| interactiveValidation(obj.myProject, obj.getUrl(), obj.getRealm(), obj.getKind()); |
| log("ask result for: " + obj.getUrl() + " is: " + result); |
| resultRef.set(result); |
| if (result) { |
| onStateChangedToSuccess(obj); |
| } |
| } |
| finally { |
| myVerificationInProgress = false; |
| } |
| } |
| }; |
| final Application application = ApplicationManager.getApplication(); |
| // also do not show auth if thread does not have progress indicator |
| if (application.isReadAccessAllowed() || !ProgressManager.getInstance().hasProgressIndicator()) { |
| application.executeOnPooledThread(checker); |
| } |
| else { |
| checker.run(); |
| return resultRef.get(); |
| } |
| return false; |
| } |
| |
| private boolean showAlreadyChecking() { |
| final IdeFrame frameFor = WindowManagerEx.getInstanceEx().findFrameFor(myProject); |
| if (frameFor != null) { |
| final JComponent component = frameFor.getComponent(); |
| Point point = component.getMousePosition(); |
| if (point == null) { |
| point = new Point((int)(component.getWidth() * 0.7), 0); |
| } |
| SwingUtilities.convertPointToScreen(point, component); |
| JBPopupFactory.getInstance().createHtmlTextBalloonBuilder("Already checking...", MessageType.WARNING, null). |
| createBalloon().show(new RelativePoint(point), Balloon.Position.below); |
| } |
| return false; |
| } |
| |
| private void onStateChangedToSuccess(final AuthenticationRequest obj) { |
| myCopiesPassiveResults.put(getKey(obj), true); |
| myVcs.invokeRefreshSvnRoots(); |
| |
| final List<SVNURL> outdatedRequests = new LinkedList<SVNURL>(); |
| final Collection<SVNURL> keys = getAllCurrentKeys(); |
| for (SVNURL key : keys) { |
| final SVNURL commonURLAncestor = SVNURLUtil.getCommonURLAncestor(key, obj.getUrl()); |
| if ((commonURLAncestor != null) && (! StringUtil.isEmptyOrSpaces(commonURLAncestor.getHost())) && |
| (! StringUtil.isEmptyOrSpaces(commonURLAncestor.getPath()))) { |
| //final AuthenticationRequest currObj = getObj(key); |
| //if ((currObj != null) && passiveValidation(myVcs.getProject(), key, true, currObj.getRealm(), currObj.getKind())) { |
| outdatedRequests.add(key); |
| //} |
| } |
| } |
| log("on state changed "); |
| ApplicationManager.getApplication().invokeLater(new Runnable() { |
| @Override |
| public void run() { |
| for (SVNURL key : outdatedRequests) { |
| removeLazyNotificationByKey(key); |
| } |
| } |
| }, ModalityState.NON_MODAL); |
| } |
| |
| @Override |
| public boolean ensureNotify(AuthenticationRequest obj) { |
| final SVNURL key = getKey(obj); |
| myCopiesPassiveResults.remove(key); |
| /*VcsBalloonProblemNotifier.showOverChangesView(myVcs.getProject(), "You are not authenticated to '" + obj.getRealm() + "'." + |
| "To login, see pending notifications.", MessageType.ERROR);*/ |
| return super.ensureNotify(obj); |
| } |
| |
| @Override |
| protected boolean onFirstNotification(AuthenticationRequest obj) { |
| if (ProgressManager.getInstance().hasProgressIndicator()) { |
| return ask(obj, null); // TODO |
| } else { |
| return false; |
| } |
| } |
| |
| @NotNull |
| @Override |
| public SVNURL getKey(final AuthenticationRequest obj) { |
| // !!! wc's URL |
| return obj.getWcUrl(); |
| } |
| |
| @Nullable |
| public SVNURL getWcUrl(final AuthenticationRequest obj) { |
| if (obj.isOutsideCopies()) return null; |
| if (obj.getWcUrl() != null) return obj.getWcUrl(); |
| |
| final WorkingCopy copy = myRootsToWorkingCopies.getMatchingCopy(obj.getUrl()); |
| if (copy != null) { |
| obj.setOutsideCopies(false); |
| obj.setWcUrl(copy.getUrl()); |
| } else { |
| obj.setOutsideCopies(true); |
| } |
| return copy == null ? null : copy.getUrl(); |
| } |
| |
| /** |
| * Bases on presence of notifications! |
| */ |
| public ThreeState isAuthenticatedFor(@NotNull VirtualFile vf, @Nullable ClientFactory factory) { |
| final WorkingCopy wcCopy = myRootsToWorkingCopies.getWcRoot(vf); |
| if (wcCopy == null) return ThreeState.UNSURE; |
| |
| // check there's no cancellation yet |
| final boolean haveCancellation = getStateFor(wcCopy.getUrl()); |
| if (haveCancellation) return ThreeState.NO; |
| |
| final Boolean keptResult = myCopiesPassiveResults.get(wcCopy.getUrl()); |
| if (Boolean.TRUE.equals(keptResult)) return ThreeState.YES; |
| if (Boolean.FALSE.equals(keptResult)) return ThreeState.NO; |
| |
| // check have credentials |
| final boolean calculatedResult = |
| factory == null ? passiveValidation(myVcs.getProject(), wcCopy.getUrl()) : passiveValidation(factory, wcCopy.getUrl()); |
| myCopiesPassiveResults.put(wcCopy.getUrl(), calculatedResult); |
| return calculatedResult ? ThreeState.YES : ThreeState.NO; |
| } |
| |
| private static boolean passiveValidation(@NotNull ClientFactory factory, @NotNull SVNURL url) { |
| Info info = null; |
| |
| try { |
| info = factory.create(InfoClient.class, false).doInfo(url, SVNRevision.UNDEFINED, SVNRevision.UNDEFINED); |
| } |
| catch (SvnBindException ignore) { |
| } |
| |
| return info != null; |
| } |
| |
| @NotNull |
| @Override |
| protected String getNotificationContent(AuthenticationRequest obj) { |
| return "<a href=\"\">Click to fix.</a> Not logged In to Subversion '" + obj.getRealm() + "' (" + obj.getUrl().toDecodedString() + ")"; |
| } |
| |
| public static class AuthenticationRequest { |
| private final Project myProject; |
| private final String myKind; |
| private final SVNURL myUrl; |
| private final String myRealm; |
| |
| private SVNURL myWcUrl; |
| private boolean myOutsideCopies; |
| private boolean myForceSaving; |
| |
| public AuthenticationRequest(Project project, String kind, SVNURL url, String realm) { |
| myProject = project; |
| myKind = kind; |
| myUrl = url; |
| myRealm = realm; |
| } |
| |
| public boolean isForceSaving() { |
| return myForceSaving; |
| } |
| |
| public void setForceSaving(boolean forceSaving) { |
| myForceSaving = forceSaving; |
| } |
| |
| public boolean isOutsideCopies() { |
| return myOutsideCopies; |
| } |
| |
| public void setOutsideCopies(boolean outsideCopies) { |
| myOutsideCopies = outsideCopies; |
| } |
| |
| public SVNURL getWcUrl() { |
| return myWcUrl; |
| } |
| |
| public void setWcUrl(SVNURL wcUrl) { |
| myWcUrl = wcUrl; |
| } |
| |
| public String getKind() { |
| return myKind; |
| } |
| |
| public SVNURL getUrl() { |
| return myUrl; |
| } |
| |
| public String getRealm() { |
| return myRealm; |
| } |
| } |
| |
| static void log(final Throwable t) { |
| LOG.debug(t); |
| } |
| |
| static void log(final String s) { |
| LOG.debug(s); |
| } |
| |
| public static boolean passiveValidation(final Project project, final SVNURL url) { |
| final SvnConfiguration configuration = SvnConfiguration.getInstance(project); |
| final SvnAuthenticationManager passiveManager = configuration.getPassiveAuthenticationManager(project); |
| return validationImpl(project, url, configuration, passiveManager, false, null, null, false); |
| } |
| |
| public static boolean interactiveValidation(final Project project, final SVNURL url, final String realm, final String kind) { |
| final SvnConfiguration configuration = SvnConfiguration.getInstance(project); |
| final SvnAuthenticationManager passiveManager = configuration.getInteractiveManager(SvnVcs.getInstance(project)); |
| return validationImpl(project, url, configuration, passiveManager, true, realm, kind, true); |
| } |
| |
| private static boolean validationImpl(final Project project, final SVNURL url, |
| final SvnConfiguration configuration, final SvnAuthenticationManager manager, |
| final boolean checkWrite, |
| final String realm, |
| final String kind, boolean interactive) { |
| // we should also NOT show proxy credentials dialog if at least fixed proxy was used, so |
| Proxy proxyToRelease = null; |
| if (! interactive && configuration.isIsUseDefaultProxy()) { |
| final HttpConfigurable instance = HttpConfigurable.getInstance(); |
| if (instance.USE_HTTP_PROXY && instance.PROXY_AUTHENTICATION && (StringUtil.isEmptyOrSpaces(instance.PROXY_LOGIN) || |
| StringUtil.isEmptyOrSpaces(instance.getPlainProxyPassword()))) { |
| return false; |
| } |
| if (instance.USE_PROXY_PAC) { |
| final List<Proxy> select; |
| try { |
| select = CommonProxy.getInstance().select(new URI(url.toString())); |
| } |
| catch (URISyntaxException e) { |
| LOG.info("wrong URL: " + url.toString()); |
| return false; |
| } |
| if (select != null && ! select.isEmpty()) { |
| for (Proxy proxy : select) { |
| if (HttpConfigurable.isRealProxy(proxy) && Proxy.Type.HTTP.equals(proxy.type())) { |
| final InetSocketAddress address = (InetSocketAddress)proxy.address(); |
| final PasswordAuthentication password = |
| HttpConfigurable.getInstance().getGenericPassword(address.getHostName(), address.getPort()); |
| if (password == null) { |
| CommonProxy.getInstance().noAuthentication("http", address.getHostName(), address.getPort()); |
| proxyToRelease = proxy; |
| } |
| } |
| } |
| } |
| } |
| } |
| SvnInteractiveAuthenticationProvider.clearCallState(); |
| try { |
| // start svnkit authentication cycle |
| SvnVcs.getInstance(project).getSvnKitManager().createWCClient(manager).doInfo(url, SVNRevision.UNDEFINED, SVNRevision.HEAD); |
| //SvnVcs.getInstance(project).getInfo(url, SVNRevision.HEAD, manager); |
| } catch (SVNAuthenticationException e) { |
| log(e); |
| return false; |
| } catch (SVNCancelException e) { |
| log(e); // auth canceled |
| return false; |
| } catch (final SVNException e) { |
| if (e.getErrorMessage().getErrorCode().isAuthentication()) { |
| log(e); |
| return false; |
| } |
| LOG.info("some other exc", e); |
| if (interactive) { |
| showAuthenticationFailedWithHotFixes(project, configuration, e); |
| } |
| return false; /// !!!! any exception means user should be notified that authorization failed |
| } finally { |
| if (! interactive && configuration.isIsUseDefaultProxy() && proxyToRelease != null) { |
| final InetSocketAddress address = (InetSocketAddress)proxyToRelease.address(); |
| CommonProxy.getInstance().noAuthentication("http", address.getHostName(), address.getPort()); |
| } |
| } |
| |
| if (! checkWrite) { |
| return true; |
| } |
| /*if (passive) { |
| return SvnInteractiveAuthenticationProvider.wasCalled(); |
| }*/ |
| |
| if (SvnInteractiveAuthenticationProvider.wasCalled() && SvnInteractiveAuthenticationProvider.wasCancelled()) return false; |
| if (SvnInteractiveAuthenticationProvider.wasCalled()) return true; |
| |
| final SvnVcs svnVcs = SvnVcs.getInstance(project); |
| |
| final SvnInteractiveAuthenticationProvider provider = new SvnInteractiveAuthenticationProvider(svnVcs, manager); |
| final SVNAuthentication svnAuthentication = provider.requestClientAuthentication(kind, url, realm, null, null, true); |
| if (svnAuthentication != null) { |
| configuration.acknowledge(kind, realm, svnAuthentication); |
| try { |
| configuration.getAuthenticationManager(svnVcs).acknowledgeAuthentication(true, kind, realm, null, svnAuthentication); |
| } |
| catch (SVNException e) { |
| LOG.info(e); |
| } |
| return true; |
| } |
| return false; |
| } |
| |
| private static void showAuthenticationFailedWithHotFixes(final Project project, |
| final SvnConfiguration configuration, |
| final SVNException e) { |
| ApplicationManager.getApplication().invokeLater(new Runnable() { |
| @Override |
| public void run() { |
| VcsBalloonProblemNotifier.showOverChangesView(project, "Authentication failed: " + e.getMessage(), MessageType.ERROR, |
| new NamedRunnable( |
| SvnBundle.message("confirmation.title.clear.authentication.cache")) { |
| @Override |
| public void run() { |
| clearAuthenticationCache(project, null, configuration |
| .getConfigurationDirectory()); |
| } |
| }, |
| new NamedRunnable(SvnBundle.message("action.title.select.configuration.directory")) { |
| @Override |
| public void run() { |
| SvnConfigurable |
| .selectConfigurationDirectory(configuration.getConfigurationDirectory(), |
| new Consumer<String>() { |
| @Override |
| public void consume(String s) { |
| configuration |
| .setConfigurationDirParameters(false, s); |
| } |
| }, project, null); |
| } |
| } |
| ); |
| } |
| }, ModalityState.NON_MODAL, project.getDisposed()); |
| } |
| |
| public static void clearAuthenticationCache(@NotNull final Project project, final Component component, final String configDirPath) { |
| if (configDirPath != null) { |
| int result; |
| if (component == null) { |
| result = Messages.showYesNoDialog(project, SvnBundle.message("confirmation.text.delete.stored.authentication.information"), |
| SvnBundle.message("confirmation.title.clear.authentication.cache"), |
| Messages.getWarningIcon()); |
| } else { |
| result = Messages.showYesNoDialog(component, SvnBundle.message("confirmation.text.delete.stored.authentication.information"), |
| SvnBundle.message("confirmation.title.clear.authentication.cache"), |
| Messages.getWarningIcon()); |
| } |
| if (result == Messages.YES) { |
| SvnConfiguration.RUNTIME_AUTH_CACHE.clear(); |
| clearAuthenticationDirectory(SvnConfiguration.getInstance(project)); |
| } |
| } |
| } |
| |
| public static void clearAuthenticationDirectory(@NotNull SvnConfiguration configuration) { |
| final File authDir = new File(configuration.getConfigurationDirectory(), "auth"); |
| if (authDir.exists()) { |
| final Runnable process = new Runnable() { |
| @Override |
| public void run() { |
| final ProgressIndicator ind = ProgressManager.getInstance().getProgressIndicator(); |
| if (ind != null) { |
| ind.setIndeterminate(true); |
| ind.setText("Clearing stored credentials in " + authDir.getAbsolutePath()); |
| } |
| final File[] files = authDir.listFiles(new FilenameFilter() { |
| @Override |
| public boolean accept(@NotNull File dir, @NotNull String name) { |
| return ourAuthKinds.contains(name); |
| } |
| }); |
| |
| for (File dir : files) { |
| if (ind != null) { |
| ind.setText("Deleting " + dir.getAbsolutePath()); |
| } |
| FileUtil.delete(dir); |
| } |
| } |
| }; |
| final Application application = ApplicationManager.getApplication(); |
| if (application.isUnitTestMode() || !application.isDispatchThread()) { |
| process.run(); |
| } |
| else { |
| ProgressManager.getInstance() |
| .runProcessWithProgressSynchronously(process, "button.text.clear.authentication.cache", false, configuration.getProject()); |
| } |
| } |
| } |
| } |