001package org.apache.maven.wagon.providers.ssh.jsch;
002
003/*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements.  See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership.  The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License.  You may obtain a copy of the License at
011 *
012 *   http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied.  See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022import java.io.BufferedReader;
023import java.io.ByteArrayInputStream;
024import java.io.File;
025import java.io.FileNotFoundException;
026import java.io.IOException;
027import java.io.InputStream;
028import java.io.InputStreamReader;
029import java.io.OutputStream;
030import java.util.List;
031import java.util.Properties;
032
033import org.apache.maven.wagon.CommandExecutionException;
034import org.apache.maven.wagon.CommandExecutor;
035import org.apache.maven.wagon.ResourceDoesNotExistException;
036import org.apache.maven.wagon.StreamWagon;
037import org.apache.maven.wagon.Streams;
038import org.apache.maven.wagon.TransferFailedException;
039import org.apache.maven.wagon.WagonConstants;
040import org.apache.maven.wagon.authentication.AuthenticationException;
041import org.apache.maven.wagon.authentication.AuthenticationInfo;
042import org.apache.maven.wagon.authorization.AuthorizationException;
043import org.apache.maven.wagon.events.TransferEvent;
044import org.apache.maven.wagon.providers.ssh.CommandExecutorStreamProcessor;
045import org.apache.maven.wagon.providers.ssh.ScpHelper;
046import org.apache.maven.wagon.providers.ssh.SshWagon;
047import org.apache.maven.wagon.providers.ssh.interactive.InteractiveUserInfo;
048import org.apache.maven.wagon.providers.ssh.interactive.NullInteractiveUserInfo;
049import org.apache.maven.wagon.providers.ssh.jsch.interactive.UserInfoUIKeyboardInteractiveProxy;
050import org.apache.maven.wagon.providers.ssh.knownhost.KnownHostChangedException;
051import org.apache.maven.wagon.providers.ssh.knownhost.KnownHostEntry;
052import org.apache.maven.wagon.providers.ssh.knownhost.KnownHostsProvider;
053import org.apache.maven.wagon.providers.ssh.knownhost.UnknownHostException;
054import org.apache.maven.wagon.proxy.ProxyInfo;
055import org.apache.maven.wagon.resource.Resource;
056import org.codehaus.plexus.util.IOUtil;
057
058import com.jcraft.jsch.ChannelExec;
059import com.jcraft.jsch.HostKey;
060import com.jcraft.jsch.HostKeyRepository;
061import com.jcraft.jsch.IdentityRepository;
062import com.jcraft.jsch.JSch;
063import com.jcraft.jsch.JSchException;
064import com.jcraft.jsch.Proxy;
065import com.jcraft.jsch.ProxyHTTP;
066import com.jcraft.jsch.ProxySOCKS5;
067import com.jcraft.jsch.Session;
068import com.jcraft.jsch.UIKeyboardInteractive;
069import com.jcraft.jsch.UserInfo;
070import com.jcraft.jsch.agentproxy.AgentProxyException;
071import com.jcraft.jsch.agentproxy.Connector;
072import com.jcraft.jsch.agentproxy.ConnectorFactory;
073import com.jcraft.jsch.agentproxy.RemoteIdentityRepository;
074
075/**
076 * AbstractJschWagon
077 */
078public abstract class AbstractJschWagon
079    extends StreamWagon
080    implements SshWagon, CommandExecutor
081{
082    protected ScpHelper sshTool = new ScpHelper( this );
083
084    protected Session session;
085
086    private String strictHostKeyChecking;
087
088    /**
089     * @plexus.requirement role-hint="file"
090     */
091    private volatile KnownHostsProvider knownHostsProvider;
092
093    /**
094     * @plexus.requirement
095     */
096    private volatile InteractiveUserInfo interactiveUserInfo;
097
098    /**
099     * @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}