View Javadoc
1   package org.apache.maven.wagon.providers.ssh.jsch;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.BufferedReader;
23  import java.io.ByteArrayInputStream;
24  import java.io.File;
25  import java.io.FileNotFoundException;
26  import java.io.IOException;
27  import java.io.InputStreamReader;
28  import java.util.List;
29  import java.util.Properties;
30  
31  import org.apache.maven.wagon.CommandExecutionException;
32  import org.apache.maven.wagon.CommandExecutor;
33  import org.apache.maven.wagon.ResourceDoesNotExistException;
34  import org.apache.maven.wagon.StreamWagon;
35  import org.apache.maven.wagon.Streams;
36  import org.apache.maven.wagon.TransferFailedException;
37  import org.apache.maven.wagon.WagonConstants;
38  import org.apache.maven.wagon.authentication.AuthenticationException;
39  import org.apache.maven.wagon.authentication.AuthenticationInfo;
40  import org.apache.maven.wagon.authorization.AuthorizationException;
41  import org.apache.maven.wagon.events.TransferEvent;
42  import org.apache.maven.wagon.providers.ssh.CommandExecutorStreamProcessor;
43  import org.apache.maven.wagon.providers.ssh.ScpHelper;
44  import org.apache.maven.wagon.providers.ssh.SshWagon;
45  import org.apache.maven.wagon.providers.ssh.interactive.InteractiveUserInfo;
46  import org.apache.maven.wagon.providers.ssh.interactive.NullInteractiveUserInfo;
47  import org.apache.maven.wagon.providers.ssh.jsch.interactive.UserInfoUIKeyboardInteractiveProxy;
48  import org.apache.maven.wagon.providers.ssh.knownhost.KnownHostChangedException;
49  import org.apache.maven.wagon.providers.ssh.knownhost.KnownHostEntry;
50  import org.apache.maven.wagon.providers.ssh.knownhost.KnownHostsProvider;
51  import org.apache.maven.wagon.providers.ssh.knownhost.UnknownHostException;
52  import org.apache.maven.wagon.proxy.ProxyInfo;
53  import org.apache.maven.wagon.resource.Resource;
54  import org.codehaus.plexus.util.IOUtil;
55  
56  import com.jcraft.jsch.ChannelExec;
57  import com.jcraft.jsch.HostKey;
58  import com.jcraft.jsch.HostKeyRepository;
59  import com.jcraft.jsch.IdentityRepository;
60  import com.jcraft.jsch.JSch;
61  import com.jcraft.jsch.JSchException;
62  import com.jcraft.jsch.Proxy;
63  import com.jcraft.jsch.ProxyHTTP;
64  import com.jcraft.jsch.ProxySOCKS5;
65  import com.jcraft.jsch.Session;
66  import com.jcraft.jsch.UIKeyboardInteractive;
67  import com.jcraft.jsch.UserInfo;
68  import com.jcraft.jsch.agentproxy.AgentProxyException;
69  import com.jcraft.jsch.agentproxy.Connector;
70  import com.jcraft.jsch.agentproxy.ConnectorFactory;
71  import com.jcraft.jsch.agentproxy.RemoteIdentityRepository;
72  
73  /**
74   * AbstractJschWagon
75   */
76  public abstract class AbstractJschWagon
77      extends StreamWagon
78      implements SshWagon, CommandExecutor
79  {
80      protected ScpHelper sshTool = new ScpHelper( this );
81  
82      protected Session session;
83  
84      private String strictHostKeyChecking;
85  
86      /**
87       * @plexus.requirement role-hint="file"
88       */
89      private volatile KnownHostsProvider knownHostsProvider;
90  
91      /**
92       * @plexus.requirement
93       */
94      private volatile InteractiveUserInfo interactiveUserInfo;
95  
96      /**
97       * @plexus.configuration default-value="gssapi-with-mic,publickey,password,keyboard-interactive"
98       */
99      private volatile String preferredAuthentications;
100 
101     /**
102      * @plexus.requirement
103      */
104     private volatile UIKeyboardInteractive uIKeyboardInteractive;
105 
106     private static final int SOCKS5_PROXY_PORT = 1080;
107 
108     protected static final String EXEC_CHANNEL = "exec";
109 
110     public void openConnectionInternal()
111         throws AuthenticationException
112     {
113         if ( authenticationInfo == null )
114         {
115             authenticationInfo = new AuthenticationInfo();
116         }
117 
118         if ( !interactive )
119         {
120             uIKeyboardInteractive = null;
121             setInteractiveUserInfo( new NullInteractiveUserInfo() );
122         }
123 
124         JSch sch = new JSch();
125 
126         File privateKey;
127         try
128         {
129             privateKey = ScpHelper.getPrivateKey( authenticationInfo );
130         }
131         catch ( FileNotFoundException e )
132         {
133             throw new AuthenticationException( e.getMessage() );
134         }
135 
136         //can only pick one method of authentication
137         if ( privateKey != null && privateKey.exists() )
138         {
139             fireSessionDebug( "Using private key: " + privateKey );
140             try
141             {
142                 sch.addIdentity( privateKey.getAbsolutePath(), authenticationInfo.getPassphrase() );
143             }
144             catch ( JSchException e )
145             {
146                 throw new AuthenticationException( "Cannot connect. Reason: " + e.getMessage(), e );
147             }
148         }
149         else
150         {
151             try
152             {
153                 Connector connector = ConnectorFactory.getDefault().createConnector();
154                 if ( connector != null )
155                 {
156                     IdentityRepository repo = new RemoteIdentityRepository( connector );
157                     sch.setIdentityRepository( repo );
158                 }
159             }
160             catch ( AgentProxyException e )
161             {
162                 fireSessionDebug( "Unable to connect to agent: " + e.toString() );
163             }
164 
165         }
166 
167         String host = getRepository().getHost();
168         int port =
169             repository.getPort() == WagonConstants.UNKNOWN_PORT ? ScpHelper.DEFAULT_SSH_PORT : repository.getPort();
170         try
171         {
172             String userName = authenticationInfo.getUserName();
173             if ( userName == null )
174             {
175                 userName = System.getProperty( "user.name" );
176             }
177             session = sch.getSession( userName, host, port );
178             session.setTimeout( getTimeout() );
179         }
180         catch ( JSchException e )
181         {
182             throw new AuthenticationException( "Cannot connect. Reason: " + e.getMessage(), e );
183         }
184 
185         Proxy proxy = null;
186         ProxyInfo proxyInfo = getProxyInfo( ProxyInfo.PROXY_SOCKS5, getRepository().getHost() );
187         if ( proxyInfo != null && proxyInfo.getHost() != null )
188         {
189             proxy = new ProxySOCKS5( proxyInfo.getHost(), proxyInfo.getPort() );
190             ( (ProxySOCKS5) proxy ).setUserPasswd( proxyInfo.getUserName(), proxyInfo.getPassword() );
191         }
192         else
193         {
194             proxyInfo = getProxyInfo( ProxyInfo.PROXY_HTTP, getRepository().getHost() );
195             if ( proxyInfo != null && proxyInfo.getHost() != null )
196             {
197                 proxy = new ProxyHTTP( proxyInfo.getHost(), proxyInfo.getPort() );
198                 ( (ProxyHTTP) proxy ).setUserPasswd( proxyInfo.getUserName(), proxyInfo.getPassword() );
199             }
200             else
201             {
202                 // Backwards compatibility
203                 proxyInfo = getProxyInfo( getRepository().getProtocol(), getRepository().getHost() );
204                 if ( proxyInfo != null && proxyInfo.getHost() != null )
205                 {
206                     // if port == 1080 we will use SOCKS5 Proxy, otherwise will use HTTP Proxy
207                     if ( proxyInfo.getPort() == SOCKS5_PROXY_PORT )
208                     {
209                         proxy = new ProxySOCKS5( proxyInfo.getHost(), proxyInfo.getPort() );
210                         ( (ProxySOCKS5) proxy ).setUserPasswd( proxyInfo.getUserName(), proxyInfo.getPassword() );
211                     }
212                     else
213                     {
214                         proxy = new ProxyHTTP( proxyInfo.getHost(), proxyInfo.getPort() );
215                         ( (ProxyHTTP) proxy ).setUserPasswd( proxyInfo.getUserName(), proxyInfo.getPassword() );
216                     }
217                 }
218             }
219         }
220         session.setProxy( proxy );
221 
222         // username and password will be given via UserInfo interface.
223         UserInfo ui = new WagonUserInfo( authenticationInfo, getInteractiveUserInfo() );
224 
225         if ( uIKeyboardInteractive != null )
226         {
227             ui = new UserInfoUIKeyboardInteractiveProxy( ui, uIKeyboardInteractive );
228         }
229 
230         Properties config = new Properties();
231         if ( getKnownHostsProvider() != null )
232         {
233             try
234             {
235                 String contents = getKnownHostsProvider().getContents();
236                 if ( contents != null )
237                 {
238                     sch.setKnownHosts( new ByteArrayInputStream( contents.getBytes() ) );
239                 }
240             }
241             catch ( JSchException e )
242             {
243                 // continue without known_hosts
244             }
245             if ( strictHostKeyChecking == null )
246             {
247                 strictHostKeyChecking = getKnownHostsProvider().getHostKeyChecking();
248             }
249             config.setProperty( "StrictHostKeyChecking", strictHostKeyChecking );
250         }
251 
252         if ( authenticationInfo.getPassword() != null )
253         {
254             config.setProperty( "PreferredAuthentications", preferredAuthentications );
255         }
256 
257         config.setProperty( "BatchMode", interactive ? "no" : "yes" );
258 
259         session.setConfig( config );
260 
261         session.setUserInfo( ui );
262 
263         try
264         {
265             session.connect();
266         }
267         catch ( JSchException e )
268         {
269             if ( e.getMessage().startsWith( "UnknownHostKey:" ) || e.getMessage().startsWith( "reject HostKey:" ) )
270             {
271                 throw new UnknownHostException( host, e );
272             }
273             else if ( e.getMessage().contains( "HostKey has been changed" ) )
274             {
275                 throw new KnownHostChangedException( host, e );
276             }
277             else
278             {
279                 throw new AuthenticationException( "Cannot connect. Reason: " + e.getMessage(), e );
280             }
281         }
282 
283         if ( getKnownHostsProvider() != null )
284         {
285             HostKeyRepository hkr = sch.getHostKeyRepository();
286 
287             HostKey[] hk = hkr.getHostKey( host, null );
288             try
289             {
290                 if ( hk != null )
291                 {
292                     for ( HostKey hostKey : hk )
293                     {
294                         KnownHostEntry knownHostEntry = new KnownHostEntry( hostKey.getHost(), hostKey.getType(),
295                             hostKey.getKey() );
296                         getKnownHostsProvider().addKnownHost( knownHostEntry );
297                     }
298                 }
299             }
300             catch ( IOException e )
301             {
302                 closeConnection();
303 
304                 throw new AuthenticationException(
305                     "Connection aborted - failed to write to known_hosts. Reason: " + e.getMessage(), e );
306             }
307         }
308     }
309 
310     public void closeConnection()
311     {
312         if ( session != null )
313         {
314             session.disconnect();
315             session = null;
316         }
317     }
318 
319     public Streams executeCommand( String command, boolean ignoreStdErr, boolean ignoreNoneZeroExitCode )
320         throws CommandExecutionException
321     {
322         ChannelExec channel = null;
323         BufferedReader stdoutReader = null;
324         BufferedReader stderrReader = null;
325         Streams streams = null;
326         try
327         {
328             channel = (ChannelExec) session.openChannel( EXEC_CHANNEL );
329 
330             fireSessionDebug( "Executing: " + command );
331             channel.setCommand( command + "\n" );
332 
333             stdoutReader = new BufferedReader( new InputStreamReader( channel.getInputStream() ) );
334             stderrReader = new BufferedReader( new InputStreamReader( channel.getErrStream() ) );
335 
336             channel.connect();
337 
338             streams = CommandExecutorStreamProcessor.processStreams( stderrReader, stdoutReader );
339 
340             stdoutReader.close();
341             stdoutReader = null;
342 
343             stderrReader.close();
344             stderrReader = null;
345 
346             int exitCode = channel.getExitStatus();
347 
348             if ( streams.getErr().length() > 0 && !ignoreStdErr )
349             {
350                 throw new CommandExecutionException( "Exit code: " + exitCode + " - " + streams.getErr() );
351             }
352 
353             if ( exitCode != 0 && !ignoreNoneZeroExitCode )
354             {
355                 throw new CommandExecutionException( "Exit code: " + exitCode + " - " + streams.getErr() );
356             }
357 
358             return streams;
359         }
360         catch ( IOException e )
361         {
362             throw new CommandExecutionException( "Cannot execute remote command: " + command, e );
363         }
364         catch ( JSchException e )
365         {
366             throw new CommandExecutionException( "Cannot execute remote command: " + command, e );
367         }
368         finally
369         {
370             if ( streams != null )
371             {
372                 fireSessionDebug( "Stdout results:" + streams.getOut() );
373                 fireSessionDebug( "Stderr results:" + streams.getErr() );
374             }
375 
376             IOUtil.close( stdoutReader );
377             IOUtil.close( stderrReader );
378             if ( channel != null )
379             {
380                 channel.disconnect();
381             }
382         }
383     }
384 
385     protected void handleGetException( Resource resource, Exception e )
386         throws TransferFailedException
387     {
388         fireTransferError( resource, e, TransferEvent.REQUEST_GET );
389 
390         String msg =
391             "Error occurred while downloading '" + resource + "' from the remote repository:" + getRepository() + ": "
392                 + e.getMessage();
393 
394         throw new TransferFailedException( msg, e );
395     }
396 
397     public List<String> getFileList( String destinationDirectory )
398         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
399     {
400         return sshTool.getFileList( destinationDirectory, repository );
401     }
402 
403     public void putDirectory( File sourceDirectory, String destinationDirectory )
404         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
405     {
406         sshTool.putDirectory( this, sourceDirectory, destinationDirectory );
407     }
408 
409     public boolean resourceExists( String resourceName )
410         throws TransferFailedException, AuthorizationException
411     {
412         return sshTool.resourceExists( resourceName, repository );
413     }
414 
415     public boolean supportsDirectoryCopy()
416     {
417         return true;
418     }
419 
420     public void executeCommand( String command )
421         throws CommandExecutionException
422     {
423         fireTransferDebug( "Executing command: " + command );
424 
425         //backward compatible with wagon 2.10
426         executeCommand( command, false, true );
427     }
428 
429     public Streams executeCommand( String command, boolean ignoreFailures )
430             throws CommandExecutionException
431     {
432         fireTransferDebug( "Executing command: " + command );
433 
434         //backward compatible with wagon 2.10
435         return executeCommand( command, ignoreFailures, true );
436     }
437 
438     public InteractiveUserInfo getInteractiveUserInfo()
439     {
440         return this.interactiveUserInfo;
441     }
442 
443     public KnownHostsProvider getKnownHostsProvider()
444     {
445         return this.knownHostsProvider;
446     }
447 
448     public void setInteractiveUserInfo( InteractiveUserInfo interactiveUserInfo )
449     {
450         this.interactiveUserInfo = interactiveUserInfo;
451     }
452 
453     public void setKnownHostsProvider( KnownHostsProvider knownHostsProvider )
454     {
455         this.knownHostsProvider = knownHostsProvider;
456     }
457 
458     public void setUIKeyboardInteractive( UIKeyboardInteractive uIKeyboardInteractive )
459     {
460         this.uIKeyboardInteractive = uIKeyboardInteractive;
461     }
462 
463     public String getPreferredAuthentications()
464     {
465         return preferredAuthentications;
466     }
467 
468     public void setPreferredAuthentications( String preferredAuthentications )
469     {
470         this.preferredAuthentications = preferredAuthentications;
471     }
472 
473     public String getStrictHostKeyChecking()
474     {
475         return strictHostKeyChecking;
476     }
477 
478     public void setStrictHostKeyChecking( String strictHostKeyChecking )
479     {
480         this.strictHostKeyChecking = strictHostKeyChecking;
481     }
482 }