View Javadoc

1   package org.apache.maven.wagon.providers.ssh.external;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.File;
23  import java.io.FileNotFoundException;
24  import java.util.List;
25  import java.util.Locale;
26  
27  import org.apache.maven.wagon.AbstractWagon;
28  import org.apache.maven.wagon.CommandExecutionException;
29  import org.apache.maven.wagon.CommandExecutor;
30  import org.apache.maven.wagon.PathUtils;
31  import org.apache.maven.wagon.PermissionModeUtils;
32  import org.apache.maven.wagon.ResourceDoesNotExistException;
33  import org.apache.maven.wagon.Streams;
34  import org.apache.maven.wagon.TransferFailedException;
35  import org.apache.maven.wagon.WagonConstants;
36  import org.apache.maven.wagon.authentication.AuthenticationException;
37  import org.apache.maven.wagon.authentication.AuthenticationInfo;
38  import org.apache.maven.wagon.authorization.AuthorizationException;
39  import org.apache.maven.wagon.events.TransferEvent;
40  import org.apache.maven.wagon.providers.ssh.ScpHelper;
41  import org.apache.maven.wagon.repository.RepositoryPermissions;
42  import org.apache.maven.wagon.resource.Resource;
43  import org.codehaus.plexus.util.StringUtils;
44  import org.codehaus.plexus.util.cli.CommandLineException;
45  import org.codehaus.plexus.util.cli.CommandLineUtils;
46  import org.codehaus.plexus.util.cli.Commandline;
47  
48  /**
49   * SCP deployer using "external" scp program.  To allow for
50   * ssh-agent type behavior, until we can construct a Java SSH Agent and interface for JSch.
51   *
52   * @author <a href="mailto:brett@apache.org">Brett Porter</a>
53   * @version $Id:ScpExternalWagon.java 477260 2006-11-20 17:11:39Z brett $
54   * @todo [BP] add compression flag
55   * 
56   * @plexus.component role="org.apache.maven.wagon.Wagon" 
57   *   role-hint="scpexe"
58   *   instantiation-strategy="per-lookup"
59   */
60  public class ScpExternalWagon
61      extends AbstractWagon
62      implements CommandExecutor
63  {
64      /**
65       * The external SCP command to use - default is <code>scp</code>.
66       *
67       * @component.configuration default="scp"
68       */
69      private String scpExecutable = "scp";
70  
71      /**
72       * The external SSH command to use - default is <code>ssh</code>.
73       *
74       * @component.configuration default="ssh"
75       */
76      private String sshExecutable = "ssh";
77  
78      /**
79       * Arguments to pass to the SCP command.
80       *
81       * @component.configuration
82       */
83      private String scpArgs;
84  
85      /**
86       * Arguments to pass to the SSH command.
87       *
88       * @component.configuration
89       */
90      private String sshArgs;
91  
92      private ScpHelper sshTool = new ScpHelper( this );
93  
94      private static final int SSH_FATAL_EXIT_CODE = 255;
95  
96      // ----------------------------------------------------------------------
97      //
98      // ----------------------------------------------------------------------
99  
100     protected void openConnectionInternal()
101         throws AuthenticationException
102     {
103         if ( authenticationInfo == null )
104         {
105             authenticationInfo = new AuthenticationInfo();
106         }
107     }
108 
109     public void closeConnection()
110     {
111         // nothing to disconnect
112     }
113 
114     public boolean getIfNewer( String resourceName, File destination, long timestamp )
115         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
116     {
117         fireSessionDebug( "getIfNewer in SCP wagon is not supported - performing an unconditional get" );
118         get( resourceName, destination );
119         return true;
120     }
121 
122     /**
123      * @return The hostname of the remote server prefixed with the username, which comes either from the repository URL
124      *         or from the authenticationInfo.
125      */
126     private String buildRemoteHost()
127     {
128         String username = this.getRepository().getUsername();
129         if ( username == null )
130         {
131             username = authenticationInfo.getUserName();
132         }
133         
134         if ( username == null )
135         {
136             return getRepository().getHost();
137         }
138         else
139         {
140             return username + "@" + getRepository().getHost();
141         }
142     }
143     
144     public void executeCommand( String command )
145         throws CommandExecutionException
146     {
147         fireTransferDebug( "Executing command: " + command );
148 
149         executeCommand( command, false );
150     }
151 
152     public Streams executeCommand( String command, boolean ignoreFailures )
153         throws CommandExecutionException
154     {
155         boolean putty = isPuTTY();
156 
157         File privateKey;
158         try
159         {
160             privateKey = ScpHelper.getPrivateKey( authenticationInfo );
161         }
162         catch ( FileNotFoundException e )
163         {
164             throw new CommandExecutionException( e.getMessage() );
165         }
166         Commandline cl = createBaseCommandLine( putty, sshExecutable, privateKey );
167 
168         int port =
169             repository.getPort() == WagonConstants.UNKNOWN_PORT ? ScpHelper.DEFAULT_SSH_PORT : repository.getPort();
170         if ( port != ScpHelper.DEFAULT_SSH_PORT )
171         {
172             if ( putty )
173             {
174                 cl.createArgument().setLine( "-P " + port );
175             }
176             else
177             {
178                 cl.createArgument().setLine( "-p " + port );
179             }
180         }
181 
182         if ( sshArgs != null )
183         {
184             cl.createArgument().setLine( sshArgs );
185         }
186         
187         String remoteHost = this.buildRemoteHost();
188 
189         cl.createArgument().setValue( remoteHost );
190 
191         cl.createArgument().setValue( command );
192 
193         fireSessionDebug( "Executing command: " + cl.toString() );
194 
195         try
196         {
197             CommandLineUtils.StringStreamConsumer out = new CommandLineUtils.StringStreamConsumer();
198             CommandLineUtils.StringStreamConsumer err = new CommandLineUtils.StringStreamConsumer();
199             int exitCode = CommandLineUtils.executeCommandLine( cl, out, err );
200             Streams streams = new Streams();
201             streams.setOut( out.getOutput() );
202             streams.setErr( err.getOutput() );
203             fireSessionDebug( streams.getOut() );
204             fireSessionDebug( streams.getErr() );
205             if ( exitCode != 0 )
206             {
207                 if ( !ignoreFailures || exitCode == SSH_FATAL_EXIT_CODE )
208                 {
209                     throw new CommandExecutionException( "Exit code " + exitCode + " - " + err.getOutput() );
210                 }
211             }
212             return streams;
213         }
214         catch ( CommandLineException e )
215         {
216             throw new CommandExecutionException( "Error executing command line", e );
217         }
218     }
219 
220     protected boolean isPuTTY()
221     {
222         return sshExecutable.toLowerCase( Locale.ENGLISH ).indexOf( "plink" ) >= 0;
223     }
224 
225     private Commandline createBaseCommandLine( boolean putty, String executable, File privateKey )
226     {
227         Commandline cl = new Commandline();
228 
229         cl.setExecutable( executable );
230 
231         if ( privateKey != null )
232         {
233             cl.createArgument().setValue( "-i" );
234             cl.createArgument().setFile( privateKey );
235         }
236 
237         String password = authenticationInfo.getPassword();
238         if ( putty && password != null )
239         {
240             cl.createArgument().setValue( "-pw" );
241             cl.createArgument().setValue( password );
242         }
243 
244         // should check interactive flag, but scpexe never works in interactive mode right now due to i/o streams
245         if ( putty )
246         {
247             cl.createArgument().setValue( "-batch" );
248         }
249         else
250         {
251             cl.createArgument().setValue( "-o" );
252             cl.createArgument().setValue( "BatchMode yes" );
253         }
254         return cl;
255     }
256 
257 
258     private void executeScpCommand( Resource resource, File localFile, boolean put )
259         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
260     {
261         boolean putty = isPuTTYSCP();
262 
263         File privateKey;
264         try
265         {
266             privateKey = ScpHelper.getPrivateKey( authenticationInfo );
267         }
268         catch ( FileNotFoundException e )
269         {
270             fireSessionConnectionRefused();
271             
272             throw new AuthorizationException( e.getMessage() );
273         }
274         Commandline cl = createBaseCommandLine( putty, scpExecutable, privateKey );
275 
276         cl.setWorkingDirectory( localFile.getParentFile().getAbsolutePath() );
277 
278         int port =
279             repository.getPort() == WagonConstants.UNKNOWN_PORT ? ScpHelper.DEFAULT_SSH_PORT : repository.getPort();
280         if ( port != ScpHelper.DEFAULT_SSH_PORT )
281         {
282             cl.createArgument().setLine( "-P " + port );
283         }
284 
285         if ( scpArgs != null )
286         {
287             cl.createArgument().setLine( scpArgs );
288         }
289         
290         String resourceName = normalizeResource( resource );
291         String remoteFile = getRepository().getBasedir() + "/" + resourceName;
292         
293         remoteFile = StringUtils.replace( remoteFile, " ", "\\ " );
294         
295         String qualifiedRemoteFile = this.buildRemoteHost() + ":" + remoteFile;
296         if ( put )
297         {
298             cl.createArgument().setValue( localFile.getName() );
299             cl.createArgument().setValue( qualifiedRemoteFile );
300         }
301         else
302         {
303             cl.createArgument().setValue( qualifiedRemoteFile );
304             cl.createArgument().setValue( localFile.getName() );
305         }
306 
307         fireSessionDebug( "Executing command: " + cl.toString() );
308 
309         try
310         {
311             CommandLineUtils.StringStreamConsumer err = new CommandLineUtils.StringStreamConsumer();
312             int exitCode = CommandLineUtils.executeCommandLine( cl, null, err );
313             if ( exitCode != 0 )
314             {
315                 if ( !put && 
316                     err.getOutput().trim().toLowerCase( Locale.ENGLISH ).indexOf( "no such file or directory" ) != -1 )
317                 {
318                     throw new ResourceDoesNotExistException( err.getOutput() );
319                 }
320                 else
321                 {
322                     TransferFailedException e = new TransferFailedException( "Exit code: " + exitCode + " - "
323                                                                              + err.getOutput() );
324 
325                     fireTransferError( resource, e, put ? TransferEvent.REQUEST_PUT : TransferEvent.REQUEST_GET );
326 
327                     throw e;
328                 }
329             }
330         }
331         catch ( CommandLineException e )
332         {
333             fireTransferError( resource, e, put ? TransferEvent.REQUEST_PUT : TransferEvent.REQUEST_GET );
334 
335             throw new TransferFailedException( "Error executing command line", e );
336         }
337     }
338 
339     boolean isPuTTYSCP()
340     {
341         return scpExecutable.toLowerCase( Locale.ENGLISH ).indexOf( "pscp" ) >= 0;
342     }
343 
344     private String normalizeResource( Resource resource )
345     {
346         return StringUtils.replace( resource.getName(), "\\", "/" );
347     }
348 
349     public void put( File source, String destination )
350         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
351     {
352         Resource resource = new Resource( destination );
353 
354         firePutInitiated( resource, source );
355 
356         if ( !source.exists() )
357         {
358             throw new ResourceDoesNotExistException( "Specified source file does not exist: " + source );
359         }
360 
361         String basedir = getRepository().getBasedir();
362 
363         String resourceName = StringUtils.replace( destination, "\\", "/" );
364 
365         String dir = PathUtils.dirname( resourceName );
366 
367         dir = StringUtils.replace( dir, "\\", "/" );
368 
369         String umaskCmd = null;
370         if ( getRepository().getPermissions() != null )
371         {
372             String dirPerms = getRepository().getPermissions().getDirectoryMode();
373 
374             if ( dirPerms != null )
375             {
376                 umaskCmd = "umask " + PermissionModeUtils.getUserMaskFor( dirPerms );
377             }
378         }
379 
380         String mkdirCmd = "mkdir -p " + basedir + "/" + dir + "\n";
381 
382         if ( umaskCmd != null )
383         {
384             mkdirCmd = umaskCmd + "; " + mkdirCmd;
385         }
386 
387         try
388         {
389             executeCommand( mkdirCmd );
390         }
391         catch ( CommandExecutionException e )
392         {
393             fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
394             
395             throw new TransferFailedException( "Error executing command for transfer", e );
396         }
397         
398         resource.setContentLength( source.length() );
399         
400         resource.setLastModified( source.lastModified() );
401 
402         firePutStarted( resource, source );
403 
404         executeScpCommand( resource, source, true );
405 
406         postProcessListeners( resource, source, TransferEvent.REQUEST_PUT );
407 
408         try
409         {
410             RepositoryPermissions permissions = getRepository().getPermissions();
411 
412             if ( permissions != null && permissions.getGroup() != null )
413             {
414                 executeCommand( "chgrp -f " + permissions.getGroup() + " " + basedir + "/" + resourceName + "\n",
415                                 true );
416             }
417 
418             if ( permissions != null && permissions.getFileMode() != null )
419             {
420                 executeCommand( "chmod -f " + permissions.getFileMode() + " " + basedir + "/" + resourceName + "\n",
421                                 true );
422             }
423         }
424         catch ( CommandExecutionException e )
425         {
426             fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
427 
428             throw new TransferFailedException( "Error executing command for transfer", e );
429         }
430         firePutCompleted( resource, source );
431     }
432 
433     public void get( String resourceName, File destination )
434         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
435     {
436         String path = StringUtils.replace( resourceName, "\\", "/" );
437 
438         Resource resource = new Resource( path );
439 
440         fireGetInitiated( resource, destination );
441         
442         createParentDirectories( destination );
443 
444         fireGetStarted( resource, destination );
445 
446         executeScpCommand( resource, destination, false );
447 
448         postProcessListeners( resource, destination, TransferEvent.REQUEST_GET );
449 
450         fireGetCompleted( resource, destination );
451     }
452 
453     //
454     // these parameters are user specific, so should not be read from the repository itself.
455     // They can be configured by plexus, or directly on the instantiated object.
456     // Alternatively, we may later accept a generic parameters argument to connect, or some other configure(Properties)
457     // method on a Wagon.
458     //
459 
460     public List getFileList( String destinationDirectory )
461         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
462     {
463         return sshTool.getFileList( destinationDirectory, repository );
464     }
465 
466     public void putDirectory( File sourceDirectory, String destinationDirectory )
467         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
468     {
469         sshTool.putDirectory( this, sourceDirectory, destinationDirectory );
470     }
471 
472     public boolean resourceExists( String resourceName )
473         throws TransferFailedException, AuthorizationException
474     {
475         return sshTool.resourceExists( resourceName, repository );
476     }
477 
478     public boolean supportsDirectoryCopy()
479     {
480         return true;
481     }
482 
483     public String getScpExecutable()
484     {
485         return scpExecutable;
486     }
487 
488     public void setScpExecutable( String scpExecutable )
489     {
490         this.scpExecutable = scpExecutable;
491     }
492 
493     public String getSshExecutable()
494     {
495         return sshExecutable;
496     }
497 
498     public void setSshExecutable( String sshExecutable )
499     {
500         this.sshExecutable = sshExecutable;
501     }
502 
503     public String getScpArgs()
504     {
505         return scpArgs;
506     }
507 
508     public void setScpArgs( String scpArgs )
509     {
510         this.scpArgs = scpArgs;
511     }
512 
513     public String getSshArgs()
514     {
515         return sshArgs;
516     }
517 
518     public void setSshArgs( String sshArgs )
519     {
520         this.sshArgs = sshArgs;
521     }
522 }