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.InputStreamReader;
028import java.util.List;
029import java.util.Properties;
030
031import org.apache.maven.wagon.CommandExecutionException;
032import org.apache.maven.wagon.CommandExecutor;
033import org.apache.maven.wagon.ResourceDoesNotExistException;
034import org.apache.maven.wagon.StreamWagon;
035import org.apache.maven.wagon.Streams;
036import org.apache.maven.wagon.TransferFailedException;
037import org.apache.maven.wagon.WagonConstants;
038import org.apache.maven.wagon.authentication.AuthenticationException;
039import org.apache.maven.wagon.authentication.AuthenticationInfo;
040import org.apache.maven.wagon.authorization.AuthorizationException;
041import org.apache.maven.wagon.events.TransferEvent;
042import org.apache.maven.wagon.providers.ssh.CommandExecutorStreamProcessor;
043import org.apache.maven.wagon.providers.ssh.ScpHelper;
044import org.apache.maven.wagon.providers.ssh.SshWagon;
045import org.apache.maven.wagon.providers.ssh.interactive.InteractiveUserInfo;
046import org.apache.maven.wagon.providers.ssh.interactive.NullInteractiveUserInfo;
047import org.apache.maven.wagon.providers.ssh.jsch.interactive.UserInfoUIKeyboardInteractiveProxy;
048import org.apache.maven.wagon.providers.ssh.knownhost.KnownHostChangedException;
049import org.apache.maven.wagon.providers.ssh.knownhost.KnownHostEntry;
050import org.apache.maven.wagon.providers.ssh.knownhost.KnownHostsProvider;
051import org.apache.maven.wagon.providers.ssh.knownhost.UnknownHostException;
052import org.apache.maven.wagon.proxy.ProxyInfo;
053import org.apache.maven.wagon.resource.Resource;
054import org.codehaus.plexus.util.IOUtil;
055
056import com.jcraft.jsch.ChannelExec;
057import com.jcraft.jsch.HostKey;
058import com.jcraft.jsch.HostKeyRepository;
059import com.jcraft.jsch.IdentityRepository;
060import com.jcraft.jsch.JSch;
061import com.jcraft.jsch.JSchException;
062import com.jcraft.jsch.Proxy;
063import com.jcraft.jsch.ProxyHTTP;
064import com.jcraft.jsch.ProxySOCKS5;
065import com.jcraft.jsch.Session;
066import com.jcraft.jsch.UIKeyboardInteractive;
067import com.jcraft.jsch.UserInfo;
068import com.jcraft.jsch.agentproxy.AgentProxyException;
069import com.jcraft.jsch.agentproxy.Connector;
070import com.jcraft.jsch.agentproxy.ConnectorFactory;
071import com.jcraft.jsch.agentproxy.RemoteIdentityRepository;
072
073/**
074 * AbstractJschWagon
075 */
076public abstract class AbstractJschWagon
077    extends StreamWagon
078    implements SshWagon, CommandExecutor
079{
080    protected ScpHelper sshTool = new ScpHelper( this );
081
082    protected Session session;
083
084    private String strictHostKeyChecking;
085
086    /**
087     * @plexus.requirement role-hint="file"
088     */
089    private volatile KnownHostsProvider knownHostsProvider;
090
091    /**
092     * @plexus.requirement
093     */
094    private volatile InteractiveUserInfo interactiveUserInfo;
095
096    /**
097     * @plexus.configuration default-value="gssapi-with-mic,publickey,password,keyboard-interactive"
098     */
099    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}