001package org.apache.maven.wagon.providers.ssh.jsch; 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 com.jcraft.jsch.ChannelExec; 023import com.jcraft.jsch.JSchException; 024import org.apache.maven.wagon.CommandExecutionException; 025import org.apache.maven.wagon.InputData; 026import org.apache.maven.wagon.OutputData; 027import org.apache.maven.wagon.ResourceDoesNotExistException; 028import org.apache.maven.wagon.TransferFailedException; 029import org.apache.maven.wagon.events.TransferEvent; 030import org.apache.maven.wagon.providers.ssh.ScpHelper; 031import org.apache.maven.wagon.repository.RepositoryPermissions; 032import org.apache.maven.wagon.resource.Resource; 033 034import java.io.EOFException; 035import java.io.IOException; 036import java.io.InputStream; 037import java.io.OutputStream; 038 039/** 040 * SCP protocol wagon. 041 * <p/> 042 * Note that this implementation is <i>not</i> thread-safe, and multiple channels can not be used on the session at 043 * the same time. 044 * <p/> 045 * See <a href="http://blogs.sun.com/janp/entry/how_the_scp_protocol_works"> 046 * http://blogs.sun.com/janp/entry/how_the_scp_protocol_works</a> 047 * for information on how the SCP protocol works. 048 * 049 * 050 * @todo [BP] add compression flag 051 * @plexus.component role="org.apache.maven.wagon.Wagon" 052 * role-hint="scp" 053 * instantiation-strategy="per-lookup" 054 */ 055public class ScpWagon 056 extends AbstractJschWagon 057{ 058 private static final char COPY_START_CHAR = 'C'; 059 060 private static final char ACK_SEPARATOR = ' '; 061 062 private static final String END_OF_FILES_MSG = "E\n"; 063 064 private static final int LINE_BUFFER_SIZE = 8192; 065 066 private static final byte LF = '\n'; 067 068 private ChannelExec channel; 069 070 private InputStream channelInputStream; 071 072 private OutputStream channelOutputStream; 073 074 private void setFileGroup( RepositoryPermissions permissions, String basedir, Resource resource ) 075 throws CommandExecutionException 076 { 077 if ( permissions != null && permissions.getGroup() != null ) 078 { 079 //executeCommand( "chgrp -f " + permissions.getGroup() + " " + getPath( basedir, resource.getName() ) ); 080 executeCommand( "chgrp -f " + permissions.getGroup() + " \"" + getPath( basedir, resource.getName() ) 081 + "\"" ); 082 } 083 } 084 085 protected void cleanupPutTransfer( Resource resource ) 086 { 087 if ( channel != null ) 088 { 089 channel.disconnect(); 090 channel = null; 091 } 092 } 093 094 protected void finishPutTransfer( Resource resource, InputStream input, OutputStream output ) 095 throws TransferFailedException 096 { 097 try 098 { 099 sendEom( output ); 100 101 checkAck( channelInputStream ); 102 103 // This came from SCPClient in Ganymede SSH2. It is sent after all files. 104 output.write( END_OF_FILES_MSG.getBytes() ); 105 output.flush(); 106 } 107 catch ( IOException e ) 108 { 109 handleIOException( resource, e ); 110 } 111 112 String basedir = getRepository().getBasedir(); 113 try 114 { 115 setFileGroup( getRepository().getPermissions(), basedir, resource ); 116 } 117 catch ( CommandExecutionException e ) 118 { 119 fireTransferError( resource, e, TransferEvent.REQUEST_PUT ); 120 121 throw new TransferFailedException( e.getMessage(), e ); 122 } 123 } 124 125 private void checkAck( InputStream in ) 126 throws IOException 127 { 128 int code = in.read(); 129 if ( code == -1 ) 130 { 131 throw new IOException( "Unexpected end of data" ); 132 } 133 else if ( code == 1 ) 134 { 135 String line = readLine( in ); 136 137 throw new IOException( "SCP terminated with error: '" + line + "'" ); 138 } 139 else if ( code == 2 ) 140 { 141 throw new IOException( "SCP terminated with error (code: " + code + ")" ); 142 } 143 else if ( code != 0 ) 144 { 145 throw new IOException( "SCP terminated with unknown error code" ); 146 } 147 } 148 149 protected void finishGetTransfer( Resource resource, InputStream input, OutputStream output ) 150 throws TransferFailedException 151 { 152 try 153 { 154 checkAck( input ); 155 156 sendEom( channelOutputStream ); 157 } 158 catch ( IOException e ) 159 { 160 handleGetException( resource, e ); 161 } 162 } 163 164 protected void cleanupGetTransfer( Resource resource ) 165 { 166 if ( channel != null ) 167 { 168 channel.disconnect(); 169 } 170 } 171 172 @Deprecated 173 protected void getTransfer( Resource resource, OutputStream output, InputStream input, boolean closeInput, 174 int maxSize ) 175 throws TransferFailedException 176 { 177 super.getTransfer( resource, output, input, closeInput, (int) resource.getContentLength() ); 178 } 179 180 protected void getTransfer( Resource resource, OutputStream output, InputStream input, boolean closeInput, 181 long maxSize ) 182 throws TransferFailedException 183 { 184 super.getTransfer( resource, output, input, closeInput, resource.getContentLength() ); 185 } 186 187 protected String readLine( InputStream in ) 188 throws IOException 189 { 190 StringBuilder sb = new StringBuilder(); 191 192 while ( true ) 193 { 194 if ( sb.length() > LINE_BUFFER_SIZE ) 195 { 196 throw new IOException( "Remote server sent a too long line" ); 197 } 198 199 int c = in.read(); 200 201 if ( c < 0 ) 202 { 203 throw new IOException( "Remote connection terminated unexpectedly." ); 204 } 205 206 if ( c == LF ) 207 { 208 break; 209 } 210 211 sb.append( (char) c ); 212 } 213 return sb.toString(); 214 } 215 216 protected static void sendEom( OutputStream out ) 217 throws IOException 218 { 219 out.write( 0 ); 220 221 out.flush(); 222 } 223 224 public void fillInputData( InputData inputData ) 225 throws TransferFailedException, ResourceDoesNotExistException 226 { 227 Resource resource = inputData.getResource(); 228 229 String path = getPath( getRepository().getBasedir(), resource.getName() ); 230 //String cmd = "scp -p -f " + path; 231 String cmd = "scp -p -f \"" + path + "\""; 232 233 fireTransferDebug( "Executing command: " + cmd ); 234 235 try 236 { 237 channel = (ChannelExec) session.openChannel( EXEC_CHANNEL ); 238 239 channel.setCommand( cmd ); 240 241 // get I/O streams for remote scp 242 channelOutputStream = channel.getOutputStream(); 243 244 InputStream in = channel.getInputStream(); 245 inputData.setInputStream( in ); 246 247 channel.connect(); 248 249 sendEom( channelOutputStream ); 250 251 int exitCode = in.read(); 252 253 if ( exitCode == 'T' ) 254 { 255 String line = readLine( in ); 256 257 String[] times = line.split( " " ); 258 259 resource.setLastModified( Long.valueOf( times[0] ).longValue() * 1000 ); 260 261 sendEom( channelOutputStream ); 262 263 exitCode = in.read(); 264 } 265 266 String line = readLine( in ); 267 268 if ( exitCode != COPY_START_CHAR ) 269 { 270 if ( exitCode == 1 && ( line.contains( "No such file or directory" ) 271 || line.indexOf( "no such file or directory" ) != 1 ) ) 272 { 273 throw new ResourceDoesNotExistException( line ); 274 } 275 else 276 { 277 throw new IOException( "Exit code: " + exitCode + " - " + line ); 278 } 279 } 280 281 if ( line == null ) 282 { 283 throw new EOFException( "Unexpected end of data" ); 284 } 285 286 String perms = line.substring( 0, 4 ); 287 fireTransferDebug( "Remote file permissions: " + perms ); 288 289 if ( line.charAt( 4 ) != ACK_SEPARATOR && line.charAt( 5 ) != ACK_SEPARATOR ) 290 { 291 throw new IOException( "Invalid transfer header: " + line ); 292 } 293 294 int index = line.indexOf( ACK_SEPARATOR, 5 ); 295 if ( index < 0 ) 296 { 297 throw new IOException( "Invalid transfer header: " + line ); 298 } 299 300 long filesize = Long.parseLong( line.substring( 5, index ) ); 301 fireTransferDebug( "Remote file size: " + filesize ); 302 303 resource.setContentLength( filesize ); 304 305 String filename = line.substring( index + 1 ); 306 fireTransferDebug( "Remote filename: " + filename ); 307 308 sendEom( channelOutputStream ); 309 } 310 catch ( JSchException e ) 311 { 312 handleGetException( resource, e ); 313 } 314 catch ( IOException e ) 315 { 316 handleGetException( resource, e ); 317 } 318 } 319 320 public void fillOutputData( OutputData outputData ) 321 throws TransferFailedException 322 { 323 Resource resource = outputData.getResource(); 324 325 String basedir = getRepository().getBasedir(); 326 327 String path = getPath( basedir, resource.getName() ); 328 329 String dir = ScpHelper.getResourceDirectory( resource.getName() ); 330 331 try 332 { 333 sshTool.createRemoteDirectories( getPath( basedir, dir ), getRepository().getPermissions() ); 334 } 335 catch ( CommandExecutionException e ) 336 { 337 fireTransferError( resource, e, TransferEvent.REQUEST_PUT ); 338 339 throw new TransferFailedException( e.getMessage(), e ); 340 } 341 342 String octalMode = getOctalMode( getRepository().getPermissions() ); 343 344 // exec 'scp -p -t rfile' remotely 345 String command = "scp"; 346 if ( octalMode != null ) 347 { 348 command += " -p"; 349 } 350 command += " -t \"" + path + "\""; 351 352 fireTransferDebug( "Executing command: " + command ); 353 354 String resourceName = resource.getName(); 355 356 OutputStream out = null; 357 try 358 { 359 channel = (ChannelExec) session.openChannel( EXEC_CHANNEL ); 360 361 channel.setCommand( command ); 362 363 // get I/O streams for remote scp 364 out = channel.getOutputStream(); 365 outputData.setOutputStream( out ); 366 367 channelInputStream = channel.getInputStream(); 368 369 channel.connect(); 370 371 checkAck( channelInputStream ); 372 373 // send "C0644 filesize filename", where filename should not include '/' 374 long filesize = resource.getContentLength(); 375 376 String mode = octalMode == null ? "0644" : octalMode; 377 command = "C" + mode + " " + filesize + " "; 378 379 if ( resourceName.lastIndexOf( ScpHelper.PATH_SEPARATOR ) > 0 ) 380 { 381 command += resourceName.substring( resourceName.lastIndexOf( ScpHelper.PATH_SEPARATOR ) + 1 ); 382 } 383 else 384 { 385 command += resourceName; 386 } 387 388 command += "\n"; 389 390 out.write( command.getBytes() ); 391 392 out.flush(); 393 394 checkAck( channelInputStream ); 395 } 396 catch ( JSchException e ) 397 { 398 fireTransferError( resource, e, TransferEvent.REQUEST_PUT ); 399 400 String msg = "Error occurred while deploying '" + resourceName + "' to remote repository: " 401 + getRepository().getUrl() + ": " + e.getMessage(); 402 403 throw new TransferFailedException( msg, e ); 404 } 405 catch ( IOException e ) 406 { 407 handleIOException( resource, e ); 408 } 409 } 410 411 private void handleIOException( Resource resource, IOException e ) 412 throws TransferFailedException 413 { 414 if ( e.getMessage().contains( "set mode: Operation not permitted" ) ) 415 { 416 fireTransferDebug( e.getMessage() ); 417 } 418 else 419 { 420 fireTransferError( resource, e, TransferEvent.REQUEST_PUT ); 421 422 String msg = "Error occurred while deploying '" + resource.getName() + "' to remote repository: " 423 + getRepository().getUrl() + ": " + e.getMessage(); 424 425 throw new TransferFailedException( msg, e ); 426 } 427 } 428 429 public String getOctalMode( RepositoryPermissions permissions ) 430 { 431 String mode = null; 432 if ( permissions != null && permissions.getFileMode() != null ) 433 { 434 if ( permissions.getFileMode().matches( "[0-9]{3,4}" ) ) 435 { 436 mode = permissions.getFileMode(); 437 438 if ( mode.length() == 3 ) 439 { 440 mode = "0" + mode; 441 } 442 } 443 else 444 { 445 // TODO: calculate? 446 // TODO: as warning 447 fireSessionDebug( "Not using non-octal permissions: " + permissions.getFileMode() ); 448 } 449 } 450 return mode; 451 } 452}