| /* |
| * 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 git4idea.commands; |
| |
| import com.intellij.ide.passwordSafe.PasswordSafe; |
| import com.intellij.ide.passwordSafe.PasswordSafeException; |
| import com.intellij.ide.passwordSafe.impl.PasswordSafeImpl; |
| import com.intellij.ide.passwordSafe.ui.PasswordSafePromptDialog; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.application.ModalityState; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.util.Couple; |
| import com.intellij.openapi.util.Pair; |
| import com.intellij.openapi.util.Ref; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.util.AuthData; |
| import com.intellij.util.UriUtil; |
| import com.intellij.util.containers.ContainerUtil; |
| import com.intellij.util.io.URLUtil; |
| import com.intellij.vcsUtil.AuthDialog; |
| import git4idea.remote.GitHttpAuthDataProvider; |
| import git4idea.remote.GitRememberedInputs; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.util.Arrays; |
| import java.util.List; |
| |
| /** |
| * <p>Handles "ask username" and "ask password" requests from Git: |
| * shows authentication dialog in the GUI, waits for user input and returns the credentials supplied by the user.</p> |
| * <p>If user cancels the dialog, empty string is returned.</p> |
| * <p>If no username is specified in the URL, Git queries for the username and for the password consecutively. |
| * In this case to avoid showing dialogs twice, the component asks for both credentials at once, |
| * and remembers the password to provide it to the Git process during the next request without requiring user interaction.</p> |
| * <p>New instance of the GitAskPassGuiHandler should be created for each session, i. e. for each remote operation call.</p> |
| * |
| * @author Kirill Likhodedov |
| */ |
| class GitHttpGuiAuthenticator implements GitHttpAuthenticator { |
| |
| private static final Logger LOG = Logger.getInstance(GitHttpGuiAuthenticator.class); |
| private static final Class<GitHttpAuthenticator> PASS_REQUESTER = GitHttpAuthenticator.class; |
| |
| @NotNull private final Project myProject; |
| @NotNull private final String myTitle; |
| @NotNull private final String myUrlFromCommand; |
| |
| @Nullable private String myPassword; |
| @Nullable private String myPasswordKey; |
| @Nullable private String myUrl; |
| @Nullable private String myLogin; |
| private boolean mySaveOnDisk; |
| @Nullable private GitHttpAuthDataProvider myDataProvider; |
| private boolean myWasCancelled; |
| |
| GitHttpGuiAuthenticator(@NotNull Project project, @NotNull GitCommand command, @NotNull String url) { |
| myProject = project; |
| myTitle = "Git " + StringUtil.capitalize(command.name()); |
| myUrlFromCommand = url; |
| } |
| |
| @Override |
| @NotNull |
| public String askPassword(@NotNull String url) { |
| if (myPassword != null) { // already asked in askUsername |
| return myPassword; |
| } |
| if (myWasCancelled) { // already pressed cancel in askUsername |
| return ""; |
| } |
| url = adjustUrl(url); |
| Pair<GitHttpAuthDataProvider, AuthData> authData = findBestAuthData(url); |
| if (authData != null && authData.second.getPassword() != null) { |
| String password = authData.second.getPassword(); |
| myDataProvider = authData.first; |
| myPassword = password; |
| return password; |
| } |
| |
| String prompt = "Enter the password for " + url; |
| myPasswordKey = url; |
| String password = PasswordSafePromptDialog.askPassword(myProject, myTitle, prompt, PASS_REQUESTER, url, false, null); |
| if (password == null) { |
| myWasCancelled = true; |
| return ""; |
| } |
| // Password is stored in the safe in PasswordSafePromptDialog.askPassword, |
| // but it is not the right behavior (incorrect password is stored too because of that) and should be fixed separately. |
| // We store it here manually, to let it work after that behavior is fixed. |
| myPassword = password; |
| myDataProvider = new GitDefaultHttpAuthDataProvider(); // workaround: askPassword remembers the password even it is not correct |
| return password; |
| } |
| |
| @Override |
| @NotNull |
| public String askUsername(@NotNull String url) { |
| url = adjustUrl(url); |
| Pair<GitHttpAuthDataProvider, AuthData> authData = findBestAuthData(url); |
| String login = null; |
| String password = null; |
| if (authData != null) { |
| login = authData.second.getLogin(); |
| password = authData.second.getPassword(); |
| myDataProvider = authData.first; |
| } |
| if (login != null && password != null) { |
| myPassword = password; |
| return login; |
| } |
| |
| AuthDialog dialog = showAuthDialog(url, login); |
| if (dialog == null || !dialog.isOK()) { |
| myWasCancelled = true; |
| return ""; |
| } |
| |
| // remember values to store in the database afterwards, if authentication succeeds |
| myPassword = dialog.getPassword(); |
| myLogin = dialog.getUsername(); |
| myUrl = url; |
| mySaveOnDisk = dialog.isRememberPassword(); |
| myPasswordKey = makeKey(myUrl, myLogin); |
| |
| return myLogin; |
| } |
| |
| @Nullable |
| private AuthDialog showAuthDialog(final String url, final String login) { |
| final Ref<AuthDialog> dialog = Ref.create(); |
| ApplicationManager.getApplication().invokeAndWait(new Runnable() { |
| @Override |
| public void run() { |
| dialog.set(new AuthDialog(myProject, myTitle, "Enter credentials for " + url, login, null, true)); |
| dialog.get().show(); |
| } |
| }, ModalityState.any()); |
| return dialog.get(); |
| } |
| |
| @Override |
| public void saveAuthData() { |
| // save login and url |
| if (myUrl != null && myLogin != null) { |
| GitRememberedInputs.getInstance().addUrl(myUrl, myLogin); |
| } |
| |
| // save password |
| if (myPasswordKey != null && myPassword != null) { |
| PasswordSafeImpl passwordSafe = (PasswordSafeImpl)PasswordSafe.getInstance(); |
| try { |
| passwordSafe.getMemoryProvider().storePassword(myProject, PASS_REQUESTER, myPasswordKey, myPassword); |
| if (mySaveOnDisk) { |
| passwordSafe.getMasterKeyProvider().storePassword(myProject, PASS_REQUESTER, myPasswordKey, myPassword); |
| } |
| } |
| catch (PasswordSafeException e) { |
| LOG.error("Couldn't remember password for " + myPasswordKey, e); |
| } |
| } |
| } |
| |
| @Override |
| public void forgetPassword() { |
| if (myDataProvider != null) { |
| myDataProvider.forgetPassword(adjustUrl(myUrl)); |
| } |
| } |
| |
| @Override |
| public boolean wasCancelled() { |
| return myWasCancelled; |
| } |
| |
| @NotNull |
| private String adjustUrl(@Nullable String url) { |
| if (StringUtil.isEmptyOrSpaces(url)) { |
| // if Git doesn't specify the URL in the username/password query, we use the url from the Git command |
| // We only take the host, to avoid entering the same password for different repositories on the same host. |
| return adjustHttpUrl(getHost(myUrlFromCommand)); |
| } |
| return adjustHttpUrl(url); |
| } |
| |
| @NotNull |
| private static String getHost(@NotNull String url) { |
| Couple<String> split = UriUtil.splitScheme(url); |
| String scheme = split.getFirst(); |
| String urlItself = split.getSecond(); |
| int pathStart = urlItself.indexOf("/"); |
| return scheme + URLUtil.SCHEME_SEPARATOR + urlItself.substring(0, pathStart); |
| } |
| |
| /** |
| * If the url scheme is HTTPS, store it as HTTP in the database, not to make user enter and remember same credentials twice. |
| */ |
| @NotNull |
| private static String adjustHttpUrl(@NotNull String url) { |
| String prefix = "https"; |
| if (url.startsWith(prefix)) { |
| return "http" + url.substring(prefix.length()); |
| } |
| return url; |
| } |
| |
| // return the first that knows username + password; otherwise return the first that knows just the username |
| @Nullable |
| private Pair<GitHttpAuthDataProvider, AuthData> findBestAuthData(@NotNull String url) { |
| Pair<GitHttpAuthDataProvider, AuthData> candidate = null; |
| for (GitHttpAuthDataProvider provider : getProviders()) { |
| AuthData data = provider.getAuthData(url); |
| if (data != null) { |
| Pair<GitHttpAuthDataProvider, AuthData> pair = Pair.create(provider, data); |
| if (data.getPassword() != null) { |
| return pair; |
| } |
| if (candidate == null) { |
| candidate = pair; |
| } |
| } |
| } |
| return candidate; |
| } |
| |
| @NotNull |
| private List<GitHttpAuthDataProvider> getProviders() { |
| List<GitHttpAuthDataProvider> providers = ContainerUtil.newArrayList(); |
| providers.add(new GitDefaultHttpAuthDataProvider()); |
| providers.addAll(Arrays.asList(GitHttpAuthDataProvider.EP_NAME.getExtensions())); |
| return providers; |
| } |
| |
| /** |
| * Makes the password database key for the URL: inserts the login after the scheme: http://login@url. |
| */ |
| @NotNull |
| private static String makeKey(@NotNull String url, @Nullable String login) { |
| if (login == null) { |
| return url; |
| } |
| Couple<String> pair = UriUtil.splitScheme(url); |
| String scheme = pair.getFirst(); |
| if (StringUtil.isEmpty(scheme)) { |
| return scheme + URLUtil.SCHEME_SEPARATOR + login + "@" + pair.getSecond(); |
| } |
| return login + "@" + url; |
| } |
| |
| public class GitDefaultHttpAuthDataProvider implements GitHttpAuthDataProvider { |
| |
| @Nullable |
| @Override |
| public AuthData getAuthData(@NotNull String url) { |
| String userName = getUsername(url); |
| String key = makeKey(url, userName); |
| final PasswordSafe passwordSafe = PasswordSafe.getInstance(); |
| try { |
| String password = passwordSafe.getPassword(myProject, PASS_REQUESTER, key); |
| return new AuthData(StringUtil.notNullize(userName), password); |
| } |
| catch (PasswordSafeException e) { |
| LOG.info("Couldn't get the password for key [" + key + "]", e); |
| return null; |
| } |
| } |
| |
| @Nullable |
| private String getUsername(@NotNull String url) { |
| return GitRememberedInputs.getInstance().getUserNameForUrl(url); |
| } |
| |
| @Override |
| public void forgetPassword(@NotNull String url) { |
| String key = myPasswordKey != null ? myPasswordKey : makeKey(url, getUsername(url)); |
| try { |
| PasswordSafe.getInstance().removePassword(myProject, PASS_REQUESTER, key); |
| } |
| catch (PasswordSafeException e) { |
| LOG.info("Couldn't forget the password for " + myPasswordKey); |
| } |
| } |
| } |
| |
| } |