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  
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     @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 }