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.File;
024import java.io.FileNotFoundException;
025import java.io.IOException;
026import java.io.InputStream;
027import java.io.InputStreamReader;
028import java.io.PrintWriter;
029import java.io.StringWriter;
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.KnownHostsProvider;
052import org.apache.maven.wagon.providers.ssh.knownhost.UnknownHostException;
053import org.apache.maven.wagon.proxy.ProxyInfo;
054import org.apache.maven.wagon.resource.Resource;
055import org.codehaus.plexus.util.IOUtil;
056import org.codehaus.plexus.util.StringInputStream;
057
058import com.jcraft.jsch.agentproxy.AgentProxyException;
059import com.jcraft.jsch.agentproxy.Connector;
060import com.jcraft.jsch.agentproxy.ConnectorFactory;
061import com.jcraft.jsch.agentproxy.RemoteIdentityRepository;
062import com.jcraft.jsch.ChannelExec;
063import com.jcraft.jsch.HostKey;
064import com.jcraft.jsch.HostKeyRepository;
065import com.jcraft.jsch.IdentityRepository;
066import com.jcraft.jsch.JSch;
067import com.jcraft.jsch.JSchException;
068import com.jcraft.jsch.Proxy;
069import com.jcraft.jsch.ProxyHTTP;
070import com.jcraft.jsch.ProxySOCKS5;
071import com.jcraft.jsch.Session;
072import com.jcraft.jsch.UIKeyboardInteractive;
073import com.jcraft.jsch.UserInfo;
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    /**
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.requirement
098     */
099    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}