3522b2fc96c293db0163225042daf86856200ffb
[idea/community.git] / plugins / git4idea / src / org / jetbrains / git4idea / ssh / GitSSHService.java
1 /*
2  * Copyright 2000-2009 JetBrains s.r.o.
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16 package org.jetbrains.git4idea.ssh;
17
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;
31
32 import java.io.File;
33 import java.io.IOException;
34 import java.util.Random;
35 import java.util.Vector;
36
37 /**
38  * The provider of SSH scripts for the Git
39  */
40 public class GitSSHService implements ApplicationComponent {
41   /**
42    * The string used to indicate missing value
43    */
44   private static final String XML_RPC_NULL_STRING = "\u0000";
45   /**
46    * the logger
47    */
48   private static final Logger log = Logger.getInstance(GitSSHService.class.getName());
49
50   /**
51    * random number generator to use
52    */
53   private static final Random RANDOM = new Random();
54   /**
55    * Name of the handler
56    */
57   @NonNls private static final String HANDLER_NAME = "Git4ideaSSHHandler";
58   /**
59    * The prefix of the ssh script name
60    */
61   @NonNls private static final String GIT_SSH_PREFIX = "git-ssh-";
62   /**
63    * If true, the component has been initialized
64    */
65   private boolean myInitialized = false;
66   /**
67    * Path to the generated script
68    */
69   private File myScriptPath;
70   /**
71    * XML RPC server
72    */
73   private final XmlRpcServer myXmlRpcServer;
74   /**
75    * Registered handlers
76    */
77   private final THashMap<Integer, Handler> handlers = new THashMap<Integer, Handler>();
78
79   /**
80    * Name of environment variable for SSH handler
81    */
82   @NonNls public static final String SSH_HANDLER_ENV = "GIT4IDEA_SSH_HANDLER";
83   /**
84    * Name of environment variable for SSH executable
85    */
86   @NonNls public static final String GIT_SSH_ENV = "GIT_SSH";
87
88
89   /**
90    * A constructor from parameter
91    *
92    * @param xmlRpcServer the injected XmlRpc server reference
93    */
94   public GitSSHService(final @NotNull XmlRpcServer xmlRpcServer) {
95     myXmlRpcServer = xmlRpcServer;
96   }
97
98   /**
99    * @return an instance of the server
100    */
101   @NotNull
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");
106     }
107     return service;
108   }
109
110   /**
111    * Get file to the script service
112    *
113    * @return path to the script
114    * @throws IOException if script cannot be generated
115    */
116   @NotNull
117   public synchronized File getScriptPath() throws IOException {
118     if (myScriptPath == null) {
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();
125     }
126     return myScriptPath;
127   }
128
129   /**
130    * {@inheritDoc}
131    */
132   @NotNull
133   public String getComponentName() {
134     return GitSSHService.class.getSimpleName();
135   }
136
137   /**
138    * {@inheritDoc}
139    */
140   public void initComponent() {
141     if (!myInitialized) {
142       myXmlRpcServer.addHandler(HANDLER_NAME, new InternalRequestHandler());
143       myInitialized = true;
144     }
145   }
146
147   /**
148    * {@inheritDoc}
149    */
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.");
155       }
156       myScriptPath = null;
157     }
158   }
159
160   /**
161    * Register handler. Note that handlers must be unregistered using {@link #unregisterHandler(int)}.
162    *
163    * @param handler a handler to register
164    * @return an identifier to pass to the environment variable
165    */
166   public synchronized int registerHandler(@NotNull Handler handler) {
167     initComponent();
168     while (true) {
169       int candidate = RANDOM.nextInt();
170       if (candidate == Integer.MIN_VALUE) {
171         continue;
172       }
173       candidate = Math.abs(candidate);
174       if (handlers.containsKey(candidate)) {
175         continue;
176       }
177       handlers.put(candidate, handler);
178       return candidate;
179     }
180   }
181
182   /**
183    * Get handler for the key
184    *
185    * @param key the key to use
186    * @return the registered handler
187    */
188   @NotNull
189   private synchronized Handler getHandler(int key) {
190     Handler rc = handlers.get(key);
191     if (rc == null) {
192       throw new IllegalStateException("No handler for the key " + key);
193     }
194     return rc;
195   }
196
197   /**
198    * Unregister handler by the key
199    *
200    * @param key the key to unregister
201    */
202   public synchronized void unregisterHandler(int key) {
203     if (handlers.remove(key) == null) {
204       throw new IllegalArgumentException("The handler " + key + " is not registered");
205     }
206   }
207
208
209   /**
210    * Handler interface to use by the client code
211    */
212   public interface Handler {
213     /**
214      * Verify key
215      *
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
222      */
223     boolean verifyServerHostKey(final String hostname,
224                                 final int port,
225                                 final String serverHostKeyAlgorithm,
226                                 final String serverHostKey,
227                                 final boolean isNew);
228
229     /**
230      * Ask passphrase
231      *
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.
236      */
237     String askPassphrase(final String username, final String keyPath, final String lastError);
238
239     /**
240      * Reply to challenge in keyboard-interactive scenario
241      *
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
250      */
251     @SuppressWarnings({"UseOfObsoleteCollectionType"})
252     Vector<String> replyToChallenge(final String username,
253                                     final String name,
254                                     final String instruction,
255                                     final int numPrompts,
256                                     final Vector<String> prompt,
257                                     final Vector<Boolean> echo,
258                                     final String lastError);
259
260     /**
261      * Ask password
262      *
263      * @param username  a user name
264      * @param lastError the previous error
265      * @return a password or null if dialog was cancelled.
266      */
267     String askPassword(final String username, final String lastError);
268
269   }
270
271   /**
272    * Internal handler implementation class, do not use it.
273    */
274   public class InternalRequestHandler implements GitSSHHandler {
275     /**
276      * {@inheritDoc}
277      */
278     public boolean verifyServerHostKey(final int handler,
279                                        final String hostname,
280                                        final int port,
281                                        final String serverHostKeyAlgorithm,
282                                        final String serverHostKey,
283                                        final boolean isNew) {
284       return getHandler(handler).verifyServerHostKey(hostname, port, serverHostKeyAlgorithm, serverHostKey, isNew);
285     }
286
287     /**
288      * {@inheritDoc}
289      */
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));
292     }
293
294     /**
295      * {@inheritDoc}
296      */
297     @SuppressWarnings({"UseOfObsoleteCollectionType"})
298     public Vector<String> replyToChallenge(final int handlerNo,
299                                            final String username,
300                                            final String name,
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));
307     }
308
309     /**
310      * {@inheritDoc}
311      */
312     public String askPassword(final int handlerNo, final String username, final String lastError) {
313       return adjustNull(getHandler(handlerNo).askPassword(username, lastError));
314     }
315
316     /**
317      * Adjust null value (by converting to {@link GitSSHService#XML_RPC_NULL_STRING})
318      *
319      * @param s a value to adjust
320      * @return a string if non-null or {@link GitSSHService#XML_RPC_NULL_STRING} if s == null
321      */
322     private String adjustNull(final String s) {
323       return s == null ? XML_RPC_NULL_STRING : s;
324     }
325
326     /**
327      * Adjust null value (returns empty array)
328      *
329      * @param s if null return empty array
330      * @return s if not null, empty array otherwise
331      */
332     @SuppressWarnings({"UseOfObsoleteCollectionType"})
333     private <T> Vector<T> adjustNull(final Vector<T> s) {
334       return s == null ? new Vector<T>() : s;
335     }
336   }
337 }