package com.ghostsq.commander.utils; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.InetAddress; import java.net.Inet6Address; import java.net.ServerSocket; import java.net.Socket; import java.net.UnknownHostException; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.StringTokenizer; import android.net.Uri; import android.util.Log; public class FTP { private final static String TAG = "FTP"; public interface ProgressSink { public boolean completed( long size, boolean done ) throws InterruptedException; }; private final static int BLOCK_SIZE = 100000; private final static boolean PRINT_DEBUG_INFO = true; private StringBuffer debugBuf = new StringBuffer(); private String host = null; private Socket cmndSocket = null; private OutputStream outputStream = null; private BufferedInputStream inputStream = null; private ServerSocket serverSocket = null; private Socket dataSocket = null; private InputStream inDataStream = null; private boolean loggedIn = false; private boolean allowActive = false; private boolean ipv6 = false; private Charset charset = null; private final boolean sendCommand( String cmd ) { try { if( outputStream == null || cmndSocket == null || !cmndSocket.isConnected() ) return false; String out = cmd.startsWith( "PASS" ) ? "PASS ***" : cmd; debugPrint( ">>> " + out ); String cmd_rn = cmd + "\r\n"; byte[] bytes = null; if( charset != null ) try { bytes = cmd_rn.getBytes( charset.name() ); } catch( Exception e ) { Log.w( TAG, "Not supported: " + charset ); } if( bytes == null ) bytes = cmd_rn.getBytes(); outputStream.write( bytes ); return true; } catch( IOException e ) { debugPrint( "connection broken" ); Log.e( TAG, "", e ); } return false; } public final void debugPrint( String message ) { Log.v( TAG, message ); if( PRINT_DEBUG_INFO ) { debugBuf.append( message ); debugBuf.append( "\n" ); } } private final boolean isPositivePreliminary( int response ) { return (response >= 100 && response < 200); } private final boolean isPositiveComplete( int response ) { return (response >= 200 && response < 300); } private final boolean isPositiveIntermediate( int response ) { return (response >= 300 && response < 400); } private final boolean isNegative( int response ) { return (response >= 400 ); } private final boolean waitForPositiveResponse() throws InterruptedException { String response = null; try { int code; do { Thread.sleep( 100 ); response = getReplyLine(); if( response == null ) return false; code = getReplyCode( response ); if( isPositiveComplete( code ) ) return true; if( isNegative( code ) ) return false; if( isPositiveIntermediate( code ) ) return true; // when this occurred? Thread.sleep( 400 ); } while( isPositivePreliminary( code ) ); } catch( RuntimeException e ) { Log.e( TAG, "Exception " + ( response == null ? "" : (" on response '" + response + "'") + "\n" ), e ); } return false; } private final int getReplyCode( String reply ) { try { return reply == null ? -1 : Integer.parseInt( reply.substring( 0, 3 ) ); } catch( NumberFormatException e ) { return -1; } } private final void flushReply() { try { while( inputStream.available() > 0 ) inputStream.read(); } catch( IOException e ) { Log.e( TAG, "", e ); } } private final String getReplyLine() { try { if( inputStream == null ) { debugPrint( "No Connection" ); return null; } final int buf_sz = 1024; int i; byte[] buf = new byte[buf_sz]; do { int cnt = 0; do if( cnt++ < 200 ) Thread.sleep( 100 ); else { Log.e( TAG, "The server did not respond. " + inputStream.toString() ); return null; } while( inputStream != null && inputStream.available() == 0 ); for( i = 0; i < buf_sz; i++ ) { int b = inputStream.read(); if( b < 0 ) break; if( b == '\r' || b == '\n' ) { buf[i] = 0; break; } buf[i] = (byte)b; } //Log.v( TAG, "\nfrom FTP:" + new String( buf, 0, i ) + "\n" ); } while( !(Character.isDigit( buf[0] ) && Character.isDigit( buf[1] ) && Character.isDigit( buf[2] ) && buf[3] == ' ' ) ); // read until a coded response be found String reply = charset != null ? new String( buf, 0, i, charset.name() ) : new String( buf, 0, i ); debugPrint( "<<< " + reply ); return reply; } catch( Exception e ) { Log.e( TAG, "", e ); disconnect( true ); return null; } } public final synchronized boolean connect( String host_, int port ) throws UnknownHostException, IOException, InterruptedException { host = host_; cmndSocket = new Socket( host, port ); InetAddress ia = cmndSocket.getInetAddress(); ipv6 = ia instanceof Inet6Address; outputStream = cmndSocket.getOutputStream(); inputStream = new BufferedInputStream( cmndSocket.getInputStream(), 256 ); if( !waitForPositiveResponse() ) { disconnect( true ); return false; } return true; } public final void disconnect( boolean brutal ) { //if( outputStream != null ) // ??? why? { try { if( !brutal && loggedIn ) logout( true ); if( outputStream != null ) outputStream.close(); if( inputStream != null ) inputStream.close(); if( cmndSocket != null ) cmndSocket.close(); if( serverSocket != null ) serverSocket.close(); if( dataSocket != null ) dataSocket.close(); if( inDataStream != null ) inDataStream.close(); } catch( Exception e ) { Log.e( TAG, "", e ); } outputStream = null; inputStream = null; cmndSocket = null; serverSocket = null; dataSocket = null; inDataStream = null; } } public final static int WAS_IN = 1; public final static int LOGGED_IN = 2; public final static int NO_CONNECT = -1; public final static int NO_LOGIN = -2; public final static int NO_WHERE = -3; public synchronized final int connectAndLogin( Uri u, String user, String pass, boolean cwd ) throws UnknownHostException, IOException, InterruptedException { if( isLoggedIn() ) { if( cwd ) { String path = u.getPath(); if( path != null ) setCurrentDir( path ); } return WAS_IN; } int port = u.getPort(); if( port == -1 ) port = 21; String host = u.getHost(); if( connect( host, port ) ) { if( login( user, pass ) ) { if( cwd ) { String path = u.getPath(); if( !Utils.str( path ) ) path = File.separator; if( !setCurrentDir( path ) && !"..".equals( path ) ) { if( !makeDir( path ) || !setCurrentDir( path ) ) return NO_WHERE; } } return LOGGED_IN; } else { disconnect( false ); Log.w( TAG, "Invalid credentials." ); return NO_LOGIN; } } return NO_CONNECT; } public final void setActiveMode( boolean a ) { allowActive = a; } public final boolean getActiveMode() { return allowActive; } public void setCharset( String charset_ ) { try { charset = charset_ == null ? null : Charset.forName( charset_ ); } catch( Exception e ) { Log.e( TAG, "invalid charset: " + charset_, e ); } } private final synchronized boolean executeCommand( String command ) throws InterruptedException { sendCommand( command ); return waitForPositiveResponse(); } private boolean announcePort( ServerSocket serverSocket ) throws IOException, InterruptedException { int localport = serverSocket.getLocalPort(); String port_command = null; if( ipv6 ) { InetAddress la = cmndSocket.getLocalAddress(); port_command = "EPRT |2|" + la.getHostAddress() + "|" + localport + "|"; } else { // get local ip address in high byte order byte[] addrbytes = cmndSocket.getLocalAddress().getAddress(); // tell server what port we are listening on short addrshorts[] = new short[4]; // problem: bytes greater than 127 are printed as negative numbers for( int i = 0; i <= 3; i++ ) { addrshorts[i] = addrbytes[i]; if( addrshorts[i] < 0 ) addrshorts[i] += 256; } port_command = "PORT " + addrshorts[0] + "," + addrshorts[1] + "," + addrshorts[2] + "," + addrshorts[3] + "," + ((localport & 0xff00) >> 8) + "," + (localport & 0x00ff); } if( executeCommand( port_command ) ) return true; Log.e( TAG, "Active mode failed" ); return false; } private final int parsePassiveResponse( String s, byte[] addr ) { try { if( s == null || s.length() < 4 ) return -1; if( !isPositiveComplete( Integer.parseInt( s.substring( 0, 3 ) ) ) ) return -1; // responses could be: // 229 Entering Extended Passive Mode (|||40839|). // 227 Entering Passive Mode (10,0,0,4,134,65) // 227 Entering Passive Mode. 10,0,0,4,134,65 int opt = s.indexOf( '(' ); int cpt = s.indexOf( ')' ); if( cpt < opt ) return -1; StringTokenizer addr_tokenizer; if( opt == -1 && cpt == -1 ) { // no parentheses String addr_str = s.replaceFirst( "\\d{3}\\s[^\\d]+", "" ); addr_tokenizer = new StringTokenizer( addr_str, "," ); } else { String in = s.substring( opt + 1, cpt ); if( ipv6 ) { String[] ss = in.split("\\|"); return Integer.parseInt( ss[3] ); } addr_tokenizer = new StringTokenizer( in, "," ); } int a = 0, b = 0; for( int i = 0; i < 6; i++ ) { short n = Short.parseShort( addr_tokenizer.nextToken() ); if( i < 4 ) addr[i] = (byte)n; else { if( i == 4 ) a = n; if( i == 5 ) b = n; } if( !addr_tokenizer.hasMoreTokens() ) break; } return a * 256 + b; } catch( RuntimeException e ) { Log.e( TAG, "Exception while parsing the string '" + s + "'", e ); } return -1; } private final synchronized Socket executeDataCommand( String commands ) { try { if( commands == null || commands.length() == 0 ) return null; Socket data_socket = null; if( allowActive ) { serverSocket = new ServerSocket( 0 ); if( !announcePort( serverSocket ) ) { allowActive = false; executeCommand( "ABOR" ); } } if( !allowActive ) { flushReply(); // emulator has a bug, it adds \n\r in the end of translated PORT serverSocket = null; // active mode failed. let's try passive final String pasv_command = ipv6 ? "EPSV" : "PASV" ; sendCommand( pasv_command ); byte[] addr = new byte[4]; int server_port = parsePassiveResponse( getReplyLine(), addr ); if( server_port < 0 ) { debugPrint( "Can't negotiate the " + pasv_command ); return null; } if( ipv6 ) data_socket = new Socket( host, server_port ); else data_socket = new Socket( InetAddress.getByAddress( addr ), server_port ); if( !data_socket.isConnected() ) { Log.e( TAG, "Can't open PASV data socket" ); return null; } } String[] cmds = commands.split( "\n" ); for( int i = 0; i < cmds.length; i++ ) { sendCommand( cmds[i] ); if( isNegative( getReplyCode( getReplyLine() ) ) ) { Log.e( TAG, "Executing " + cmds[i] + " failed" ); return null; } } if( data_socket == null && serverSocket != null ) {// active mode Log.i( TAG, "Awaiting the data connection to PORT" ); data_socket = serverSocket.accept(); // will block } if( data_socket == null || !data_socket.isConnected() ) { debugPrint( "Can't establish data connection for " + commands ); return null; } return data_socket; } catch( Exception e ) { Log.e( TAG, "Exception on executing data command '" + commands + "'", e ); } return null; } private final boolean cleanUpDataCommand( boolean wait_reps ) throws InterruptedException { // Clean up the data structures try { if( dataSocket != null ) dataSocket.close(); dataSocket = null; if( serverSocket != null ) serverSocket.close(); serverSocket = null; } catch( IOException e ) { Log.e( TAG, "", e ); } return wait_reps ? waitForPositiveResponse() : true; } /* * public methods */ public final synchronized void clearLog() { debugBuf.setLength( 0 ); } public final synchronized String getLog() { return debugBuf.toString(); } public final boolean isLoggedIn() { if( cmndSocket == null || !cmndSocket.isConnected() ) loggedIn = false; return loggedIn; } public final synchronized boolean login( String username, String password ) throws IOException, InterruptedException { if( !executeCommand( "USER " + username ) ) return false; loggedIn = executeCommand( "PASS " + password ); return loggedIn; } public final boolean logout( boolean quit ) throws IOException, InterruptedException { boolean quit_res = quit ? executeCommand( "QUIT" ) : false; loggedIn = false; return quit_res; } public final void heartBeat() throws InterruptedException { executeCommand( "NOOP" ); } public final synchronized boolean rename( String from, String to ) throws InterruptedException { if( !executeCommand( "RNFR " + from ) ) return false; return executeCommand( "RNTO " + to ); } public final synchronized boolean site( String cmd ) throws InterruptedException { return executeCommand( "SITE " + cmd ); } public final synchronized OutputStream prepStore( String fn ) { dataSocket = null; try { if( !isLoggedIn() ) return null; executeCommand( "TYPE I" ); dataSocket = executeDataCommand( "STOR " + fn ); if( dataSocket != null ) return dataSocket.getOutputStream(); } catch( Exception e ) { debugPrint( e.getLocalizedMessage() ); Log.e( TAG, "", e ); } return null; } public final boolean store( String fn, InputStream in, FTP.ProgressSink report_to ) throws InterruptedException { try { OutputStream out = prepStore( fn ); if( out == null ) { Log.e( TAG, "data socket does not give up the output stream to upload a file" ); return false; } byte buf[] = new byte[BLOCK_SIZE]; int n = 0, last_n = 0; while( true ) { n = in.read( buf ); if( n < 0 ) break; out.write( buf, 0, n ); last_n = n; if( report_to != null ) if( !report_to.completed( n, false ) ) { out.close(); Log.w( TAG, "Deleting incompleted file" + fn ); delete( fn ); return false; } } if( report_to != null ) report_to.completed( last_n, true ); out.close(); return true; } catch( Exception e ) { debugPrint( e.getLocalizedMessage() ); Log.e( TAG, "", e ); } finally { cleanUpDataCommand( dataSocket != null ); } return false; } public final synchronized InputStream prepRetr( String fn, long skip ) throws InterruptedException { dataSocket = null; try { if( !isLoggedIn() ) return null; executeCommand( "TYPE I" ); String retr_cmd = ( skip > 0 ? "REST " + skip + "\n" : "" ) + "RETR " + fn; dataSocket = executeDataCommand( retr_cmd ); if( dataSocket != null ) return dataSocket.getInputStream(); } catch( IOException e ) { debugPrint( e.getLocalizedMessage() ); Log.e( TAG, "", e ); } cleanUpDataCommand( dataSocket != null ); return null; } public final boolean retrieve( String fn, OutputStream out, FTP.ProgressSink report_to ) throws InterruptedException { InputStream in = prepRetr( fn, 0 ); if( in == null ) return false; try { byte buf[] = new byte[BLOCK_SIZE]; int n = 0, last_n = 0; while( true ) { n = in.read( buf ); //Log.v( TAG, "FTP has read " + n + "bytes" ); if( n < 0 ) break; out.write( buf, 0, n ); last_n = n; if( report_to != null ) { if( !report_to.completed( n, false ) ) { executeCommand( "ABOR" ); return false; } } } if( report_to != null ) report_to.completed( last_n, true ); return true; } catch( IOException e ) { debugPrint( e.getLocalizedMessage() ); Log.e( TAG, "", e ); } finally { try { if( in != null ) in.close(); if( out != null ) out.close(); } catch( IOException e ) { Log.e( TAG, "Exception on streams closing (finnaly section)", e ); } cleanUpDataCommand( dataSocket != null ); } return false; } public final boolean setCurrentDir( String dir ) throws InterruptedException { if( !Utils.str( dir ) ) dir = "/"; return executeCommand( dir.compareTo( ".." ) == 0 ? "CDUP" : "CWD " + dir ); } public final synchronized String getCurrentDir() { sendCommand( "PWD" ); // MS IIS responds as: 257 "/" is current directory. // all the others respond as: 257 "/" String pwd_answer = getReplyLine(); if( !isPositiveComplete( getReplyCode( pwd_answer ) ) ) return null; String[] parts = pwd_answer.split( "\"" ); if( parts.length < 2 ) return null; return parts[1]; } public final boolean makeDir( String dir ) throws InterruptedException { return executeCommand( "MKD " + dir ); } public final boolean rmDir( String dir ) throws InterruptedException { return executeCommand( "RMD " + dir ); } public final boolean delete( String name ) throws InterruptedException { return executeCommand( "DELE " + name ); } public final LsItem[] getDirList( String path, boolean force_hidden ) throws InterruptedException { if( !isLoggedIn() ) return null; String cur_dir = null; if( path != null && path.length() > 0 ) { // some servers do not understand the LIST's parameter and always return the list of the current directory cur_dir = getCurrentDir(); if( cur_dir == null ) return null; setCurrentDir( path ); } ArrayList<LsItem> array = null; try { dataSocket = executeDataCommand( "LIST" + ( force_hidden ? " -a" : "" ) ); if( dataSocket == null ) return null; inDataStream = dataSocket.getInputStream(); if( inDataStream == null ) { debugPrint( "data socket does not give up the input stream" ); return null; } InputStreamReader isr = null; if( charset != null ) isr = new InputStreamReader( inDataStream, charset ); if( isr == null ) isr = new InputStreamReader( inDataStream ); BufferedReader dataReader = new BufferedReader( isr, 4096 ); array = new ArrayList<LsItem>(); final String dot = "."; final String dotdot = ".."; while( true ) { String dir_line = dataReader.readLine(); if( dir_line == null ) break; LsItem item = new LsItem( dir_line ); String name = item.getName(); if( item.isValid() && !dotdot.equals( name ) && !dot.equals( name ) ) array.add( item ); } inDataStream.close(); if( cur_dir != null ) setCurrentDir( cur_dir ); } catch( Exception e ) { debugPrint( e.getLocalizedMessage() ); } finally { cleanUpDataCommand( dataSocket != null ); } if( array != null ) { LsItem[] result = new LsItem[array.size()]; return array.toArray( result ); } return null; } }