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 * @version $Id:ScpExternalWagon.java 477260 2006-11-20 17:11:39Z brett $ 054 * @todo [BP] add compression flag 055 * @plexus.component role="org.apache.maven.wagon.Wagon" 056 * role-hint="scpexe" 057 * instantiation-strategy="per-lookup" 058 */ 059public class ScpExternalWagon 060 extends AbstractWagon 061 implements CommandExecutor 062{ 063 /** 064 * The external SCP command to use - default is <code>scp</code>. 065 * 066 * @component.configuration default="scp" 067 */ 068 private String scpExecutable = "scp"; 069 070 /** 071 * The external SSH command to use - default is <code>ssh</code>. 072 * 073 * @component.configuration default="ssh" 074 */ 075 private String sshExecutable = "ssh"; 076 077 /** 078 * Arguments to pass to the SCP command. 079 * 080 * @component.configuration 081 */ 082 private String scpArgs; 083 084 /** 085 * Arguments to pass to the SSH command. 086 * 087 * @component.configuration 088 */ 089 private String sshArgs; 090 091 private ScpHelper sshTool = new ScpHelper( this ); 092 093 private static final int SSH_FATAL_EXIT_CODE = 255; 094 095 // ---------------------------------------------------------------------- 096 // 097 // ---------------------------------------------------------------------- 098 099 protected void openConnectionInternal() 100 throws AuthenticationException 101 { 102 if ( authenticationInfo == null ) 103 { 104 authenticationInfo = new AuthenticationInfo(); 105 } 106 } 107 108 public void closeConnection() 109 { 110 // nothing to disconnect 111 } 112 113 public boolean getIfNewer( String resourceName, File destination, long timestamp ) 114 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 115 { 116 fireSessionDebug( "getIfNewer in SCP wagon is not supported - performing an unconditional get" ); 117 get( resourceName, destination ); 118 return true; 119 } 120 121 /** 122 * @return The hostname of the remote server prefixed with the username, which comes either from the repository URL 123 * or from the authenticationInfo. 124 */ 125 private String buildRemoteHost() 126 { 127 String username = this.getRepository().getUsername(); 128 if ( username == null ) 129 { 130 username = authenticationInfo.getUserName(); 131 } 132 133 if ( username == null ) 134 { 135 return getRepository().getHost(); 136 } 137 else 138 { 139 return username + "@" + getRepository().getHost(); 140 } 141 } 142 143 public void executeCommand( String command ) 144 throws CommandExecutionException 145 { 146 fireTransferDebug( "Executing command: " + command ); 147 148 executeCommand( command, false ); 149 } 150 151 public Streams executeCommand( String command, boolean ignoreFailures ) 152 throws CommandExecutionException 153 { 154 boolean putty = isPuTTY(); 155 156 File privateKey; 157 try 158 { 159 privateKey = ScpHelper.getPrivateKey( authenticationInfo ); 160 } 161 catch ( FileNotFoundException e ) 162 { 163 throw new CommandExecutionException( e.getMessage(), e ); 164 } 165 Commandline cl = createBaseCommandLine( putty, sshExecutable, privateKey ); 166 167 int port = 168 repository.getPort() == WagonConstants.UNKNOWN_PORT ? ScpHelper.DEFAULT_SSH_PORT : repository.getPort(); 169 if ( port != ScpHelper.DEFAULT_SSH_PORT ) 170 { 171 if ( putty ) 172 { 173 cl.createArg().setLine( "-P " + port ); 174 } 175 else 176 { 177 cl.createArg().setLine( "-p " + port ); 178 } 179 } 180 181 if ( sshArgs != null ) 182 { 183 cl.createArg().setLine( sshArgs ); 184 } 185 186 String remoteHost = this.buildRemoteHost(); 187 188 cl.createArg().setValue( remoteHost ); 189 190 cl.createArg().setValue( command ); 191 192 fireSessionDebug( "Executing command: " + cl.toString() ); 193 194 try 195 { 196 CommandLineUtils.StringStreamConsumer out = new CommandLineUtils.StringStreamConsumer(); 197 CommandLineUtils.StringStreamConsumer err = new CommandLineUtils.StringStreamConsumer(); 198 int exitCode = CommandLineUtils.executeCommandLine( cl, out, err ); 199 Streams streams = new Streams(); 200 streams.setOut( out.getOutput() ); 201 streams.setErr( err.getOutput() ); 202 fireSessionDebug( streams.getOut() ); 203 fireSessionDebug( streams.getErr() ); 204 if ( exitCode != 0 ) 205 { 206 if ( !ignoreFailures || exitCode == SSH_FATAL_EXIT_CODE ) 207 { 208 throw new CommandExecutionException( "Exit code " + exitCode + " - " + err.getOutput() ); 209 } 210 } 211 return streams; 212 } 213 catch ( CommandLineException e ) 214 { 215 throw new CommandExecutionException( "Error executing command line", e ); 216 } 217 } 218 219 protected boolean isPuTTY() 220 { 221 return sshExecutable.toLowerCase( Locale.ENGLISH ).indexOf( "plink" ) >= 0; 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 && err.getOutput().trim().toLowerCase( Locale.ENGLISH ).indexOf( "no such file or directory" ) 315 != -1 ) 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 return scpExecutable.toLowerCase( Locale.ENGLISH ).indexOf( "pscp" ) >= 0; 341 } 342 343 private String normalizeResource( Resource resource ) 344 { 345 return StringUtils.replace( resource.getName(), "\\", "/" ); 346 } 347 348 public void put( File source, String destination ) 349 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 350 { 351 Resource resource = new Resource( destination ); 352 353 firePutInitiated( resource, source ); 354 355 if ( !source.exists() ) 356 { 357 throw new ResourceDoesNotExistException( "Specified source file does not exist: " + source ); 358 } 359 360 String basedir = getRepository().getBasedir(); 361 362 String resourceName = StringUtils.replace( destination, "\\", "/" ); 363 364 String dir = PathUtils.dirname( resourceName ); 365 366 dir = StringUtils.replace( dir, "\\", "/" ); 367 368 String umaskCmd = null; 369 if ( getRepository().getPermissions() != null ) 370 { 371 String dirPerms = getRepository().getPermissions().getDirectoryMode(); 372 373 if ( dirPerms != null ) 374 { 375 umaskCmd = "umask " + PermissionModeUtils.getUserMaskFor( dirPerms ); 376 } 377 } 378 379 String mkdirCmd = "mkdir -p " + basedir + "/" + dir + "\n"; 380 381 if ( umaskCmd != null ) 382 { 383 mkdirCmd = umaskCmd + "; " + mkdirCmd; 384 } 385 386 try 387 { 388 executeCommand( mkdirCmd ); 389 } 390 catch ( CommandExecutionException e ) 391 { 392 fireTransferError( resource, e, TransferEvent.REQUEST_PUT ); 393 394 throw new TransferFailedException( "Error executing command for transfer", e ); 395 } 396 397 resource.setContentLength( source.length() ); 398 399 resource.setLastModified( source.lastModified() ); 400 401 firePutStarted( resource, source ); 402 403 executeScpCommand( resource, source, true ); 404 405 postProcessListeners( resource, source, TransferEvent.REQUEST_PUT ); 406 407 try 408 { 409 RepositoryPermissions permissions = getRepository().getPermissions(); 410 411 if ( permissions != null && permissions.getGroup() != null ) 412 { 413 executeCommand( "chgrp -f " + permissions.getGroup() + " " + basedir + "/" + resourceName + "\n", 414 true ); 415 } 416 417 if ( permissions != null && permissions.getFileMode() != null ) 418 { 419 executeCommand( "chmod -f " + permissions.getFileMode() + " " + basedir + "/" + resourceName + "\n", 420 true ); 421 } 422 } 423 catch ( CommandExecutionException e ) 424 { 425 fireTransferError( resource, e, TransferEvent.REQUEST_PUT ); 426 427 throw new TransferFailedException( "Error executing command for transfer", e ); 428 } 429 firePutCompleted( resource, source ); 430 } 431 432 public void get( String resourceName, File destination ) 433 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 434 { 435 String path = StringUtils.replace( resourceName, "\\", "/" ); 436 437 Resource resource = new Resource( path ); 438 439 fireGetInitiated( resource, destination ); 440 441 createParentDirectories( destination ); 442 443 fireGetStarted( resource, destination ); 444 445 executeScpCommand( resource, destination, false ); 446 447 postProcessListeners( resource, destination, TransferEvent.REQUEST_GET ); 448 449 fireGetCompleted( resource, destination ); 450 } 451 452 // 453 // these parameters are user specific, so should not be read from the repository itself. 454 // They can be configured by plexus, or directly on the instantiated object. 455 // Alternatively, we may later accept a generic parameters argument to connect, or some other configure(Properties) 456 // method on a Wagon. 457 // 458 459 public List<String> getFileList( String destinationDirectory ) 460 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 461 { 462 return sshTool.getFileList( destinationDirectory, repository ); 463 } 464 465 public void putDirectory( File sourceDirectory, String destinationDirectory ) 466 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 467 { 468 sshTool.putDirectory( this, sourceDirectory, destinationDirectory ); 469 } 470 471 public boolean resourceExists( String resourceName ) 472 throws TransferFailedException, AuthorizationException 473 { 474 return sshTool.resourceExists( resourceName, repository ); 475 } 476 477 public boolean supportsDirectoryCopy() 478 { 479 return true; 480 } 481 482 public String getScpExecutable() 483 { 484 return scpExecutable; 485 } 486 487 public void setScpExecutable( String scpExecutable ) 488 { 489 this.scpExecutable = scpExecutable; 490 } 491 492 public String getSshExecutable() 493 { 494 return sshExecutable; 495 } 496 497 public void setSshExecutable( String sshExecutable ) 498 { 499 this.sshExecutable = sshExecutable; 500 } 501 502 public String getScpArgs() 503 { 504 return scpArgs; 505 } 506 507 public void setScpArgs( String scpArgs ) 508 { 509 this.scpArgs = scpArgs; 510 } 511 512 public String getSshArgs() 513 { 514 return sshArgs; 515 } 516 517 public void setSshArgs( String sshArgs ) 518 { 519 this.sshArgs = sshArgs; 520 } 521}