2 * Copyright 2000-2009 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package org.jetbrains.git4idea.ssh;
18 import com.intellij.ide.XmlRpcServer;
19 import com.intellij.openapi.components.ApplicationComponent;
20 import com.intellij.openapi.components.ServiceManager;
21 import com.intellij.openapi.diagnostic.Logger;
22 import com.intellij.openapi.util.io.FileUtil;
23 import com.trilead.ssh2.KnownHosts;
24 import git4idea.commands.ScriptGenerator;
25 import git4idea.i18n.GitBundle;
26 import gnu.trove.THashMap;
27 import org.apache.commons.codec.DecoderException;
28 import org.apache.xmlrpc.XmlRpcClientLite;
29 import org.jetbrains.annotations.NonNls;
30 import org.jetbrains.annotations.NotNull;
33 import java.io.IOException;
34 import java.util.Random;
35 import java.util.Vector;
38 * The provider of SSH scripts for the Git
40 public class GitSSHService implements ApplicationComponent {
42 * The string used to indicate missing value
44 private static final String XML_RPC_NULL_STRING = "\u0000";
48 private static final Logger log = Logger.getInstance(GitSSHService.class.getName());
51 * random number generator to use
53 private static final Random RANDOM = new Random();
57 @NonNls private static final String HANDLER_NAME = "Git4ideaSSHHandler";
59 * The prefix of the ssh script name
61 @NonNls private static final String GIT_SSH_PREFIX = "git-ssh-";
63 * If true, the component has been initialized
65 private boolean myInitialized = false;
67 * Path to the generated script
69 private File myScriptPath;
73 private final XmlRpcServer myXmlRpcServer;
77 private final THashMap<Integer, Handler> handlers = new THashMap<Integer, Handler>();
80 * Name of environment variable for SSH handler
82 @NonNls public static final String SSH_HANDLER_ENV = "GIT4IDEA_SSH_HANDLER";
84 * Name of environment variable for SSH executable
86 @NonNls public static final String GIT_SSH_ENV = "GIT_SSH";
90 * A constructor from parameter
92 * @param xmlRpcServer the injected XmlRpc server reference
94 public GitSSHService(final @NotNull XmlRpcServer xmlRpcServer) {
95 myXmlRpcServer = xmlRpcServer;
99 * @return an instance of the server
102 public static GitSSHService getInstance() {
103 final GitSSHService service = ServiceManager.getService(GitSSHService.class);
104 if (service == null) {
105 throw new IllegalStateException("The service " + GitSSHService.class.getName() + " cannot be located");
111 * Get file to the script service
113 * @return path to the script
114 * @throws IOException if script cannot be generated
117 public synchronized File getScriptPath() throws IOException {
118 if (myScriptPath == null || !myScriptPath.exists()) {
119 ScriptGenerator generator = new ScriptGenerator(GIT_SSH_PREFIX, SSHMain.class);
120 generator.addInternal(Integer.toString(myXmlRpcServer.getPortNumber()));
121 generator.addClasses(XmlRpcClientLite.class, DecoderException.class);
122 generator.addClasses(KnownHosts.class, FileUtil.class);
123 generator.addResource(GitBundle.class, "/git4idea/i18n/GitBundle.properties");
124 myScriptPath = generator.generate();
133 public String getComponentName() {
134 return GitSSHService.class.getSimpleName();
140 public void initComponent() {
141 if (!myInitialized) {
142 myXmlRpcServer.addHandler(HANDLER_NAME, new InternalRequestHandler());
143 myInitialized = true;
150 public synchronized void disposeComponent() {
151 myXmlRpcServer.removeHandler(HANDLER_NAME);
152 if (myScriptPath != null) {
153 if (!myScriptPath.delete()) {
154 log.warn("The temporary file " + myScriptPath + " generated by git4idea plugin failed to be removed during disposing.");
161 * Register handler. Note that handlers must be unregistered using {@link #unregisterHandler(int)}.
163 * @param handler a handler to register
164 * @return an identifier to pass to the environment variable
166 public synchronized int registerHandler(@NotNull Handler handler) {
169 int candidate = RANDOM.nextInt();
170 if (candidate == Integer.MIN_VALUE) {
173 candidate = Math.abs(candidate);
174 if (handlers.containsKey(candidate)) {
177 handlers.put(candidate, handler);
183 * Get handler for the key
185 * @param key the key to use
186 * @return the registered handler
189 private synchronized Handler getHandler(int key) {
190 Handler rc = handlers.get(key);
192 throw new IllegalStateException("No handler for the key " + key);
198 * Unregister handler by the key
200 * @param key the key to unregister
202 public synchronized void unregisterHandler(int key) {
203 if (handlers.remove(key) == null) {
204 throw new IllegalArgumentException("The handler " + key + " is not registered");
210 * Handler interface to use by the client code
212 public interface Handler {
216 * @param hostname a host name
217 * @param port a port number
218 * @param serverHostKeyAlgorithm an algorithm
219 * @param serverHostKey a key
220 * @param isNew a isNew key
221 * @return true if the key is valid
223 boolean verifyServerHostKey(final String hostname,
225 final String serverHostKeyAlgorithm,
226 final String serverHostKey,
227 final boolean isNew);
232 * @param username a user name
233 * @param keyPath a key path
234 * @param lastError the last error for the handler
235 * @return a passphrase or null if dialog was cancelled.
237 String askPassphrase(final String username, final String keyPath, final String lastError);
240 * Reply to challenge in keyboard-interactive scenario
242 * @param username a user name
243 * @param name a name of challenge
244 * @param instruction a instructions
245 * @param numPrompts number of prompts
246 * @param prompt prompts
247 * @param echo true if the reply for corresponding prompt should be echoed
248 * @param lastError the last error
249 * @return replies to the challenges
251 @SuppressWarnings({"UseOfObsoleteCollectionType"})
252 Vector<String> replyToChallenge(final String username,
254 final String instruction,
255 final int numPrompts,
256 final Vector<String> prompt,
257 final Vector<Boolean> echo,
258 final String lastError);
263 * @param username a user name
264 * @param lastError the previous error
265 * @return a password or null if dialog was cancelled.
267 String askPassword(final String username, final String lastError);
272 * Internal handler implementation class, do not use it.
274 public class InternalRequestHandler implements GitSSHHandler {
278 public boolean verifyServerHostKey(final int handler,
279 final String hostname,
281 final String serverHostKeyAlgorithm,
282 final String serverHostKey,
283 final boolean isNew) {
284 return getHandler(handler).verifyServerHostKey(hostname, port, serverHostKeyAlgorithm, serverHostKey, isNew);
290 public String askPassphrase(final int handler, final String username, final String keyPath, final String lastError) {
291 return adjustNull(getHandler(handler).askPassphrase(username, keyPath, lastError));
297 @SuppressWarnings({"UseOfObsoleteCollectionType"})
298 public Vector<String> replyToChallenge(final int handlerNo,
299 final String username,
301 final String instruction,
302 final int numPrompts,
303 final Vector<String> prompt,
304 final Vector<Boolean> echo,
305 final String lastError) {
306 return adjustNull(getHandler(handlerNo).replyToChallenge(username, name, instruction, numPrompts, prompt, echo, lastError));
312 public String askPassword(final int handlerNo, final String username, final String lastError) {
313 return adjustNull(getHandler(handlerNo).askPassword(username, lastError));
317 * Adjust null value (by converting to {@link GitSSHService#XML_RPC_NULL_STRING})
319 * @param s a value to adjust
320 * @return a string if non-null or {@link GitSSHService#XML_RPC_NULL_STRING} if s == null
322 private String adjustNull(final String s) {
323 return s == null ? XML_RPC_NULL_STRING : s;
327 * Adjust null value (returns empty array)
329 * @param s if null return empty array
330 * @return s if not null, empty array otherwise
332 @SuppressWarnings({"UseOfObsoleteCollectionType"})
333 private <T> Vector<T> adjustNull(final Vector<T> s) {
334 return s == null ? new Vector<T>() : s;