001package org.apache.maven.wagon.providers.ssh;
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 org.apache.sshd.common.util.DirectoryScanner;
023import org.apache.sshd.server.Command;
024import org.apache.sshd.server.Environment;
025import org.apache.sshd.server.ExitCallback;
026import org.apache.sshd.server.FileSystemAware;
027import org.apache.sshd.server.FileSystemView;
028import org.apache.sshd.server.SshFile;
029import org.slf4j.Logger;
030import org.slf4j.LoggerFactory;
031
032import java.io.ByteArrayOutputStream;
033import java.io.EOFException;
034import java.io.IOException;
035import java.io.InputStream;
036import java.io.OutputStream;
037import java.util.Arrays;
038
039/**
040 * This commands provide SCP support on both server and client side.
041 * Permissions and preservation of access / modification times on files
042 * are not supported.
043 * olamy : copy of a class from mina for changing return codes in case of file not found 1 warning instead of 2
044 *
045 * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
046 */
047public class ScpCommand
048    implements Command, Runnable, FileSystemAware
049{
050
051    protected static final Logger log = LoggerFactory.getLogger( ScpCommand.class );
052
053    protected static final int OK = 0;
054
055    protected static final int WARNING = 1;
056
057    protected static final int ERROR = 2;
058
059    protected String name;
060
061    protected boolean optR;
062
063    protected boolean optT;
064
065    protected boolean optF;
066
067    protected boolean optV;
068
069    protected boolean optD;
070
071    protected boolean optP;
072
073    protected FileSystemView root;
074
075    protected String path;
076
077    protected InputStream in;
078
079    protected OutputStream out;
080
081    protected OutputStream err;
082
083    protected ExitCallback callback;
084
085    protected IOException error;
086
087    public ScpCommand( String[] args )
088    {
089        name = Arrays.asList( args ).toString();
090        if ( log.isDebugEnabled() )
091        {
092            log.debug( "Executing command {}", name );
093        }
094        path = ".";
095        for ( int i = 1; i < args.length; i++ )
096        {
097            if ( args[i].charAt( 0 ) == '-' )
098            {
099                for ( int j = 1; j < args[i].length(); j++ )
100                {
101                    switch ( args[i].charAt( j ) )
102                    {
103                        case 'f':
104                            optF = true;
105                            break;
106                        case 'p':
107                            optP = true;
108                            break;
109                        case 'r':
110                            optR = true;
111                            break;
112                        case 't':
113                            optT = true;
114                            break;
115                        case 'v':
116                            optV = true;
117                            break;
118                        case 'd':
119                            optD = true;
120                            break;
121//                          default:
122//                            error = new IOException("Unsupported option: " + args[i].charAt(j));
123//                            return;
124                    }
125                }
126            }
127            else if ( i == args.length - 1 )
128            {
129                path = args[args.length - 1];
130            }
131        }
132        if ( !optF && !optT )
133        {
134            error = new IOException( "Either -f or -t option should be set" );
135        }
136    }
137
138    public void setInputStream( InputStream in )
139    {
140        this.in = in;
141    }
142
143    public void setOutputStream( OutputStream out )
144    {
145        this.out = out;
146    }
147
148    public void setErrorStream( OutputStream err )
149    {
150        this.err = err;
151    }
152
153    public void setExitCallback( ExitCallback callback )
154    {
155        this.callback = callback;
156    }
157
158    public void start( Environment env )
159        throws IOException
160    {
161        if ( error != null )
162        {
163            throw error;
164        }
165        new Thread( this, "ScpCommand: " + name ).start();
166    }
167
168    public void destroy()
169    {
170    }
171
172    public void run()
173    {
174        int exitValue = OK;
175        String exitMessage = null;
176
177        try
178        {
179            if ( optT )
180            {
181                ack();
182                for (; ; )
183                {
184                    String line;
185                    boolean isDir = false;
186                    int c = readAck( true );
187                    switch ( c )
188                    {
189                        case -1:
190                            return;
191                        case 'D':
192                            isDir = true;
193                        case 'C':
194                            line = ( (char) c ) + readLine();
195                            break;
196                        case 'E':
197                            readLine();
198                            return;
199                        default:
200                            //a real ack that has been acted upon already
201                            continue;
202                    }
203
204                    if ( optR && isDir )
205                    {
206                        writeDir( line, root.getFile( path ) );
207                    }
208                    else
209                    {
210                        writeFile( line, root.getFile( path ) );
211                    }
212                }
213            }
214            else if ( optF )
215            {
216                String pattern = path;
217                int idx = pattern.indexOf( '*' );
218                if ( idx >= 0 )
219                {
220                    String basedir = "";
221                    int lastSep = pattern.substring( 0, idx ).lastIndexOf( '/' );
222                    if ( lastSep >= 0 )
223                    {
224                        basedir = pattern.substring( 0, lastSep );
225                        pattern = pattern.substring( lastSep + 1 );
226                    }
227                    String[] included = new DirectoryScanner( basedir, pattern ).scan();
228                    for ( String path : included )
229                    {
230                        SshFile file = root.getFile( basedir + "/" + path );
231                        if ( file.isFile() )
232                        {
233                            readFile( file );
234                        }
235                        else if ( file.isDirectory() )
236                        {
237                            if ( !optR )
238                            {
239                                out.write( WARNING );
240                                out.write( ( path + " not a regular file\n" ).getBytes() );
241                            }
242                            else
243                            {
244                                readDir( file );
245                            }
246                        }
247                        else
248                        {
249                            out.write( WARNING );
250                            out.write( ( path + " unknown file type\n" ).getBytes() );
251                        }
252                    }
253                }
254                else
255                {
256                    String basedir = "";
257                    int lastSep = pattern.lastIndexOf( '/' );
258                    if ( lastSep >= 0 )
259                    {
260                        basedir = pattern.substring( 0, lastSep );
261                        pattern = pattern.substring( lastSep + 1 );
262                    }
263                    SshFile file = root.getFile( basedir + "/" + pattern );
264                    if ( !file.doesExist() )
265                    {
266                        exitValue = WARNING;
267                        throw new IOException( file + ": no such file or directory" );
268                    }
269                    if ( file.isFile() )
270                    {
271                        readFile( file );
272                    }
273                    else if ( file.isDirectory() )
274                    {
275                        if ( !optR )
276                        {
277                            throw new IOException( file + " not a regular file" );
278                        }
279                        else
280                        {
281                            readDir( file );
282                        }
283                    }
284                    else
285                    {
286                        throw new IOException( file + ": unknown file type" );
287                    }
288                }
289            }
290            else
291            {
292                throw new IOException( "Unsupported mode" );
293            }
294        }
295        catch ( IOException e )
296        {
297            try
298            {
299                exitValue = ( exitValue != OK ? exitValue : ERROR );
300                exitMessage = e.getMessage();
301                out.write( exitValue );
302                out.write( exitMessage.getBytes() );
303                out.write( '\n' );
304                out.flush();
305            }
306            catch ( IOException e2 )
307            {
308                // Ignore
309            }
310            log.info( "Error in scp command", e );
311        }
312        finally
313        {
314            if ( callback != null )
315            {
316                callback.onExit( exitValue, exitMessage );
317            }
318        }
319    }
320
321    protected void writeDir( String header, SshFile path )
322        throws IOException
323    {
324        if ( log.isDebugEnabled() )
325        {
326            log.debug( "Writing dir {}", path );
327        }
328        if ( !header.startsWith( "D" ) )
329        {
330            throw new IOException( "Expected a D message but got '" + header + "'" );
331        }
332
333        String perms = header.substring( 1, 5 );
334        int length = Integer.parseInt( header.substring( 6, header.indexOf( ' ', 6 ) ) );
335        String name = header.substring( header.indexOf( ' ', 6 ) + 1 );
336
337        if ( length != 0 )
338        {
339            throw new IOException( "Expected 0 length for directory but got " + length );
340        }
341        SshFile file;
342        if ( path.doesExist() && path.isDirectory() )
343        {
344            file = root.getFile( path, name );
345        }
346        else if ( !path.doesExist() && path.getParentFile().doesExist() && path.getParentFile().isDirectory() )
347        {
348            file = path;
349        }
350        else
351        {
352            throw new IOException( "Can not write to " + path );
353        }
354        if ( !( file.doesExist() && file.isDirectory() ) && !file.mkdir() )
355        {
356            throw new IOException( "Could not create directory " + file );
357        }
358
359        ack();
360
361        for (; ; )
362        {
363            header = readLine();
364            if ( header.startsWith( "C" ) )
365            {
366                writeFile( header, file );
367            }
368            else if ( header.startsWith( "D" ) )
369            {
370                writeDir( header, file );
371            }
372            else if ( header.equals( "E" ) )
373            {
374                ack();
375                break;
376            }
377            else
378            {
379                throw new IOException( "Unexpected message: '" + header + "'" );
380            }
381        }
382
383    }
384
385    protected void writeFile( String header, SshFile path )
386        throws IOException
387    {
388        if ( log.isDebugEnabled() )
389        {
390            log.debug( "Writing file {}", path );
391        }
392        if ( !header.startsWith( "C" ) )
393        {
394            throw new IOException( "Expected a C message but got '" + header + "'" );
395        }
396
397        String perms = header.substring( 1, 5 );
398        long length = Long.parseLong( header.substring( 6, header.indexOf( ' ', 6 ) ) );
399        String name = header.substring( header.indexOf( ' ', 6 ) + 1 );
400
401        SshFile file;
402        if ( path.doesExist() && path.isDirectory() )
403        {
404            file = root.getFile( path, name );
405        }
406        else if ( path.doesExist() && path.isFile() )
407        {
408            file = path;
409        }
410        else if ( !path.doesExist() && path.getParentFile().doesExist() && path.getParentFile().isDirectory() )
411        {
412            file = path;
413        }
414        else
415        {
416            throw new IOException( "Can not write to " + path );
417        }
418        if ( file.doesExist() && file.isDirectory() )
419        {
420            throw new IOException( "File is a directory: " + file );
421        }
422        else if ( file.doesExist() && !file.isWritable() )
423        {
424            throw new IOException( "Can not write to file: " + file );
425        }
426        OutputStream os = file.createOutputStream( 0 );
427        try
428        {
429            ack();
430
431            byte[] buffer = new byte[8192];
432            while ( length > 0 )
433            {
434                int len = (int) Math.min( length, buffer.length );
435                len = in.read( buffer, 0, len );
436                if ( len <= 0 )
437                {
438                    throw new IOException( "End of stream reached" );
439                }
440                os.write( buffer, 0, len );
441                length -= len;
442            }
443        }
444        finally
445        {
446            os.close();
447        }
448
449        ack();
450        readAck( false );
451    }
452
453    protected String readLine()
454        throws IOException
455    {
456        ByteArrayOutputStream baos = new ByteArrayOutputStream();
457        for (; ; )
458        {
459            int c = in.read();
460            if ( c == '\n' )
461            {
462                return baos.toString();
463            }
464            else if ( c == -1 )
465            {
466                throw new IOException( "End of stream" );
467            }
468            else
469            {
470                baos.write( c );
471            }
472        }
473    }
474
475    protected void readFile( SshFile path )
476        throws IOException
477    {
478        if ( log.isDebugEnabled() )
479        {
480            log.debug( "Reading file {}", path );
481        }
482        StringBuffer buf = new StringBuffer();
483        buf.append( "C" );
484        buf.append( "0644" ); // what about perms
485        buf.append( " " );
486        buf.append( path.getSize() ); // length
487        buf.append( " " );
488        buf.append( path.getName() );
489        buf.append( "\n" );
490        out.write( buf.toString().getBytes() );
491        out.flush();
492        readAck( false );
493
494        InputStream is = path.createInputStream( 0 );
495        try
496        {
497            byte[] buffer = new byte[8192];
498            for (; ; )
499            {
500                int len = is.read( buffer, 0, buffer.length );
501                if ( len == -1 )
502                {
503                    break;
504                }
505                out.write( buffer, 0, len );
506            }
507        }
508        finally
509        {
510            is.close();
511        }
512        ack();
513        readAck( false );
514    }
515
516    protected void readDir( SshFile path )
517        throws IOException
518    {
519        if ( log.isDebugEnabled() )
520        {
521            log.debug( "Reading directory {}", path );
522        }
523        StringBuffer buf = new StringBuffer();
524        buf.append( "D" );
525        buf.append( "0755" ); // what about perms
526        buf.append( " " );
527        buf.append( "0" ); // length
528        buf.append( " " );
529        buf.append( path.getName() );
530        buf.append( "\n" );
531        out.write( buf.toString().getBytes() );
532        out.flush();
533        readAck( false );
534
535        for ( SshFile child : path.listSshFiles() )
536        {
537            if ( child.isFile() )
538            {
539                readFile( child );
540            }
541            else if ( child.isDirectory() )
542            {
543                readDir( child );
544            }
545        }
546
547        out.write( "E\n".getBytes() );
548        out.flush();
549        readAck( false );
550    }
551
552    protected void ack()
553        throws IOException
554    {
555        out.write( 0 );
556        out.flush();
557    }
558
559    protected int readAck( boolean canEof )
560        throws IOException
561    {
562        int c = in.read();
563        switch ( c )
564        {
565            case -1:
566                if ( !canEof )
567                {
568                    throw new EOFException();
569                }
570                break;
571            case OK:
572                break;
573            case WARNING:
574                log.warn( "Received warning: " + readLine() );
575                break;
576            case ERROR:
577                throw new IOException( "Received nack: " + readLine() );
578            default:
579                break;
580        }
581        return c;
582    }
583
584    public void setFileSystemView( FileSystemView view )
585    {
586        this.root = view;
587    }
588}