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 314 && err.getOutput().trim().toLowerCase( Locale.ENGLISH ).contains( "no such file or directory" ) ) 315 { 316 throw new ResourceDoesNotExistException( err.getOutput() ); 317 } 318 else 319 { 320 TransferFailedException e = 321 new TransferFailedException( "Exit code: " + exitCode + " - " + err.getOutput() ); 322 323 fireTransferError( resource, e, put ? TransferEvent.REQUEST_PUT : TransferEvent.REQUEST_GET ); 324 325 throw e; 326 } 327 } 328 } 329 catch ( CommandLineException e ) 330 { 331 fireTransferError( resource, e, put ? TransferEvent.REQUEST_PUT : TransferEvent.REQUEST_GET ); 332 333 throw new TransferFailedException( "Error executing command line", e ); 334 } 335 } 336 337 boolean isPuTTYSCP() 338 { 339 return scpExecutable.toLowerCase( Locale.ENGLISH ).contains( "pscp" ); 340 } 341 342 private String normalizeResource( Resource resource ) 343 { 344 return StringUtils.replace( resource.getName(), "\\", "/" ); 345 } 346 347 public void put( File source, String destination ) 348 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 349 { 350 Resource resource = new Resource( destination ); 351 352 firePutInitiated( resource, source ); 353 354 if ( !source.exists() ) 355 { 356 throw new ResourceDoesNotExistException( "Specified source file does not exist: " + source ); 357 } 358 359 String basedir = getRepository().getBasedir(); 360 361 String resourceName = StringUtils.replace( destination, "\\", "/" ); 362 363 String dir = PathUtils.dirname( resourceName ); 364 365 dir = StringUtils.replace( dir, "\\", "/" ); 366 367 String umaskCmd = null; 368 if ( getRepository().getPermissions() != null ) 369 { 370 String dirPerms = getRepository().getPermissions().getDirectoryMode(); 371 372 if ( dirPerms != null ) 373 { 374 umaskCmd = "umask " + PermissionModeUtils.getUserMaskFor( dirPerms ); 375 } 376 } 377 378 String mkdirCmd = "mkdir -p " + basedir + "/" + dir + "\n"; 379 380 if ( umaskCmd != null ) 381 { 382 mkdirCmd = umaskCmd + "; " + mkdirCmd; 383 } 384 385 try 386 { 387 executeCommand( mkdirCmd ); 388 } 389 catch ( CommandExecutionException e ) 390 { 391 fireTransferError( resource, e, TransferEvent.REQUEST_PUT ); 392 393 throw new TransferFailedException( "Error executing command for transfer", e ); 394 } 395 396 resource.setContentLength( source.length() ); 397 398 resource.setLastModified( source.lastModified() ); 399 400 firePutStarted( resource, source ); 401 402 executeScpCommand( resource, source, true ); 403 404 postProcessListeners( resource, source, TransferEvent.REQUEST_PUT ); 405 406 try 407 { 408 RepositoryPermissions permissions = getRepository().getPermissions(); 409 410 if ( permissions != null && permissions.getGroup() != null ) 411 { 412 executeCommand( "chgrp -f " + permissions.getGroup() + " " + basedir + "/" + resourceName + "\n", 413 true ); 414 } 415 416 if ( permissions != null && permissions.getFileMode() != null ) 417 { 418 executeCommand( "chmod -f " + permissions.getFileMode() + " " + basedir + "/" + resourceName + "\n", 419 true ); 420 } 421 } 422 catch ( CommandExecutionException e ) 423 { 424 fireTransferError( resource, e, TransferEvent.REQUEST_PUT ); 425 426 throw new TransferFailedException( "Error executing command for transfer", e ); 427 } 428 firePutCompleted( resource, source ); 429 } 430 431 public void get( String resourceName, File destination ) 432 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 433 { 434 String path = StringUtils.replace( resourceName, "\\", "/" ); 435 436 Resource resource = new Resource( path ); 437 438 fireGetInitiated( resource, destination ); 439 440 createParentDirectories( destination ); 441 442 fireGetStarted( resource, destination ); 443 444 executeScpCommand( resource, destination, false ); 445 446 postProcessListeners( resource, destination, TransferEvent.REQUEST_GET ); 447 448 fireGetCompleted( resource, destination ); 449 } 450 451 // 452 // these parameters are user specific, so should not be read from the repository itself. 453 // They can be configured by plexus, or directly on the instantiated object. 454 // Alternatively, we may later accept a generic parameters argument to connect, or some other configure(Properties) 455 // method on a Wagon. 456 // 457 458 public List<String> getFileList( String destinationDirectory ) 459 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 460 { 461 return sshTool.getFileList( destinationDirectory, repository ); 462 } 463 464 public void putDirectory( File sourceDirectory, String destinationDirectory ) 465 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 466 { 467 sshTool.putDirectory( this, sourceDirectory, destinationDirectory ); 468 } 469 470 public boolean resourceExists( String resourceName ) 471 throws TransferFailedException, AuthorizationException 472 { 473 return sshTool.resourceExists( resourceName, repository ); 474 } 475 476 public boolean supportsDirectoryCopy() 477 { 478 return true; 479 } 480 481 public String getScpExecutable() 482 { 483 return scpExecutable; 484 } 485 486 public void setScpExecutable( String scpExecutable ) 487 { 488 this.scpExecutable = scpExecutable; 489 } 490 491 public String getSshExecutable() 492 { 493 return sshExecutable; 494 } 495 496 public void setSshExecutable( String sshExecutable ) 497 { 498 this.sshExecutable = sshExecutable; 499 } 500 501 public String getScpArgs() 502 { 503 return scpArgs; 504 } 505 506 public void setScpArgs( String scpArgs ) 507 { 508 this.scpArgs = scpArgs; 509 } 510 511 public String getSshArgs() 512 { 513 return sshArgs; 514 } 515 516 public void setSshArgs( String sshArgs ) 517 { 518 this.sshArgs = sshArgs; 519 } 520}