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 com.jcraft.jsch.ChannelExec;
23  import com.jcraft.jsch.JSchException;
24  import org.apache.maven.wagon.CommandExecutionException;
25  import org.apache.maven.wagon.InputData;
26  import org.apache.maven.wagon.OutputData;
27  import org.apache.maven.wagon.ResourceDoesNotExistException;
28  import org.apache.maven.wagon.TransferFailedException;
29  import org.apache.maven.wagon.events.TransferEvent;
30  import org.apache.maven.wagon.providers.ssh.ScpHelper;
31  import org.apache.maven.wagon.repository.RepositoryPermissions;
32  import org.apache.maven.wagon.resource.Resource;
33  
34  import java.io.EOFException;
35  import java.io.IOException;
36  import java.io.InputStream;
37  import java.io.OutputStream;
38  
39  /**
40   * SCP protocol wagon.
41   * <p/>
42   * Note that this implementation is <i>not</i> thread-safe, and multiple channels can not be used on the session at
43   * the same time.
44   * <p/>
45   * See <a href="http://blogs.sun.com/janp/entry/how_the_scp_protocol_works">
46   * http://blogs.sun.com/janp/entry/how_the_scp_protocol_works</a>
47   * for information on how the SCP protocol works.
48   *
49   *
50   * @todo [BP] add compression flag
51   * @plexus.component role="org.apache.maven.wagon.Wagon"
52   * role-hint="scp"
53   * instantiation-strategy="per-lookup"
54   */
55  public class ScpWagon
56      extends AbstractJschWagon
57  {
58      private static final char COPY_START_CHAR = 'C';
59  
60      private static final char ACK_SEPARATOR = ' ';
61  
62      private static final String END_OF_FILES_MSG = "E\n";
63  
64      private static final int LINE_BUFFER_SIZE = 8192;
65  
66      private static final byte LF = '\n';
67  
68      private ChannelExec channel;
69  
70      private InputStream channelInputStream;
71  
72      private OutputStream channelOutputStream;
73  
74      private void setFileGroup( RepositoryPermissions permissions, String basedir, Resource resource )
75          throws CommandExecutionException
76      {
77          if ( permissions != null && permissions.getGroup() != null )
78          {
79              //executeCommand( "chgrp -f " + permissions.getGroup() + " " + getPath( basedir, resource.getName() ) );
80              executeCommand( "chgrp -f " + permissions.getGroup() + " \"" + getPath( basedir, resource.getName() ) + "\"" );
81          }
82      }
83  
84      protected void cleanupPutTransfer( Resource resource )
85      {
86          if ( channel != null )
87          {
88              channel.disconnect();
89              channel = null;
90          }
91      }
92  
93      protected void finishPutTransfer( Resource resource, InputStream input, OutputStream output )
94          throws TransferFailedException
95      {
96          try
97          {
98              sendEom( output );
99  
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 }