View Javadoc

1   package org.apache.maven.wagon.providers.ssh.jsch;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.EOFException;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.OutputStream;
26  
27  import org.apache.maven.wagon.CommandExecutionException;
28  import org.apache.maven.wagon.InputData;
29  import org.apache.maven.wagon.OutputData;
30  import org.apache.maven.wagon.ResourceDoesNotExistException;
31  import org.apache.maven.wagon.TransferFailedException;
32  import org.apache.maven.wagon.events.TransferEvent;
33  import org.apache.maven.wagon.providers.ssh.ScpHelper;
34  import org.apache.maven.wagon.repository.RepositoryPermissions;
35  import org.apache.maven.wagon.resource.Resource;
36  
37  import com.jcraft.jsch.ChannelExec;
38  import com.jcraft.jsch.JSchException;
39  
40  /**
41   * SCP protocol wagon.
42   * 
43   * Note that this implementation is <i>not</i> thread-safe, and multiple channels can not be used on the session at
44   * the same time.
45   * 
46   * See <a href="http://blogs.sun.com/janp/entry/how_the_scp_protocol_works">
47   * http://blogs.sun.com/janp/entry/how_the_scp_protocol_works</a>
48   * for information on how the SCP protocol works.
49   *
50   * @version $Id: ScpWagon.java 688222 2008-08-22 23:06:12Z hboutemy $
51   * @todo [BP] add compression flag
52   * 
53   * @plexus.component role="org.apache.maven.wagon.Wagon" 
54   *   role-hint="scp"
55   *   instantiation-strategy="per-lookup"
56   */
57  public class ScpWagon
58      extends AbstractJschWagon
59  {
60      private static final char COPY_START_CHAR = 'C';
61  
62      private static final char ACK_SEPARATOR = ' ';
63  
64      private static final String END_OF_FILES_MSG = "E\n";
65  
66      private static final int LINE_BUFFER_SIZE = 8192;
67  
68      private static final byte LF = '\n';
69  
70      private ChannelExec channel;
71  
72      private InputStream channelInputStream;
73  
74      private OutputStream channelOutputStream;
75  
76      private void setFileGroup( RepositoryPermissions permissions, String basedir, Resource resource )
77          throws CommandExecutionException
78      {
79          if ( permissions != null && permissions.getGroup() != null )
80          {
81              executeCommand( "chgrp -f " + permissions.getGroup() + " " + getPath( basedir, resource.getName() ) );
82          }
83      }
84  
85      protected void cleanupPutTransfer( Resource resource )
86      {
87          if ( channel != null )
88          {
89              channel.disconnect();
90              channel = null;
91          }
92      }
93  
94      protected void finishPutTransfer( Resource resource, InputStream input, OutputStream output )
95          throws TransferFailedException
96      {
97          try
98          {
99              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     protected void getTransfer( Resource resource, OutputStream output, InputStream input, boolean closeInput,
173                                 int maxSize )
174         throws TransferFailedException
175     {
176         super.getTransfer( resource, output, input, closeInput, (int) resource.getContentLength() );
177     }
178     
179     protected String readLine( InputStream in )
180         throws IOException
181     {
182         StringBuffer sb = new StringBuffer();
183 
184         while ( true )
185         {
186             if ( sb.length() > LINE_BUFFER_SIZE )
187             {
188                 throw new IOException( "Remote server sent a too long line" );
189             }
190 
191             int c = in.read();
192 
193             if ( c < 0 )
194             {
195                 throw new IOException( "Remote connection terminated unexpectedly." );
196             }
197 
198             if ( c == LF )
199             {
200                 break;
201             }
202 
203             sb.append( (char) c );
204         }
205         return sb.toString();
206     }
207 
208     protected static void sendEom( OutputStream out )
209         throws IOException
210     {
211         out.write( 0 );
212 
213         out.flush();
214     }
215 
216     public void fillInputData( InputData inputData )
217         throws TransferFailedException, ResourceDoesNotExistException
218     {
219         Resource resource = inputData.getResource();
220         
221         String path = getPath( getRepository().getBasedir(), resource.getName() );
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                 {
263                     throw new ResourceDoesNotExistException( line );
264                 }
265                 else
266                 {
267                     throw new IOException( "Exit code: " + exitCode + " - " + line );
268                 }
269             }
270 
271             if ( line == null )
272             {
273                 throw new EOFException( "Unexpected end of data" );
274             }
275 
276             String perms = line.substring( 0, 4 );
277             fireTransferDebug( "Remote file permissions: " + perms );
278 
279             if ( line.charAt( 4 ) != ACK_SEPARATOR && line.charAt( 5 ) != ACK_SEPARATOR )
280             {
281                 throw new IOException( "Invalid transfer header: " + line );
282             }
283 
284             int index = line.indexOf( ACK_SEPARATOR, 5 );
285             if ( index < 0 )
286             {
287                 throw new IOException( "Invalid transfer header: " + line );
288             }
289 
290             int filesize = Integer.valueOf( line.substring( 5, index ) ).intValue();
291             fireTransferDebug( "Remote file size: " + filesize );
292 
293             resource.setContentLength( filesize );
294 
295             String filename = line.substring( index + 1 );
296             fireTransferDebug( "Remote filename: " + filename );
297 
298             sendEom( channelOutputStream );
299         }
300         catch ( JSchException e )
301         {
302             handleGetException( resource, e );
303         }
304         catch ( IOException e )
305         {
306             handleGetException( resource, e );
307         }
308     }
309 
310     public void fillOutputData( OutputData outputData )
311         throws TransferFailedException
312     {
313         Resource resource = outputData.getResource();
314         
315         String basedir = getRepository().getBasedir();
316 
317         String path = getPath( basedir, resource.getName() );
318 
319         String dir = ScpHelper.getResourceDirectory( resource.getName() );
320 
321         try
322         {
323             sshTool.createRemoteDirectories( getPath( basedir, dir ), getRepository().getPermissions() );
324         }
325         catch ( CommandExecutionException e )
326         {
327             fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
328 
329             throw new TransferFailedException( e.getMessage(), e );
330         }
331         
332         String octalMode = getOctalMode( getRepository().getPermissions() );
333         
334         // exec 'scp -p -t rfile' remotely
335         String command = "scp";
336         if ( octalMode != null )
337         {
338             command += " -p";
339         }
340         command += " -t \"" + path + "\"";
341 
342         fireTransferDebug( "Executing command: " + command );
343 
344         String resourceName = resource.getName();
345 
346         OutputStream out = null;
347         try
348         {
349             channel = (ChannelExec) session.openChannel( EXEC_CHANNEL );
350 
351             channel.setCommand( command );
352 
353             // get I/O streams for remote scp
354             out = channel.getOutputStream();
355             outputData.setOutputStream( out );
356 
357             channelInputStream = channel.getInputStream();
358 
359             channel.connect();
360 
361             checkAck( channelInputStream );
362 
363             // send "C0644 filesize filename", where filename should not include '/'
364             long filesize = resource.getContentLength();
365 
366             String mode = octalMode == null ? "0644" : octalMode;
367             command = "C" + mode + " " + filesize + " ";
368 
369             if ( resourceName.lastIndexOf( ScpHelper.PATH_SEPARATOR ) > 0 )
370             {
371                 command += resourceName.substring( resourceName.lastIndexOf( ScpHelper.PATH_SEPARATOR ) + 1 );
372             }
373             else
374             {
375                 command += resourceName;
376             }
377 
378             command += "\n";
379 
380             out.write( command.getBytes() );
381 
382             out.flush();
383 
384             checkAck( channelInputStream );
385         }
386         catch ( JSchException e )
387         {
388             fireTransferError( resource, e, TransferEvent.REQUEST_PUT );            
389             
390             String msg = "Error occurred while deploying '" + resourceName + "' to remote repository: "
391                 + getRepository().getUrl() + ": " + e.getMessage();
392 
393             throw new TransferFailedException( msg, e );
394         }
395         catch ( IOException e )
396         {
397             handleIOException( resource, e );
398         }
399     }
400 
401     private void handleIOException( Resource resource, IOException e )
402         throws TransferFailedException
403     {
404         if ( e.getMessage().indexOf( "set mode: Operation not permitted" ) >= 0 )
405         {
406             fireTransferDebug( e.getMessage() );                
407         }
408         else
409         {
410             fireTransferError( resource, e, TransferEvent.REQUEST_PUT );            
411             
412             String msg = "Error occurred while deploying '" + resource.getName() + "' to remote repository: "
413                 + getRepository().getUrl() + ": " + e.getMessage();
414    
415             throw new TransferFailedException( msg, e );
416         }
417     }
418 
419     public String getOctalMode( RepositoryPermissions permissions )
420     {
421         String mode = null;
422         if ( permissions != null && permissions.getFileMode() != null )
423         {
424             if ( permissions.getFileMode().matches( "[0-9]{3,4}" ) )
425             {
426                 mode = permissions.getFileMode();
427 
428                 if ( mode.length() == 3 )
429                 {
430                     mode = "0" + mode;
431                 }
432             }
433             else
434             {
435                 // TODO: calculate?
436                 // TODO: as warning
437                 fireSessionDebug( "Not using non-octal permissions: " + permissions.getFileMode() );
438             }
439         }
440         return mode;
441     }
442 }