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