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}