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.File;
24  import java.io.FileNotFoundException;
25  import java.io.IOException;
26  import java.io.InputStream;
27  import java.io.InputStreamReader;
28  import java.io.PrintWriter;
29  import java.io.StringWriter;
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.KnownHostsProvider;
52  import org.apache.maven.wagon.providers.ssh.knownhost.UnknownHostException;
53  import org.apache.maven.wagon.proxy.ProxyInfo;
54  import org.apache.maven.wagon.resource.Resource;
55  import org.codehaus.plexus.util.IOUtil;
56  import org.codehaus.plexus.util.StringInputStream;
57  
58  import com.jcraft.jsch.agentproxy.AgentProxyException;
59  import com.jcraft.jsch.agentproxy.Connector;
60  import com.jcraft.jsch.agentproxy.ConnectorFactory;
61  import com.jcraft.jsch.agentproxy.RemoteIdentityRepository;
62  import com.jcraft.jsch.ChannelExec;
63  import com.jcraft.jsch.HostKey;
64  import com.jcraft.jsch.HostKeyRepository;
65  import com.jcraft.jsch.IdentityRepository;
66  import com.jcraft.jsch.JSch;
67  import com.jcraft.jsch.JSchException;
68  import com.jcraft.jsch.Proxy;
69  import com.jcraft.jsch.ProxyHTTP;
70  import com.jcraft.jsch.ProxySOCKS5;
71  import com.jcraft.jsch.Session;
72  import com.jcraft.jsch.UIKeyboardInteractive;
73  import com.jcraft.jsch.UserInfo;
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      /**
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.requirement
98       */
99      private volatile UIKeyboardInteractive uIKeyboardInteractive;
100 
101     private static final int SOCKS5_PROXY_PORT = 1080;
102 
103     protected static final String EXEC_CHANNEL = "exec";
104 
105     public void openConnectionInternal()
106         throws AuthenticationException
107     {
108         if ( authenticationInfo == null )
109         {
110             authenticationInfo = new AuthenticationInfo();
111         }
112 
113         if ( !interactive )
114         {
115             uIKeyboardInteractive = null;
116             setInteractiveUserInfo( new NullInteractiveUserInfo() );
117         }
118 
119         JSch sch = new JSch();
120 
121         File privateKey;
122         try
123         {
124             privateKey = ScpHelper.getPrivateKey( authenticationInfo );
125         }
126         catch ( FileNotFoundException e )
127         {
128             throw new AuthenticationException( e.getMessage() );
129         }
130 
131         //can only pick one method of authentication
132         if ( privateKey != null && privateKey.exists() )
133         {
134             fireSessionDebug( "Using private key: " + privateKey );
135             try
136             {
137                 sch.addIdentity( privateKey.getAbsolutePath(), authenticationInfo.getPassphrase() );
138             }
139             catch ( JSchException e )
140             {
141                 throw new AuthenticationException( "Cannot connect. Reason: " + e.getMessage(), e );
142             }
143         }
144         else
145         {
146             try
147             {
148                 Connector connector = ConnectorFactory.getDefault().createConnector();
149                 if ( connector != null )
150                 {
151                     IdentityRepository repo = new RemoteIdentityRepository( connector );
152                     sch.setIdentityRepository( repo );
153                 }
154             }
155             catch ( AgentProxyException e )
156             {
157                 fireSessionDebug( "Unable to connect to agent: " + e.toString() );
158             }
159 
160         }
161 
162         String host = getRepository().getHost();
163         int port =
164             repository.getPort() == WagonConstants.UNKNOWN_PORT ? ScpHelper.DEFAULT_SSH_PORT : repository.getPort();
165         try
166         {
167             String userName = authenticationInfo.getUserName();
168             if ( userName == null )
169             {
170                 userName = System.getProperty( "user.name" );
171             }
172             session = sch.getSession( userName, host, port );
173             session.setTimeout( getTimeout() );
174         }
175         catch ( JSchException e )
176         {
177             throw new AuthenticationException( "Cannot connect. Reason: " + e.getMessage(), e );
178         }
179 
180         Proxy proxy = null;
181         ProxyInfo proxyInfo = getProxyInfo( ProxyInfo.PROXY_SOCKS5, getRepository().getHost() );
182         if ( proxyInfo != null && proxyInfo.getHost() != null )
183         {
184             proxy = new ProxySOCKS5( proxyInfo.getHost(), proxyInfo.getPort() );
185             ( (ProxySOCKS5) proxy ).setUserPasswd( proxyInfo.getUserName(), proxyInfo.getPassword() );
186         }
187         else
188         {
189             proxyInfo = getProxyInfo( ProxyInfo.PROXY_HTTP, getRepository().getHost() );
190             if ( proxyInfo != null && proxyInfo.getHost() != null )
191             {
192                 proxy = new ProxyHTTP( proxyInfo.getHost(), proxyInfo.getPort() );
193                 ( (ProxyHTTP) proxy ).setUserPasswd( proxyInfo.getUserName(), proxyInfo.getPassword() );
194             }
195             else
196             {
197                 // Backwards compatibility
198                 proxyInfo = getProxyInfo( getRepository().getProtocol(), getRepository().getHost() );
199                 if ( proxyInfo != null && proxyInfo.getHost() != null )
200                 {
201                     // if port == 1080 we will use SOCKS5 Proxy, otherwise will use HTTP Proxy
202                     if ( proxyInfo.getPort() == SOCKS5_PROXY_PORT )
203                     {
204                         proxy = new ProxySOCKS5( proxyInfo.getHost(), proxyInfo.getPort() );
205                         ( (ProxySOCKS5) proxy ).setUserPasswd( proxyInfo.getUserName(), proxyInfo.getPassword() );
206                     }
207                     else
208                     {
209                         proxy = new ProxyHTTP( proxyInfo.getHost(), proxyInfo.getPort() );
210                         ( (ProxyHTTP) proxy ).setUserPasswd( proxyInfo.getUserName(), proxyInfo.getPassword() );
211                     }
212                 }
213             }
214         }
215         session.setProxy( proxy );
216 
217         // username and password will be given via UserInfo interface.
218         UserInfo ui = new WagonUserInfo( authenticationInfo, getInteractiveUserInfo() );
219 
220         if ( uIKeyboardInteractive != null )
221         {
222             ui = new UserInfoUIKeyboardInteractiveProxy( ui, uIKeyboardInteractive );
223         }
224 
225         Properties config = new Properties();
226         if ( getKnownHostsProvider() != null )
227         {
228             try
229             {
230                 String contents = getKnownHostsProvider().getContents();
231                 if ( contents != null )
232                 {
233                     sch.setKnownHosts( new StringInputStream( contents ) );
234                 }
235             }
236             catch ( JSchException e )
237             {
238                 // continue without known_hosts
239             }
240             config.setProperty( "StrictHostKeyChecking", getKnownHostsProvider().getHostKeyChecking() );
241         }
242 
243         if ( authenticationInfo.getPassword() != null )
244         {
245             config.setProperty( "PreferredAuthentications", "gssapi-with-mic,publickey,password,keyboard-interactive" );
246         }
247 
248         config.setProperty( "BatchMode", interactive ? "no" : "yes" );
249 
250         session.setConfig( config );
251 
252         session.setUserInfo( ui );
253 
254         StringWriter stringWriter = new StringWriter();
255         try
256         {
257             session.connect();
258 
259             if ( getKnownHostsProvider() != null )
260             {
261                 PrintWriter w = new PrintWriter( stringWriter );
262 
263                 HostKeyRepository hkr = sch.getHostKeyRepository();
264                 HostKey[] keys = hkr.getHostKey();
265 
266                 for ( int i = 0; keys != null && i < keys.length; i++ )
267                 {
268                     HostKey key = keys[i];
269                     w.println( key.getHost() + " " + key.getType() + " " + key.getKey() );
270                 }
271             }
272         }
273         catch ( JSchException e )
274         {
275             if ( e.getMessage().startsWith( "UnknownHostKey:" ) || e.getMessage().startsWith( "reject HostKey:" ) )
276             {
277                 throw new UnknownHostException( host, e );
278             }
279             else if ( e.getMessage().contains( "HostKey has been changed" ) )
280             {
281                 throw new KnownHostChangedException( host, e );
282             }
283             else
284             {
285                 throw new AuthenticationException( "Cannot connect. Reason: " + e.getMessage(), e );
286             }
287         }
288 
289         try
290         {
291             getKnownHostsProvider().storeKnownHosts( stringWriter.toString() );
292         }
293         catch ( IOException e )
294         {
295             closeConnection();
296 
297             throw new AuthenticationException(
298                 "Connection aborted - failed to write to known_hosts. Reason: " + e.getMessage(), e );
299         }
300     }
301 
302     public void closeConnection()
303     {
304         if ( session != null )
305         {
306             session.disconnect();
307             session = null;
308         }
309     }
310 
311     public Streams executeCommand( String command, boolean ignoreFailures )
312         throws CommandExecutionException
313     {
314         ChannelExec channel = null;
315         BufferedReader stdoutReader = null;
316         BufferedReader stderrReader = null;
317         try
318         {
319             channel = (ChannelExec) session.openChannel( EXEC_CHANNEL );
320 
321             channel.setCommand( command + "\n" );
322 
323             InputStream stdout = channel.getInputStream();
324             InputStream stderr = channel.getErrStream();
325 
326             channel.connect();
327 
328             stdoutReader = new BufferedReader( new InputStreamReader( stdout ) );
329             stderrReader = new BufferedReader( new InputStreamReader( stderr ) );
330 
331             Streams streams = CommandExecutorStreamProcessor.processStreams( stderrReader, stdoutReader );
332 
333             if ( streams.getErr().length() > 0 && !ignoreFailures )
334             {
335                 int exitCode = channel.getExitStatus();
336                 throw new CommandExecutionException( "Exit code: " + exitCode + " - " + streams.getErr() );
337             }
338 
339             return streams;
340         }
341         catch ( IOException e )
342         {
343             throw new CommandExecutionException( "Cannot execute remote command: " + command, e );
344         }
345         catch ( JSchException e )
346         {
347             throw new CommandExecutionException( "Cannot execute remote command: " + command, e );
348         }
349         finally
350         {
351             IOUtil.close( stdoutReader );
352             IOUtil.close( stderrReader );
353             if ( channel != null )
354             {
355                 channel.disconnect();
356             }
357         }
358     }
359 
360     protected void handleGetException( Resource resource, Exception e )
361         throws TransferFailedException
362     {
363         fireTransferError( resource, e, TransferEvent.REQUEST_GET );
364 
365         String msg =
366             "Error occurred while downloading '" + resource + "' from the remote repository:" + getRepository() + ": "
367                 + e.getMessage();
368 
369         throw new TransferFailedException( msg, e );
370     }
371 
372     public List<String> getFileList( String destinationDirectory )
373         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
374     {
375         return sshTool.getFileList( destinationDirectory, repository );
376     }
377 
378     public void putDirectory( File sourceDirectory, String destinationDirectory )
379         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
380     {
381         sshTool.putDirectory( this, sourceDirectory, destinationDirectory );
382     }
383 
384     public boolean resourceExists( String resourceName )
385         throws TransferFailedException, AuthorizationException
386     {
387         return sshTool.resourceExists( resourceName, repository );
388     }
389 
390     public boolean supportsDirectoryCopy()
391     {
392         return true;
393     }
394 
395     public void executeCommand( String command )
396         throws CommandExecutionException
397     {
398         fireTransferDebug( "Executing command: " + command );
399 
400         executeCommand( command, false );
401     }
402 
403     public InteractiveUserInfo getInteractiveUserInfo()
404     {
405         return this.interactiveUserInfo;
406     }
407 
408     public KnownHostsProvider getKnownHostsProvider()
409     {
410         return this.knownHostsProvider;
411     }
412 
413     public void setInteractiveUserInfo( InteractiveUserInfo interactiveUserInfo )
414     {
415         this.interactiveUserInfo = interactiveUserInfo;
416     }
417 
418     public void setKnownHostsProvider( KnownHostsProvider knownHostsProvider )
419     {
420         this.knownHostsProvider = knownHostsProvider;
421     }
422 
423     public void setUIKeyboardInteractive( UIKeyboardInteractive uIKeyboardInteractive )
424     {
425         this.uIKeyboardInteractive = uIKeyboardInteractive;
426     }
427 }