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}