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 String exe = sshExecutable.toLowerCase( Locale.ENGLISH ); 221 return exe.contains( "plink" ) || exe.contains( "klink" ); 222 } 223 224 private Commandline createBaseCommandLine( boolean putty, String executable, File privateKey ) 225 { 226 Commandline cl = new Commandline(); 227 228 cl.setExecutable( executable ); 229 230 if ( privateKey != null ) 231 { 232 cl.createArg().setValue( "-i" ); 233 cl.createArg().setFile( privateKey ); 234 } 235 236 String password = authenticationInfo.getPassword(); 237 if ( putty && password != null ) 238 { 239 cl.createArg().setValue( "-pw" ); 240 cl.createArg().setValue( password ); 241 } 242 243 // should check interactive flag, but scpexe never works in interactive mode right now due to i/o streams 244 if ( putty ) 245 { 246 cl.createArg().setValue( "-batch" ); 247 } 248 else 249 { 250 cl.createArg().setValue( "-o" ); 251 cl.createArg().setValue( "BatchMode yes" ); 252 } 253 return cl; 254 } 255 256 257 private void executeScpCommand( Resource resource, File localFile, boolean put ) 258 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 259 { 260 boolean putty = isPuTTYSCP(); 261 262 File privateKey; 263 try 264 { 265 privateKey = ScpHelper.getPrivateKey( authenticationInfo ); 266 } 267 catch ( FileNotFoundException e ) 268 { 269 fireSessionConnectionRefused(); 270 271 throw new AuthorizationException( e.getMessage() ); 272 } 273 Commandline cl = createBaseCommandLine( putty, scpExecutable, privateKey ); 274 275 cl.setWorkingDirectory( localFile.getParentFile().getAbsolutePath() ); 276 277 int port = 278 repository.getPort() == WagonConstants.UNKNOWN_PORT ? ScpHelper.DEFAULT_SSH_PORT : repository.getPort(); 279 if ( port != ScpHelper.DEFAULT_SSH_PORT ) 280 { 281 cl.createArg().setLine( "-P " + port ); 282 } 283 284 if ( scpArgs != null ) 285 { 286 cl.createArg().setLine( scpArgs ); 287 } 288 289 String resourceName = normalizeResource( resource ); 290 String remoteFile = getRepository().getBasedir() + "/" + resourceName; 291 292 remoteFile = StringUtils.replace( remoteFile, " ", "\\ " ); 293 294 String qualifiedRemoteFile = this.buildRemoteHost() + ":" + remoteFile; 295 if ( put ) 296 { 297 cl.createArg().setValue( localFile.getName() ); 298 cl.createArg().setValue( qualifiedRemoteFile ); 299 } 300 else 301 { 302 cl.createArg().setValue( qualifiedRemoteFile ); 303 cl.createArg().setValue( localFile.getName() ); 304 } 305 306 fireSessionDebug( "Executing command: " + cl.toString() ); 307 308 try 309 { 310 CommandLineUtils.StringStreamConsumer err = new CommandLineUtils.StringStreamConsumer(); 311 int exitCode = CommandLineUtils.executeCommandLine( cl, null, err ); 312 if ( exitCode != 0 ) 313 { 314 if ( !put 315 && err.getOutput().trim().toLowerCase( Locale.ENGLISH ).contains( "no such file or directory" ) ) 316 { 317 throw new ResourceDoesNotExistException( err.getOutput() ); 318 } 319 else 320 { 321 TransferFailedException e = 322 new TransferFailedException( "Exit code: " + exitCode + " - " + err.getOutput() ); 323 324 fireTransferError( resource, e, put ? TransferEvent.REQUEST_PUT : TransferEvent.REQUEST_GET ); 325 326 throw e; 327 } 328 } 329 } 330 catch ( CommandLineException e ) 331 { 332 fireTransferError( resource, e, put ? TransferEvent.REQUEST_PUT : TransferEvent.REQUEST_GET ); 333 334 throw new TransferFailedException( "Error executing command line", e ); 335 } 336 } 337 338 boolean isPuTTYSCP() 339 { 340 String exe = scpExecutable.toLowerCase( Locale.ENGLISH ); 341 return exe.contains( "pscp" ) || exe.contains( "kscp" ); 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<String> 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}