diff --git a/platform/util/src/com/intellij/util/io/URLUtil.java b/platform/util/src/com/intellij/util/io/URLUtil.java
index 51f01d4c939d..363bf554ad8f 100644
--- a/platform/util/src/com/intellij/util/io/URLUtil.java
+++ b/platform/util/src/com/intellij/util/io/URLUtil.java
@@ -16,6 +16,7 @@
package com.intellij.util.io;
+import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.io.FileUtil;
import gnu.trove.TIntArrayList;
import org.jetbrains.annotations.NonNls;
@@ -147,4 +148,21 @@ public class URLUtil {
public static boolean containsScheme(String url) {
return url.contains("://");
}
+
+ /**
+ * Splits the url into 2 parts: the scheme ("http://" for instance) and the rest of the URL.
+ * The scheme can be null.
+ */
+ @NotNull
+ public static Pair splitScheme(@NotNull String url) {
+ final String schemeSeparator = "://";
+ int ind = url.indexOf(schemeSeparator);
+ if (ind >= 0) {
+ String scheme = url.substring(0, ind + schemeSeparator.length());
+ return Pair.create(scheme, url.substring(ind));
+ }
+ else {
+ return Pair.create(null, url);
+ }
+ }
}
diff --git a/plugins/git4idea/rt/src/org/jetbrains/git4idea/http/GitAskPassApp.java b/plugins/git4idea/rt/src/org/jetbrains/git4idea/http/GitAskPassApp.java
new file mode 100644
index 000000000000..38d8ba855a0c
--- /dev/null
+++ b/plugins/git4idea/rt/src/org/jetbrains/git4idea/http/GitAskPassApp.java
@@ -0,0 +1,100 @@
+/*
+ * 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.git4idea.http;
+
+import com.intellij.openapi.util.Pair;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * This is a program that would be called by Git when an HTTP connection is needed, that requires authorization,
+ * and if {@code GIT_ASKPASS} variable is set to the script that invokes this program.
+ * The program is called separately for each authorization aspect.
+ * I. e. if no username is specified, then it is started and queried for the username, and then started once again for the password.
+ * Query format is the following:
+ *
+ * Username for 'https://bitbucket.org':
+ * Password for 'https://bitbucket.org':
+ * Password for 'https://username@bitbucket.org':
+ *
+ *
+ * Git expects the reply from the program's standard output.
+ *
+ * @author Kirill Likhodedov
+ */
+public class GitAskPassApp {
+
+ // STDOUT is used to provide credentials to Git process; STDERR is used to print error message to the main IDEA command line.
+ @SuppressWarnings("UseOfSystemOutOrSystemErr")
+ public static void main(String[] args) {
+ try {
+ if (args.length < 1) {
+ throw new IllegalArgumentException("No arguments specified!");
+ }
+
+ Pair arguments = parseArguments(args[0]);
+ boolean usernameNeeded = arguments.getFirst();
+ String url = arguments.getSecond();
+
+ int handler = Integer.parseInt(getNotNull(GitAskPassXmlRpcHandler.GIT_ASK_PASS_HANDLER_ENV));
+ int xmlRpcPort = Integer.parseInt(getNotNull(GitAskPassXmlRpcHandler.GIT_ASK_PASS_PORT_ENV));
+ GitAskPassXmlRpcClient xmlRpcClient = new GitAskPassXmlRpcClient(xmlRpcPort);
+
+ if (usernameNeeded) {
+ String username = xmlRpcClient.askUsername(handler, url);
+ System.out.println(username);
+ }
+ else {
+ String pass = xmlRpcClient.askPassword(handler, url);
+ System.out.println(pass);
+ }
+ }
+ catch (Throwable t) {
+ System.err.println(t.getMessage());
+ t.printStackTrace(System.err);
+ }
+ }
+
+ @NotNull
+ private static String getNotNull(@NotNull String env) {
+ String handlerValue = System.getenv(env);
+ if (handlerValue == null) {
+ throw new IllegalStateException(env + " environment variable is not defined!");
+ }
+ return handlerValue;
+ }
+
+ @NotNull
+ private static Pair parseArguments(@NotNull String arg) {
+ String[] split = arg.split(" ");
+ if (split.length < 3) {
+ throw new IllegalArgumentException("Unknown argument format: " + arg);
+ }
+ boolean username = split[0].equalsIgnoreCase("username");
+ String url = split[2];
+ // un-quote and remove the trailing colon
+ if (url.startsWith("'")) {
+ url = url.substring(1);
+ }
+ if (url.endsWith(":")) {
+ url = url.substring(0, url.length() - 1);
+ }
+ if (url.endsWith("'")) {
+ url = url.substring(0, url.length() - 1);
+ }
+ return Pair.create(username, url);
+ }
+
+}
diff --git a/plugins/git4idea/rt/src/org/jetbrains/git4idea/http/GitAskPassXmlRpcClient.java b/plugins/git4idea/rt/src/org/jetbrains/git4idea/http/GitAskPassXmlRpcClient.java
new file mode 100644
index 000000000000..5d3f8681f099
--- /dev/null
+++ b/plugins/git4idea/rt/src/org/jetbrains/git4idea/http/GitAskPassXmlRpcClient.java
@@ -0,0 +1,80 @@
+/*
+ * 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.git4idea.http;
+
+import org.apache.xmlrpc.XmlRpcClientLite;
+import org.apache.xmlrpc.XmlRpcException;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.util.Vector;
+
+/**
+ * Calls {@link GitAskPassXmlRpcHandler} methods via XML RPC.
+ *
+ * @author Kirill Likhodedov
+ */
+class GitAskPassXmlRpcClient {
+
+ @NotNull private final XmlRpcClientLite myClient;
+
+ GitAskPassXmlRpcClient(int port) throws MalformedURLException {
+ myClient = new XmlRpcClientLite("127.0.0.1", port);
+ }
+
+ // Obsolete collection usage because of the XmlRpcClientLite API
+ @SuppressWarnings({"UseOfObsoleteCollectionType", "unchecked"})
+ String askUsername(int handler, @NotNull String url) {
+ Vector parameters = new Vector();
+ parameters.add(handler);
+ parameters.add(url);
+
+ try {
+ return (String)myClient.execute(methodName("askUsername"), parameters);
+ }
+ catch (XmlRpcException e) {
+ throw new RuntimeException("Invocation failed " + e.getMessage(), e);
+ }
+ catch (IOException e) {
+ throw new RuntimeException("Invocation failed " + e.getMessage(), e);
+ }
+ }
+
+ // Obsolete collection usage because of the XmlRpcClientLite API
+ @SuppressWarnings({"UseOfObsoleteCollectionType", "unchecked"})
+ String askPassword(int handler, @NotNull String url) {
+ Vector parameters = new Vector();
+ parameters.add(handler);
+ parameters.add(url);
+
+ try {
+ return (String)myClient.execute(methodName("askPassword"), parameters);
+ }
+ catch (XmlRpcException e) {
+ throw new RuntimeException("Invocation failed " + e.getMessage(), e);
+ }
+ catch (IOException e) {
+ throw new RuntimeException("Invocation failed " + e.getMessage(), e);
+ }
+ }
+
+ @NotNull
+ private static String methodName(@NotNull String method) {
+ return GitAskPassXmlRpcHandler.HANDLER_NAME + "." + method;
+ }
+
+}
diff --git a/plugins/git4idea/rt/src/org/jetbrains/git4idea/http/GitAskPassXmlRpcHandler.java b/plugins/git4idea/rt/src/org/jetbrains/git4idea/http/GitAskPassXmlRpcHandler.java
new file mode 100644
index 000000000000..36cc365c1d80
--- /dev/null
+++ b/plugins/git4idea/rt/src/org/jetbrains/git4idea/http/GitAskPassXmlRpcHandler.java
@@ -0,0 +1,55 @@
+/*
+ * 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.git4idea.http;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * This handler is called via XML RPC from {@link GitAskPassApp} when Git requests user credentials.
+ *
+ * @author Kirill Likhodedov
+ */
+public interface GitAskPassXmlRpcHandler {
+
+ String GIT_ASK_PASS_ENV = "GIT_ASKPASS";
+ String GIT_ASK_PASS_HANDLER_ENV = "GIT_ASKPASS_HANDLER";
+ String GIT_ASK_PASS_PORT_ENV = "GIT_ASKPASS_PORT";
+ String HANDLER_NAME = GitAskPassXmlRpcHandler.class.getName();
+
+ /**
+ * Get the username from the user to access the given URL.
+ * @param handler XML RPC handler number.
+ * @param url URL which Git tries to access.
+ * @return The Username which should be used for the URL.
+ */
+ // UnusedDeclaration suppressed: the method is used via XML RPC
+ @SuppressWarnings("UnusedDeclaration")
+ @NotNull
+ String askUsername(int handler, @NotNull String url);
+
+ /**
+ * Get the password from the user to access the given URL.
+ * It is assumed that the username either is specified in the URL (http://username@host.com), or has been asked earlier.
+ * @param handler XML RPC handler number.
+ * @param url URL which Git tries to access.
+ * @return The password which should be used for the URL.
+ */
+ // UnusedDeclaration suppressed: the method is used via XML RPC
+ @SuppressWarnings("UnusedDeclaration")
+ @NotNull
+ String askPassword(int handler, @NotNull String url);
+
+}
diff --git a/plugins/git4idea/src/META-INF/plugin.xml b/plugins/git4idea/src/META-INF/plugin.xml
index 85242d0449cd..f6f784cea289 100644
--- a/plugins/git4idea/src/META-INF/plugin.xml
+++ b/plugins/git4idea/src/META-INF/plugin.xml
@@ -151,6 +151,8 @@
serviceImplementation="git4idea.config.GitVcsApplicationSettings"/>
+
{
+
+ @NotNull
+ @Override
+ protected String getScriptTempFilePrefix() {
+ return "git-askpass-";
+ }
+
+ @NotNull
+ @Override
+ protected Class> getScriptMainClass() {
+ return GitAskPassApp.class;
+ }
+
+ @Override
+ protected void customizeScriptGenerator(@NotNull ScriptGenerator generator) {
+ }
+
+ @NotNull
+ @Override
+ protected String getRpcHandlerName() {
+ return GitAskPassXmlRpcHandler.HANDLER_NAME;
+ }
+
+ @NotNull
+ @Override
+ protected Object createRpcRequestHandlerDelegate() {
+ return new InternalRequestHandlerDelegate();
+ }
+
+ /**
+ * Internal handler implementation class, it is made public to be accessible via XML RPC.
+ */
+ public class InternalRequestHandlerDelegate implements GitAskPassXmlRpcHandler {
+ @NotNull
+ @Override
+ public String askUsername(int handler, @NotNull String url) {
+ return getHandler(handler).askUsername(url);
+ }
+
+ @NotNull
+ @Override
+ public String askPassword(int handler, @NotNull String url) {
+ return getHandler(handler).askPassword(url);
+ }
+ }
+
+}
diff --git a/plugins/git4idea/src/git4idea/commands/GitHttpAuthenticator.java b/plugins/git4idea/src/git4idea/commands/GitHttpAuthenticator.java
new file mode 100644
index 000000000000..cd292ca82b52
--- /dev/null
+++ b/plugins/git4idea/src/git4idea/commands/GitHttpAuthenticator.java
@@ -0,0 +1,206 @@
+/*
+ * 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 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.Pair;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.util.AuthData;
+import com.intellij.util.io.URLUtil;
+import com.intellij.vcsUtil.AuthDialog;
+import git4idea.jgit.GitHttpAuthDataProvider;
+import git4idea.remote.GitRememberedInputs;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * 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.
+ * If user cancels the dialog, empty string is returned.
+ * 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.
+ * New instance of the GitAskPassGuiHandler should be created for each session, i. e. for each remote operation call.
+ *
+ * @author Kirill Likhodedov
+ */
+class GitHttpAuthenticator {
+
+ private static final Logger LOG = Logger.getInstance(GitHttpAuthenticator.class);
+ private static final Class PASS_REQUESTER = GitHttpAuthenticator.class;
+
+ @NotNull private final Project myProject;
+ @Nullable private final ModalityState myModalityState;
+ @NotNull private final String myTitle;
+
+ @Nullable private String myPassword;
+ @Nullable private String myPasswordKey;
+ @Nullable private String myUrl;
+ @Nullable private String myLogin;
+ private boolean myRememberOnDisk;
+
+ GitHttpAuthenticator(@NotNull Project project, @Nullable ModalityState modalityState, @NotNull GitCommand command) {
+ myProject = project;
+ myModalityState = modalityState;
+ myTitle = "Git " + StringUtil.capitalize(command.name());
+ }
+
+ @NotNull
+ String askPassword(@NotNull String url) {
+ if (myPassword != null) { // already asked in askUsername
+ return myPassword;
+ }
+ String prompt = "Enter the password for " + url;
+ String key = adjustHttpUrl(url);
+ myPasswordKey = key;
+ return PasswordSafePromptDialog.askPassword(myProject, myModalityState, myTitle, prompt, PASS_REQUESTER, key, false, null);
+ }
+
+ @NotNull
+ String askUsername(@NotNull String url) {
+ String key = adjustHttpUrl(url);
+ AuthData authData = getSavedAuthData(myProject, key);
+ String login = null;
+ String password = null;
+ if (authData != null) {
+ login = authData.getLogin();
+ password = authData.getPassword();
+ }
+ if (login != null && password != null) {
+ myPassword = password;
+ return login;
+ }
+
+ final AuthDialog dialog = new AuthDialog(myProject, myTitle, "Enter credentials for " + url, login, null, true);
+ ApplicationManager.getApplication().invokeAndWait(new Runnable() {
+ @Override
+ public void run() {
+ dialog.show();
+ }
+ }, myModalityState == null ? ModalityState.defaultModalityState() : myModalityState);
+
+ if (!dialog.isOK()) {
+ return "";
+ }
+
+ // remember values to store in the database afterwards, if authentication succeeds
+ myPassword = dialog.getPassword();
+ myLogin = dialog.getUsername();
+ myUrl = key;
+ myRememberOnDisk = dialog.isRememberPassword();
+ myPasswordKey = makeKey(myUrl, myLogin);
+
+ return myLogin;
+ }
+
+ 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 (myRememberOnDisk) {
+ passwordSafe.getMasterKeyProvider().storePassword(myProject, PASS_REQUESTER, myPasswordKey, myPassword);
+ }
+ }
+ catch (PasswordSafeException e) {
+ LOG.error("Couldn't remember password for " + myPasswordKey, e);
+ }
+ }
+ }
+
+ void forgetPassword() {
+ if (myPasswordKey != null) {
+ try {
+ PasswordSafe.getInstance().removePassword(myProject, PASS_REQUESTER, myPasswordKey);
+ }
+ catch (PasswordSafeException e) {
+ LOG.info("Couldn't forget the password for " + myPasswordKey);
+ }
+ }
+ }
+
+ /**
+ * 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;
+ }
+
+ @Nullable
+ private static AuthData getSavedAuthData(@NotNull Project project, @NotNull String url) {
+ String userName = GitRememberedInputs.getInstance().getUserNameForUrl(url);
+ if (userName == null) {
+ return trySavedAuthDataFromProviders(url);
+ }
+ String key = makeKey(url, userName);
+ final PasswordSafe passwordSafe = PasswordSafe.getInstance();
+ try {
+ String password = passwordSafe.getPassword(project, PASS_REQUESTER, key);
+ if (password != null) {
+ return new AuthData(userName, password);
+ }
+ return null;
+ }
+ catch (PasswordSafeException e) {
+ LOG.info("Couldn't get the password for key [" + key + "]", e);
+ return null;
+ }
+ }
+
+ @Nullable
+ private static AuthData trySavedAuthDataFromProviders(@NotNull String url) {
+ GitHttpAuthDataProvider[] extensions = GitHttpAuthDataProvider.EP_NAME.getExtensions();
+ for (GitHttpAuthDataProvider provider : extensions) {
+ AuthData authData = provider.getAuthData(url);
+ if (authData != null) {
+ return authData;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 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, @NotNull String login) {
+ Pair pair = URLUtil.splitScheme(url);
+ String scheme = pair.getFirst();
+ if (scheme != null) {
+ return scheme + login + "@" + pair.getSecond();
+ }
+ return login + "@" + url;
+ }
+
+}