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.ChannelExec;
059import com.jcraft.jsch.HostKey;
060import com.jcraft.jsch.HostKeyRepository;
061import com.jcraft.jsch.JSch;
062import com.jcraft.jsch.JSchException;
063import com.jcraft.jsch.Proxy;
064import com.jcraft.jsch.ProxyHTTP;
065import com.jcraft.jsch.ProxySOCKS5;
066import com.jcraft.jsch.Session;
067import com.jcraft.jsch.UIKeyboardInteractive;
068import com.jcraft.jsch.UserInfo;
069
070/**
071 * AbstractJschWagon 
072 *
073 *
074 */
075public abstract class AbstractJschWagon
076    extends StreamWagon
077    implements SshWagon, CommandExecutor
078{
079    protected ScpHelper sshTool = new ScpHelper( this );
080    
081    protected Session session;
082    
083    /**
084     * @plexus.requirement role-hint="file"
085     */
086    private volatile KnownHostsProvider knownHostsProvider;
087    
088    /**
089     * @plexus.requirement
090     */
091    private volatile InteractiveUserInfo interactiveUserInfo;
092
093    /**
094     * @plexus.requirement
095     */
096    private volatile UIKeyboardInteractive uIKeyboardInteractive;
097
098    private static final int SOCKS5_PROXY_PORT = 1080;
099
100    protected static final String EXEC_CHANNEL = "exec";
101
102    public void openConnectionInternal()
103        throws AuthenticationException
104    {
105        if ( authenticationInfo == null )
106        {
107            authenticationInfo = new AuthenticationInfo();
108        }
109
110        if ( !interactive )
111        {
112            uIKeyboardInteractive = null;
113            setInteractiveUserInfo( new NullInteractiveUserInfo() );
114        }
115
116        JSch sch = new JSch();
117
118        File privateKey;
119        try
120        {
121            privateKey = ScpHelper.getPrivateKey( authenticationInfo );
122        }
123        catch ( FileNotFoundException e )
124        {
125            throw new AuthenticationException( e.getMessage() );
126        }
127
128        if ( privateKey != null && privateKey.exists() )
129        {
130            fireSessionDebug( "Using private key: " + privateKey );
131            try
132            {
133                sch.addIdentity( privateKey.getAbsolutePath(), authenticationInfo.getPassphrase() );
134            }
135            catch ( JSchException e )
136            {
137                throw new AuthenticationException( "Cannot connect. Reason: " + e.getMessage(), e );
138            }
139        }
140
141        String host = getRepository().getHost();
142        int port =
143            repository.getPort() == WagonConstants.UNKNOWN_PORT ? ScpHelper.DEFAULT_SSH_PORT : repository.getPort();
144        try
145        {
146            String userName = authenticationInfo.getUserName();
147            if ( userName == null )
148            {
149                userName = System.getProperty( "user.name" );
150            }
151            session = sch.getSession( userName, host, port );
152            session.setTimeout( getTimeout() );
153        }
154        catch ( JSchException e )
155        {
156            throw new AuthenticationException( "Cannot connect. Reason: " + e.getMessage(), e );
157        }
158
159        Proxy proxy = null;
160        ProxyInfo proxyInfo = getProxyInfo( ProxyInfo.PROXY_SOCKS5, getRepository().getHost() );
161        if ( proxyInfo != null && proxyInfo.getHost() != null )
162        {
163            proxy = new ProxySOCKS5( proxyInfo.getHost(), proxyInfo.getPort() );
164            ( (ProxySOCKS5) proxy ).setUserPasswd( proxyInfo.getUserName(), proxyInfo.getPassword() );
165        }
166        else
167        {
168            proxyInfo = getProxyInfo( ProxyInfo.PROXY_HTTP, getRepository().getHost() );
169            if ( proxyInfo != null && proxyInfo.getHost() != null )
170            {
171                proxy = new ProxyHTTP( proxyInfo.getHost(), proxyInfo.getPort() );
172                ( (ProxyHTTP) proxy ).setUserPasswd( proxyInfo.getUserName(), proxyInfo.getPassword() );
173            }
174            else
175            {
176                // Backwards compatibility
177                proxyInfo = getProxyInfo( getRepository().getProtocol(), getRepository().getHost() );
178                if ( proxyInfo != null && proxyInfo.getHost() != null )
179                {
180                    // if port == 1080 we will use SOCKS5 Proxy, otherwise will use HTTP Proxy
181                    if ( proxyInfo.getPort() == SOCKS5_PROXY_PORT )
182                    {
183                        proxy = new ProxySOCKS5( proxyInfo.getHost(), proxyInfo.getPort() );
184                        ( (ProxySOCKS5) proxy ).setUserPasswd( proxyInfo.getUserName(), proxyInfo.getPassword() );
185                    }
186                    else
187                    {
188                        proxy = new ProxyHTTP( proxyInfo.getHost(), proxyInfo.getPort() );
189                        ( (ProxyHTTP) proxy ).setUserPasswd( proxyInfo.getUserName(), proxyInfo.getPassword() );
190                    }
191                }
192            }
193        }
194        session.setProxy( proxy );
195
196        // username and password will be given via UserInfo interface.
197        UserInfo ui = new WagonUserInfo( authenticationInfo, getInteractiveUserInfo() );
198
199        if ( uIKeyboardInteractive != null )
200        {
201            ui = new UserInfoUIKeyboardInteractiveProxy( ui, uIKeyboardInteractive );
202        }
203
204        Properties config = new Properties();
205        if ( getKnownHostsProvider() != null )
206        {
207            try
208            {
209                String contents = getKnownHostsProvider().getContents();
210                if ( contents != null )
211                {
212                    sch.setKnownHosts( new StringInputStream( contents ) );
213                }
214            }
215            catch ( JSchException e )
216            {
217                // continue without known_hosts
218            }
219            config.setProperty( "StrictHostKeyChecking", getKnownHostsProvider().getHostKeyChecking() );
220        }
221
222        if ( authenticationInfo.getPassword() != null )
223        {
224            config.setProperty( "PreferredAuthentications", "gssapi-with-mic,publickey,password,keyboard-interactive" );
225        }
226        
227        config.setProperty( "BatchMode", interactive ? "no" : "yes" );
228
229        session.setConfig( config );
230
231        session.setUserInfo( ui );
232
233        StringWriter stringWriter = new StringWriter();
234        try
235        {
236            session.connect();
237
238            if ( getKnownHostsProvider() != null )
239            {
240                PrintWriter w = new PrintWriter( stringWriter );
241
242                HostKeyRepository hkr = sch.getHostKeyRepository();
243                HostKey[] keys = hkr.getHostKey();
244
245                for ( int i = 0; keys != null && i < keys.length; i++ )
246                {
247                    HostKey key = keys[i];
248                    w.println( key.getHost() + " " + key.getType() + " " + key.getKey() );
249                }
250            }
251        }
252        catch ( JSchException e )
253        {
254            if ( e.getMessage().startsWith( "UnknownHostKey:" ) || e.getMessage().startsWith( "reject HostKey:" ) )
255            {
256                throw new UnknownHostException( host, e );
257            }
258            else if ( e.getMessage().indexOf( "HostKey has been changed" ) >= 0 )
259            {
260                throw new KnownHostChangedException( host, e );
261            }
262            else
263            {
264                throw new AuthenticationException( "Cannot connect. Reason: " + e.getMessage(), e );
265            }
266        }
267
268        try
269        {
270            getKnownHostsProvider().storeKnownHosts( stringWriter.toString() );
271        }
272        catch ( IOException e )
273        {
274            closeConnection();
275
276            throw new AuthenticationException(
277                "Connection aborted - failed to write to known_hosts. Reason: " + e.getMessage(), e );
278        }
279    }
280
281    public void closeConnection()
282    {
283        if ( session != null )
284        {
285            session.disconnect();
286            session = null;
287        }
288    }
289
290    public Streams executeCommand( String command, boolean ignoreFailures )
291        throws CommandExecutionException
292    {
293        ChannelExec channel = null;
294        BufferedReader stdoutReader = null;
295        BufferedReader stderrReader = null;
296        try
297        {
298            channel = (ChannelExec) session.openChannel( EXEC_CHANNEL );
299
300            channel.setCommand( command + "\n" );
301
302            InputStream stdout = channel.getInputStream();
303            InputStream stderr = channel.getErrStream();
304
305            channel.connect();
306
307            stdoutReader = new BufferedReader( new InputStreamReader( stdout ) );
308            stderrReader = new BufferedReader( new InputStreamReader( stderr ) );
309
310            Streams streams = CommandExecutorStreamProcessor.processStreams( stderrReader, stdoutReader );
311
312            if ( streams.getErr().length() > 0 && !ignoreFailures )
313            {
314                int exitCode = channel.getExitStatus();
315                throw new CommandExecutionException( "Exit code: " + exitCode + " - " + streams.getErr() );
316            }
317
318            return streams;
319        }
320        catch ( IOException e )
321        {
322            throw new CommandExecutionException( "Cannot execute remote command: " + command, e );
323        }
324        catch ( JSchException e )
325        {
326            throw new CommandExecutionException( "Cannot execute remote command: " + command, e );
327        }
328        finally
329        {
330            IOUtil.close( stdoutReader );
331            IOUtil.close( stderrReader );
332            if ( channel != null )
333            {
334                channel.disconnect();
335            }
336        }
337    }
338
339    protected void handleGetException( Resource resource, Exception e )
340        throws TransferFailedException
341    {
342        fireTransferError( resource, e, TransferEvent.REQUEST_GET );
343
344        String msg =
345            "Error occurred while downloading '" + resource + "' from the remote repository:" + getRepository() + ": "
346                + e.getMessage();
347
348        throw new TransferFailedException( msg, e );
349    }
350
351    public List<String> getFileList( String destinationDirectory )
352        throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
353    {
354        return sshTool.getFileList( destinationDirectory, repository );
355    }
356
357    public void putDirectory( File sourceDirectory, String destinationDirectory )
358        throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
359    {
360        sshTool.putDirectory( this, sourceDirectory, destinationDirectory );
361    }
362
363    public boolean resourceExists( String resourceName )
364        throws TransferFailedException, AuthorizationException
365    {
366        return sshTool.resourceExists( resourceName, repository );
367    }
368
369    public boolean supportsDirectoryCopy()
370    {
371        return true;
372    }
373
374    public void executeCommand( String command )
375        throws CommandExecutionException
376    {
377        fireTransferDebug( "Executing command: " + command );
378
379        executeCommand( command, false );
380    }
381
382    public InteractiveUserInfo getInteractiveUserInfo()
383    {
384        return this.interactiveUserInfo;
385    }
386
387    public KnownHostsProvider getKnownHostsProvider()
388    {
389        return this.knownHostsProvider;
390    }
391
392    public void setInteractiveUserInfo( InteractiveUserInfo interactiveUserInfo )
393    {
394        this.interactiveUserInfo = interactiveUserInfo;
395    }
396
397    public void setKnownHostsProvider( KnownHostsProvider knownHostsProvider )
398    {
399        this.knownHostsProvider = knownHostsProvider;
400    }
401}