001package org.apache.maven.wagon.providers.ssh.external; 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 org.apache.maven.wagon.AbstractWagon; 023import org.apache.maven.wagon.CommandExecutionException; 024import org.apache.maven.wagon.CommandExecutor; 025import org.apache.maven.wagon.PathUtils; 026import org.apache.maven.wagon.PermissionModeUtils; 027import org.apache.maven.wagon.ResourceDoesNotExistException; 028import org.apache.maven.wagon.Streams; 029import org.apache.maven.wagon.TransferFailedException; 030import org.apache.maven.wagon.WagonConstants; 031import org.apache.maven.wagon.authentication.AuthenticationException; 032import org.apache.maven.wagon.authentication.AuthenticationInfo; 033import org.apache.maven.wagon.authorization.AuthorizationException; 034import org.apache.maven.wagon.events.TransferEvent; 035import org.apache.maven.wagon.providers.ssh.ScpHelper; 036import org.apache.maven.wagon.repository.RepositoryPermissions; 037import org.apache.maven.wagon.resource.Resource; 038import org.codehaus.plexus.util.StringUtils; 039import org.codehaus.plexus.util.cli.CommandLineException; 040import org.codehaus.plexus.util.cli.CommandLineUtils; 041import org.codehaus.plexus.util.cli.Commandline; 042 043import java.io.File; 044import java.io.FileNotFoundException; 045import java.util.List; 046import java.util.Locale; 047 048/** 049 * SCP deployer using "external" scp program. To allow for 050 * ssh-agent type behavior, until we can construct a Java SSH Agent and interface for JSch. 051 * 052 * @author <a href="mailto:brett@apache.org">Brett Porter</a> 053 * @todo [BP] add compression flag 054 * @plexus.component role="org.apache.maven.wagon.Wagon" 055 * role-hint="scpexe" 056 * instantiation-strategy="per-lookup" 057 */ 058public class ScpExternalWagon 059 extends AbstractWagon 060 implements CommandExecutor 061{ 062 /** 063 * The external SCP command to use - default is <code>scp</code>. 064 * 065 * @component.configuration default="scp" 066 */ 067 private String scpExecutable = "scp"; 068 069 /** 070 * The external SSH command to use - default is <code>ssh</code>. 071 * 072 * @component.configuration default="ssh" 073 */ 074 private String sshExecutable = "ssh"; 075 076 /** 077 * Arguments to pass to the SCP command. 078 * 079 * @component.configuration 080 */ 081 private String scpArgs; 082 083 /** 084 * Arguments to pass to the SSH command. 085 * 086 * @component.configuration 087 */ 088 private String sshArgs; 089 090 private ScpHelper sshTool = new ScpHelper( this ); 091 092 private static final int SSH_FATAL_EXIT_CODE = 255; 093 094 // ---------------------------------------------------------------------- 095 // 096 // ---------------------------------------------------------------------- 097 098 protected void openConnectionInternal() 099 throws AuthenticationException 100 { 101 if ( authenticationInfo == null ) 102 { 103 authenticationInfo = new AuthenticationInfo(); 104 } 105 } 106 107 public void closeConnection() 108 { 109 // nothing to disconnect 110 } 111 112 public boolean getIfNewer( String resourceName, File destination, long timestamp ) 113 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 114 { 115 fireSessionDebug( "getIfNewer in SCP wagon is not supported - performing an unconditional get" ); 116 get( resourceName, destination ); 117 return true; 118 } 119 120 /** 121 * @return The hostname of the remote server prefixed with the username, which comes either from the repository URL 122 * or from the authenticationInfo. 123 */ 124 private String buildRemoteHost() 125 { 126 String username = this.getRepository().getUsername(); 127 if ( username == null ) 128 { 129 username = authenticationInfo.getUserName(); 130 } 131 132 if ( username == null ) 133 { 134 return getRepository().getHost(); 135 } 136 else 137 { 138 return username + "@" + getRepository().getHost(); 139 } 140 } 141 142 public void executeCommand( String command ) 143 throws CommandExecutionException 144 { 145 fireTransferDebug( "Executing command: " + command ); 146 147 executeCommand( command, false ); 148 } 149 150 public Streams executeCommand( String command, boolean ignoreFailures ) 151 throws CommandExecutionException 152 { 153 boolean putty = isPuTTY(); 154 155 File privateKey; 156 try 157 { 158 privateKey = ScpHelper.getPrivateKey( authenticationInfo ); 159 } 160 catch ( FileNotFoundException e ) 161 { 162 throw new CommandExecutionException( e.getMessage(), e ); 163 } 164 Commandline cl = createBaseCommandLine( putty, sshExecutable, privateKey ); 165 166 int port = 167 repository.getPort() == WagonConstants.UNKNOWN_PORT ? ScpHelper.DEFAULT_SSH_PORT : repository.getPort(); 168 if ( port != ScpHelper.DEFAULT_SSH_PORT ) 169 { 170 if ( putty ) 171 { 172 cl.createArg().setLine( "-P " + port ); 173 } 174 else 175 { 176 cl.createArg().setLine( "-p " + port ); 177 } 178 } 179 180 if ( sshArgs != null ) 181 { 182 cl.createArg().setLine( sshArgs ); 183 } 184 185 String remoteHost = this.buildRemoteHost(); 186 187 cl.createArg().setValue( remoteHost ); 188 189 cl.createArg().setValue( command ); 190 191 fireSessionDebug( "Executing command: " + cl.toString() ); 192 193 try 194 { 195 CommandLineUtils.StringStreamConsumer out = new CommandLineUtils.StringStreamConsumer(); 196 CommandLineUtils.StringStreamConsumer err = new CommandLineUtils.StringStreamConsumer(); 197 int exitCode = CommandLineUtils.executeCommandLine( cl, out, err ); 198 Streams streams = new Streams(); 199 streams.setOut( out.getOutput() ); 200 streams.setErr( err.getOutput() ); 201 fireSessionDebug( streams.getOut() ); 202 fireSessionDebug( streams.getErr() ); 203 if ( exitCode != 0 ) 204 { 205 if ( !ignoreFailures || exitCode == SSH_FATAL_EXIT_CODE ) 206 { 207 throw new CommandExecutionException( "Exit code " + exitCode + " - " + err.getOutput() ); 208 } 209 } 210 return streams; 211 } 212 catch ( CommandLineException e ) 213 { 214 throw new CommandExecutionException( "Error executing command line", e ); 215 } 216 } 217 218 protected boolean isPuTTY() 219 { 220 return sshExecutable.toLowerCase( Locale.ENGLISH ).contains( "plink" ); 221 } 222 223 private Commandline createBaseCommandLine( boolean putty, String executable, File privateKey ) 224 { 225 Commandline cl = new Commandline(); 226 227 cl.setExecutable( executable ); 228 229 if ( privateKey != null ) 230 { 231 cl.createArg().setValue( "-i" ); 232 cl.createArg().setFile( privateKey ); 233 } 234 235 String password = authenticationInfo.getPassword(); 236 if ( putty && password != null ) 237 { 238 cl.createArg().setValue( "-pw" ); 239 cl.createArg().setValue( password ); 240 } 241 242 // should check interactive flag, but scpexe never works in interactive mode right now due to i/o streams 243 if ( putty ) 244 { 245 cl.createArg().setValue( "-batch" ); 246 } 247 else 248 { 249 cl.createArg().setValue( "-o" ); 250 cl.createArg().setValue( "BatchMode yes" ); 251 } 252 return cl; 253 } 254 255 256 private void executeScpCommand( Resource resource, File localFile, boolean put ) 257 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 258 { 259 boolean putty = isPuTTYSCP(); 260 261 File privateKey; 262 try 263 { 264 privateKey = ScpHelper.getPrivateKey( authenticationInfo ); 265 } 266 catch ( FileNotFoundException e ) 267 { 268 fireSessionConnectionRefused(); 269 270 throw new AuthorizationException( e.getMessage() ); 271 } 272 Commandline cl = createBaseCommandLine( putty, scpExecutable, privateKey ); 273 274 cl.setWorkingDirectory( localFile.getParentFile().getAbsolutePath() ); 275 276 int port = 277 repository.getPort() == WagonConstants.UNKNOWN_PORT ? ScpHelper.DEFAULT_SSH_PORT : repository.getPort(); 278 if ( port != ScpHelper.DEFAULT_SSH_PORT ) 279 { 280 cl.createArg().setLine( "-P " + port ); 281 } 282 283 if ( scpArgs != null ) 284 { 285 cl.createArg().setLine( scpArgs ); 286 } 287 288 String resourceName = normalizeResource( resource ); 289 String remoteFile = getRepository().getBasedir() + "/" + resourceName; 290 291 remoteFile = StringUtils.replace( remoteFile, " ", "\\ " ); 292 293 String qualifiedRemoteFile = this.buildRemoteHost() + ":" + remoteFile; 294 if ( put ) 295 { 296 cl.createArg().setValue( localFile.getName() ); 297 cl.createArg().setValue( qualifiedRemoteFile ); 298 } 299 else 300 { 301 cl.createArg().setValue( qualifiedRemoteFile ); 302 cl.createArg().setValue( localFile.getName() ); 303 } 304 305 fireSessionDebug( "Executing command: " + cl.toString() ); 306 307 try 308 { 309 CommandLineUtils.StringStreamConsumer err = new CommandLineUtils.StringStreamConsumer(); 310 int exitCode = CommandLineUtils.executeCommandLine( cl, null, err ); 311 if ( exitCode != 0 ) 312 { 313 if ( !put && err.getOutput().trim().toLowerCase( Locale.ENGLISH ).contains( "no such file or directory" )) 314 { 315 throw new ResourceDoesNotExistException( err.getOutput() ); 316 } 317 else 318 { 319 TransferFailedException e = 320 new TransferFailedException( "Exit code: " + exitCode + " - " + err.getOutput() ); 321 322 fireTransferError( resource, e, put ? TransferEvent.REQUEST_PUT : TransferEvent.REQUEST_GET ); 323 324 throw e; 325 } 326 } 327 } 328 catch ( CommandLineException e ) 329 { 330 fireTransferError( resource, e, put ? TransferEvent.REQUEST_PUT : TransferEvent.REQUEST_GET ); 331 332 throw new TransferFailedException( "Error executing command line", e ); 333 } 334 } 335 336 boolean isPuTTYSCP() 337 { 338 return scpExecutable.toLowerCase( Locale.ENGLISH ).contains( "pscp" ); 339 } 340 341 private String normalizeResource( Resource resource ) 342 { 343 return StringUtils.replace( resource.getName(), "\\", "/" ); 344 } 345 346 public void put( File source, String destination ) 347 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 348 { 349 Resource resource = new Resource( destination ); 350 351 firePutInitiated( resource, source ); 352 353 if ( !source.exists() ) 354 { 355 throw new ResourceDoesNotExistException( "Specified source file does not exist: " + source ); 356 } 357 358 String basedir = getRepository().getBasedir(); 359 360 String resourceName = StringUtils.replace( destination, "\\", "/" ); 361 362 String dir = PathUtils.dirname( resourceName ); 363 364 dir = StringUtils.replace( dir, "\\", "/" ); 365 366 String umaskCmd = null; 367 if ( getRepository().getPermissions() != null ) 368 { 369 String dirPerms = getRepository().getPermissions().getDirectoryMode(); 370 371 if ( dirPerms != null ) 372 { 373 umaskCmd = "umask " + PermissionModeUtils.getUserMaskFor( dirPerms ); 374 } 375 } 376 377 String mkdirCmd = "mkdir -p " + basedir + "/" + dir + "\n"; 378 379 if ( umaskCmd != null ) 380 { 381 mkdirCmd = umaskCmd + "; " + mkdirCmd; 382 } 383 384 try 385 { 386 executeCommand( mkdirCmd ); 387 } 388 catch ( CommandExecutionException e ) 389 { 390 fireTransferError( resource, e, TransferEvent.REQUEST_PUT ); 391 392 throw new TransferFailedException( "Error executing command for transfer", e ); 393 } 394 395 resource.setContentLength( source.length() ); 396 397 resource.setLastModified( source.lastModified() ); 398 399 firePutStarted( resource, source ); 400 401 executeScpCommand( resource, source, true ); 402 403 postProcessListeners( resource, source, TransferEvent.REQUEST_PUT ); 404 405 try 406 { 407 RepositoryPermissions permissions = getRepository().getPermissions(); 408 409 if ( permissions != null && permissions.getGroup() != null ) 410 { 411 executeCommand( "chgrp -f " + permissions.getGroup() + " " + basedir + "/" + resourceName + "\n", 412 true ); 413 } 414 415 if ( permissions != null && permissions.getFileMode() != null ) 416 { 417 executeCommand( "chmod -f " + permissions.getFileMode() + " " + basedir + "/" + resourceName + "\n", 418 true ); 419 } 420 } 421 catch ( CommandExecutionException e ) 422 { 423 fireTransferError( resource, e, TransferEvent.REQUEST_PUT ); 424 425 throw new TransferFailedException( "Error executing command for transfer", e ); 426 } 427 firePutCompleted( resource, source ); 428 } 429 430 public void get( String resourceName, File destination ) 431 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 432 { 433 String path = StringUtils.replace( resourceName, "\\", "/" ); 434 435 Resource resource = new Resource( path ); 436 437 fireGetInitiated( resource, destination ); 438 439 createParentDirectories( destination ); 440 441 fireGetStarted( resource, destination ); 442 443 executeScpCommand( resource, destination, false ); 444 445 postProcessListeners( resource, destination, TransferEvent.REQUEST_GET ); 446 447 fireGetCompleted( resource, destination ); 448 } 449 450 // 451 // these parameters are user specific, so should not be read from the repository itself. 452 // They can be configured by plexus, or directly on the instantiated object. 453 // Alternatively, we may later accept a generic parameters argument to connect, or some other configure(Properties) 454 // method on a Wagon. 455 // 456 457 public List<String> getFileList( String destinationDirectory ) 458 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 459 { 460 return sshTool.getFileList( destinationDirectory, repository ); 461 } 462 463 public void putDirectory( File sourceDirectory, String destinationDirectory ) 464 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 465 { 466 sshTool.putDirectory( this, sourceDirectory, destinationDirectory ); 467 } 468 469 public boolean resourceExists( String resourceName ) 470 throws TransferFailedException, AuthorizationException 471 { 472 return sshTool.resourceExists( resourceName, repository ); 473 } 474 475 public boolean supportsDirectoryCopy() 476 { 477 return true; 478 } 479 480 public String getScpExecutable() 481 { 482 return scpExecutable; 483 } 484 485 public void setScpExecutable( String scpExecutable ) 486 { 487 this.scpExecutable = scpExecutable; 488 } 489 490 public String getSshExecutable() 491 { 492 return sshExecutable; 493 } 494 495 public void setSshExecutable( String sshExecutable ) 496 { 497 this.sshExecutable = sshExecutable; 498 } 499 500 public String getScpArgs() 501 { 502 return scpArgs; 503 } 504 505 public void setScpArgs( String scpArgs ) 506 { 507 this.scpArgs = scpArgs; 508 } 509 510 public String getSshArgs() 511 { 512 return sshArgs; 513 } 514 515 public void setSshArgs( String sshArgs ) 516 { 517 this.sshArgs = sshArgs; 518 } 519}