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        try
132        {
133            Connector connector = ConnectorFactory.getDefault().createConnector();
134            if ( connector != null )
135            {
136                IdentityRepository repo = new RemoteIdentityRepository( connector );
137                sch.setIdentityRepository( repo );
138            }
139        }
140        catch ( AgentProxyException e )
141        {
142            fireSessionDebug( "Unable to connect to agent: " + e.toString() );
143        }
144
145        if ( privateKey != null && privateKey.exists() )
146        {
147            fireSessionDebug( "Using private key: " + privateKey );
148            try
149            {
150                sch.addIdentity( privateKey.getAbsolutePath(), authenticationInfo.getPassphrase() );
151            }
152            catch ( JSchException e )
153            {
154                throw new AuthenticationException( "Cannot connect. Reason: " + e.getMessage(), e );
155            }
156        }
157
158        String host = getRepository().getHost();
159        int port =
160            repository.getPort() == WagonConstants.UNKNOWN_PORT ? ScpHelper.DEFAULT_SSH_PORT : repository.getPort();
161        try
162        {
163            String userName = authenticationInfo.getUserName();
164            if ( userName == null )
165            {
166                userName = System.getProperty( "user.name" );
167            }
168            session = sch.getSession( userName, host, port );
169            session.setTimeout( getTimeout() );
170        }
171        catch ( JSchException e )
172        {
173            throw new AuthenticationException( "Cannot connect. Reason: " + e.getMessage(), e );
174        }
175
176        Proxy proxy = null;
177        ProxyInfo proxyInfo = getProxyInfo( ProxyInfo.PROXY_SOCKS5, getRepository().getHost() );
178        if ( proxyInfo != null && proxyInfo.getHost() != null )
179        {
180            proxy = new ProxySOCKS5( proxyInfo.getHost(), proxyInfo.getPort() );
181            ( (ProxySOCKS5) proxy ).setUserPasswd( proxyInfo.getUserName(), proxyInfo.getPassword() );
182        }
183        else
184        {
185            proxyInfo = getProxyInfo( ProxyInfo.PROXY_HTTP, getRepository().getHost() );
186            if ( proxyInfo != null && proxyInfo.getHost() != null )
187            {
188                proxy = new ProxyHTTP( proxyInfo.getHost(), proxyInfo.getPort() );
189                ( (ProxyHTTP) proxy ).setUserPasswd( proxyInfo.getUserName(), proxyInfo.getPassword() );
190            }
191            else
192            {
193                // Backwards compatibility
194                proxyInfo = getProxyInfo( getRepository().getProtocol(), getRepository().getHost() );
195                if ( proxyInfo != null && proxyInfo.getHost() != null )
196                {
197                    // if port == 1080 we will use SOCKS5 Proxy, otherwise will use HTTP Proxy
198                    if ( proxyInfo.getPort() == SOCKS5_PROXY_PORT )
199                    {
200                        proxy = new ProxySOCKS5( proxyInfo.getHost(), proxyInfo.getPort() );
201                        ( (ProxySOCKS5) proxy ).setUserPasswd( proxyInfo.getUserName(), proxyInfo.getPassword() );
202                    }
203                    else
204                    {
205                        proxy = new ProxyHTTP( proxyInfo.getHost(), proxyInfo.getPort() );
206                        ( (ProxyHTTP) proxy ).setUserPasswd( proxyInfo.getUserName(), proxyInfo.getPassword() );
207                    }
208                }
209            }
210        }
211        session.setProxy( proxy );
212
213        // username and password will be given via UserInfo interface.
214        UserInfo ui = new WagonUserInfo( authenticationInfo, getInteractiveUserInfo() );
215
216        if ( uIKeyboardInteractive != null )
217        {
218            ui = new UserInfoUIKeyboardInteractiveProxy( ui, uIKeyboardInteractive );
219        }
220
221        Properties config = new Properties();
222        if ( getKnownHostsProvider() != null )
223        {
224            try
225            {
226                String contents = getKnownHostsProvider().getContents();
227                if ( contents != null )
228                {
229                    sch.setKnownHosts( new StringInputStream( contents ) );
230                }
231            }
232            catch ( JSchException e )
233            {
234                // continue without known_hosts
235            }
236            config.setProperty( "StrictHostKeyChecking", getKnownHostsProvider().getHostKeyChecking() );
237        }
238
239        if ( authenticationInfo.getPassword() != null )
240        {
241            config.setProperty( "PreferredAuthentications", "gssapi-with-mic,publickey,password,keyboard-interactive" );
242        }
243
244        config.setProperty( "BatchMode", interactive ? "no" : "yes" );
245
246        session.setConfig( config );
247
248        session.setUserInfo( ui );
249
250        StringWriter stringWriter = new StringWriter();
251        try
252        {
253            session.connect();
254
255            if ( getKnownHostsProvider() != null )
256            {
257                PrintWriter w = new PrintWriter( stringWriter );
258
259                HostKeyRepository hkr = sch.getHostKeyRepository();
260                HostKey[] keys = hkr.getHostKey();
261
262                for ( int i = 0; keys != null && i < keys.length; i++ )
263                {
264                    HostKey key = keys[i];
265                    w.println( key.getHost() + " " + key.getType() + " " + key.getKey() );
266                }
267            }
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        try
286        {
287            getKnownHostsProvider().storeKnownHosts( stringWriter.toString() );
288        }
289        catch ( IOException e )
290        {
291            closeConnection();
292
293            throw new AuthenticationException(
294                "Connection aborted - failed to write to known_hosts. Reason: " + e.getMessage(), e );
295        }
296    }
297
298    public void closeConnection()
299    {
300        if ( session != null )
301        {
302            session.disconnect();
303            session = null;
304        }
305    }
306
307    public Streams executeCommand( String command, boolean ignoreFailures )
308        throws CommandExecutionException
309    {
310        ChannelExec channel = null;
311        BufferedReader stdoutReader = null;
312        BufferedReader stderrReader = null;
313        try
314        {
315            channel = (ChannelExec) session.openChannel( EXEC_CHANNEL );
316
317            channel.setCommand( command + "\n" );
318
319            InputStream stdout = channel.getInputStream();
320            InputStream stderr = channel.getErrStream();
321
322            channel.connect();
323
324            stdoutReader = new BufferedReader( new InputStreamReader( stdout ) );
325            stderrReader = new BufferedReader( new InputStreamReader( stderr ) );
326
327            Streams streams = CommandExecutorStreamProcessor.processStreams( stderrReader, stdoutReader );
328
329            if ( streams.getErr().length() > 0 && !ignoreFailures )
330            {
331                int exitCode = channel.getExitStatus();
332                throw new CommandExecutionException( "Exit code: " + exitCode + " - " + streams.getErr() );
333            }
334
335            return streams;
336        }
337        catch ( IOException e )
338        {
339            throw new CommandExecutionException( "Cannot execute remote command: " + command, e );
340        }
341        catch ( JSchException e )
342        {
343            throw new CommandExecutionException( "Cannot execute remote command: " + command, e );
344        }
345        finally
346        {
347            IOUtil.close( stdoutReader );
348            IOUtil.close( stderrReader );
349            if ( channel != null )
350            {
351                channel.disconnect();
352            }
353        }
354    }
355
356    protected void handleGetException( Resource resource, Exception e )
357        throws TransferFailedException
358    {
359        fireTransferError( resource, e, TransferEvent.REQUEST_GET );
360
361        String msg =
362            "Error occurred while downloading '" + resource + "' from the remote repository:" + getRepository() + ": "
363                + e.getMessage();
364
365        throw new TransferFailedException( msg, e );
366    }
367
368    public List<String> getFileList( String destinationDirectory )
369        throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
370    {
371        return sshTool.getFileList( destinationDirectory, repository );
372    }
373
374    public void putDirectory( File sourceDirectory, String destinationDirectory )
375        throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
376    {
377        sshTool.putDirectory( this, sourceDirectory, destinationDirectory );
378    }
379
380    public boolean resourceExists( String resourceName )
381        throws TransferFailedException, AuthorizationException
382    {
383        return sshTool.resourceExists( resourceName, repository );
384    }
385
386    public boolean supportsDirectoryCopy()
387    {
388        return true;
389    }
390
391    public void executeCommand( String command )
392        throws CommandExecutionException
393    {
394        fireTransferDebug( "Executing command: " + command );
395
396        executeCommand( command, false );
397    }
398
399    public InteractiveUserInfo getInteractiveUserInfo()
400    {
401        return this.interactiveUserInfo;
402    }
403
404    public KnownHostsProvider getKnownHostsProvider()
405    {
406        return this.knownHostsProvider;
407    }
408
409    public void setInteractiveUserInfo( InteractiveUserInfo interactiveUserInfo )
410    {
411        this.interactiveUserInfo = interactiveUserInfo;
412    }
413
414    public void setKnownHostsProvider( KnownHostsProvider knownHostsProvider )
415    {
416        this.knownHostsProvider = knownHostsProvider;
417    }
418}