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