001    package com.sptci.system;
002    
003    import java.io.FileInputStream;
004    import java.io.InputStream;
005    import java.io.InputStreamReader;
006    import java.io.IOException;
007    import java.io.OutputStream;
008    import java.io.PrintStream;
009    import java.net.SocketException;
010    
011    import java.util.Properties;
012    import java.util.logging.Level;
013    import java.util.logging.Logger;
014    
015    import org.apache.commons.net.telnet.TelnetClient;
016    
017    /**
018     * The class that is used to interact with the system.  Provides methods
019     * to log user to the system and execute the password change command.
020     *
021     * <p>Modified version of class documented in
022     * <a href='http://www.informit.com/guides/content.asp?g=java&seqNum=40&rl=1'>informit.com</a>.</p>
023     * @author Rakesh Vidyadharan 2007-04-21
024     * @version $Id: CommandExecutor.java 3252 2007-05-12 19:12:31Z rakesh $
025     */
026    public class CommandExecutor
027    {
028      /**
029       * The logger to use to log errors/messages to.
030       */
031      private static final Logger logger =
032        Logger.getLogger( CommandExecutor.class.getName() );
033    
034      /**
035       * The name of the system property that points to the properties file
036       * used to configure this instance.
037       *
038       * {@value}
039       */
040      public static final String PROPERTY_FILE = "password.properties.file";
041    
042      /**
043       * The name of the property that contains the hostname of the server to
044       * which a <code>telnet</code> session will be initiated.
045       *
046       * {@value}
047       */
048      public static final String SERVER = "server";
049    
050      /**
051       * The name of the property that contains the port on which a
052       * <code>telnet</code> session will be established.  This propery is
053       * optional.  By default a connection to the standard <code>telnet</code>
054       * port <code>23</code> will be attempted.
055       *
056       * {@value}
057       */
058      public static final String PORT = "port";
059    
060      /**
061       * The name of the property that contains the login prompt sent back by
062       * telnet server when a connection attempt is made.
063       *
064       * {@value}
065       */
066      public static final String LOGIN_PROMPT = "login";
067    
068      /**
069       * The name of the property that contains the password prompt sent back
070       * by telnet server after a response is sent to the {@link #LOGIN_PROMPT}
071       *
072       * {@value}
073       */
074      public static final String PASSWORD_PROMPT = "password";
075    
076      /**
077       * The name of the property that contains the shell prompt character used 
078       * by the system.
079       *
080       * <p><b>Note:</b> This of course depends upon the shell used by the
081       * user and the user's settings.</p>
082       *
083       * {@value}
084       */
085      public static final String PROMPT = "prompt";
086    
087      /**
088       * The server to log on to affect the change in password.
089       */
090      final String server;
091    
092      /**
093       * The port on which to connect to the {@link #server}.
094       */
095      final int port;
096    
097      /**
098       * The user who is trying to change his password.
099       */
100      String user;
101    
102      /**
103       * The telnet client instance to use to communicate with the {@link 
104       * #server}.
105       */
106      private TelnetClient client = new TelnetClient();
107    
108      /**
109       * The properties object that contains the configured properties for
110       * this instance.
111       */
112      final Properties properties;
113    
114      /**
115       * The <code>InputStream</code> for the <code>telnet</code> session.
116       */
117      private InputStream in;
118    
119      /**
120       * The <code>OutputStream</code> for the <code>telnet</code> session.
121       */
122      private PrintStream out;
123    
124      /**
125       * Create a new instance of the class;
126       *
127       * @throws RuntimeException If errors are encountered while initialising
128       *   the instance from the {@link #PROPERTY_FILE} file.
129       */
130      public CommandExecutor() throws RuntimeException
131      {
132        properties = new Properties();
133    
134        try
135        {
136          properties.loadFromXML(
137              new FileInputStream( System.getProperty( PROPERTY_FILE ) ) );
138        }
139        catch ( Throwable t )
140        {
141          logger.log( Level.SEVERE, "Error loading properties file", t );
142          throw new RuntimeException( t );
143        }
144    
145        server = properties.getProperty( SERVER );
146        port = Integer.parseInt( properties.getProperty( PORT, "23" ) );
147      }
148    
149      /**
150       * Connect to the {@link #server} via <code>telnet</code> and log the
151       * user to the system.  This method assumes that caller has either
152       * invoked {@link #setUser} or {@link #connect( String, String )} prior
153       * to calling this method.
154       *
155       * @see #connect( String, String )
156       * @throws SocketException If the socket timeout could not be set.
157       * @throws IOException If the socket could not be opened. In most cases
158       *   you will only want to catch IOException since SocketException is
159       *   derived from it.
160       * @throws AuthenticationFailedException If the login attempt failed due
161       *   to username or password error.
162       */
163      public void connect( String password )
164        throws SocketException, IOException, AuthenticationFailedException
165      {
166        connect( user, password );
167      }
168    
169      /**
170       * Connect to the {@link #server} via <code>telnet</code> and log the
171       * user to the system.
172       *
173       * @see #readUntil
174       * @see #write
175       * @see #readLoginResponse
176       * @throws SocketException If the socket timeout could not be set.
177       * @throws IOException If the socket could not be opened. In most cases
178       *   you will only want to catch IOException since SocketException is
179       *   derived from it.
180       * @throws AuthenticationFailedException If the login attempt failed due
181       *   to username or password error.
182       */
183      public void connect( String user, String password )
184        throws SocketException, IOException, AuthenticationFailedException
185      {
186        setUser( user );
187        client.connect( server, port );
188    
189        in = client.getInputStream();
190        out = new PrintStream( client.getOutputStream() );
191    
192        readUntil( properties.getProperty( LOGIN_PROMPT ) );
193        write( user );
194        readUntil( properties.getProperty( PASSWORD_PROMPT ) );
195        write( password );
196        readLoginResponse();
197      }
198    
199      /**
200       * Change the {@link #user}'s password.
201       *
202       * @param password The current password of the user.
203       * @param newpassword The new password to set.
204       * @throws IOException If the socket could not be opened. In most cases
205       *   you will only want to catch IOException since SocketException is
206       *   derived from it.
207       * @throws PasswordException If the current or new password are incorrect
208       *   or insufficient
209       */
210      public void password( String password, String newpassword )
211        throws IOException, PasswordException
212      {
213        write( "passwd" );
214        readUntil( "password:" );
215        write( password );
216        readPasswordResponse( "password:", "Sorry", "Incorrect current password" );
217        write( newpassword );
218        readPasswordResponse( "password:", "Please try again", "Insufficient new password" );
219        write( newpassword );
220        readPasswordResponse( properties.getProperty( PROMPT ) + " ",
221            "Sorry", "Incorrect current password" );
222      }
223    
224      /**
225       * Disconnect from the <code>telnet</code> session.
226       */
227      public void disconnect()
228      {
229        try
230        {
231          if ( client.isConnected() ) client.disconnect();
232        }
233        catch ( Throwable t )
234        {
235          logger.log( Level.WARNING,
236              "Error closing telnet connection for user: " + user, t );
237        }
238      }
239    
240      /**
241       * Read the response from the server until the specified pattern is
242       * found.
243       *
244       * @param pattern The pattern until which the response from the server
245       *   is to be read
246       * @throws IOException If errors are encountered while reading/writing
247       *   to the socket streams.
248       */
249      private String readUntil( String pattern ) throws IOException
250      {
251        String result = null;
252        char lastChar = pattern.charAt( pattern.length() - 1 );
253        StringBuilder builder = new StringBuilder();
254        boolean found = false;
255        char ch = (char) in.read();
256    
257        while ( true )
258        {
259          //System.out.print( ch );
260          builder.append( ch );
261    
262          if ( ch == lastChar )
263          {
264            if ( builder.toString().toLowerCase().endsWith( pattern.toLowerCase() ) )
265            {
266              result = builder.toString();
267              break;
268            }
269          }
270    
271          ch = (char) in.read();
272        }
273    
274        return result;
275      }
276    
277      /**
278       * Read the response from the server after a login attempt.  Handle
279       * either a successful login or a failed login.  Failed logins are
280       * indicated when the system responds with a fresh login prompt along with
281       * an error message.
282       *
283       * @throws IOException If errors are encountered while reading/writing
284       *   to the socket streams.
285       * @throws AuthenticationFailedException If the login attempt failed due
286       *   to username or password error.
287       */
288      private String readLoginResponse()
289        throws IOException, AuthenticationFailedException
290      {
291        String result = null;
292        String pattern = properties.getProperty( PROMPT ) + " ";
293        char lastChar = pattern.charAt( pattern.length() - 1 );
294        String error = "Login incorrect";
295        char lastCharError = error.charAt( error.length() - 1 );
296        StringBuilder builder = new StringBuilder();
297        boolean found = false;
298        char ch = (char) in.read();
299    
300        while ( true )
301        {
302          //System.out.print( ch );
303          builder.append( ch );
304    
305          if ( ch == lastChar )
306          {
307            if ( builder.toString().toLowerCase().endsWith( pattern.toLowerCase() ) )
308            {
309              result = builder.toString();
310              break;
311            }
312          }
313          else if ( ch == lastCharError )
314          {
315            if ( builder.toString().endsWith( error ) )
316            {
317              result = builder.toString();
318              throw new AuthenticationFailedException( "Login failed." );
319            }
320          }
321    
322          ch = (char) in.read();
323        }
324    
325        return result;
326      }
327    
328      /**
329       * Read the response from the server after the current password has
330       * been entered during a password change request.
331       *
332       * @param success The pattern that indicates a successful entry
333       * @param error The pattern that indicates an error
334       * @param message The message to use in {@link PasswordException}.
335       * @throws IOException If errors are encountered while reading/writing
336       *   to the socket streams.
337       * @throws PasswordException If the current password specified is
338       *   incorrect.
339       */
340      private String readPasswordResponse( String success, String error,
341          String message ) throws IOException, PasswordException
342      {
343        String result = null;
344        char lastChar = success.charAt( success.length() - 1 );
345        char lastCharError = error.charAt( error.length() - 1 );
346    
347        StringBuilder builder = new StringBuilder();
348        boolean found = false;
349        char ch = (char) in.read();
350    
351        while ( true )
352        {
353          //System.out.print( ch );
354          builder.append( ch );
355    
356          if ( ch == lastChar )
357          {
358            if ( builder.toString().toLowerCase().endsWith( success.toLowerCase() ) )
359            {
360              result = builder.toString();
361              break;
362            }
363          }
364          else if ( ch == lastCharError )
365          {
366            if ( builder.toString().toLowerCase().endsWith( error.toLowerCase() ) )
367            {
368              result = builder.toString();
369              throw new PasswordException( message );
370            }
371          }
372    
373          ch = (char) in.read();
374        }
375    
376        return result;
377      }
378    
379      /**
380       * Write a response to the <code>telnet OutputStream</code>.
381       *
382       * @param response The response that is to be written to the stream.
383       * @throws IOException If errors are encountered while reading/writing
384       *   to the socket streams.
385       */
386      private void write( String response ) throws IOException
387      {
388        out.println( response );
389        out.flush();
390        //System.out.println( response );
391      }
392    
393      /**
394       * Send a command for execution to the telnet session.
395       *
396       * @see #write
397       * @see #readUntil
398       * @param command The command that is to be executed.
399       * @throws IOException If errors are encountered while sending the
400       *   command for execution.
401       */
402      private String execute( String command ) throws IOException
403      {
404        write( command );
405        return readUntil( PROMPT + " " );
406      }
407      
408      /**
409       * Returns {@link #user}.
410       *
411       * @return The value/reference of/to user.
412       */
413      public String getUser()
414      {
415        return user;
416      }
417      
418      /**
419       * Set {@link #user}.
420       *
421       * @param user The value to set.
422       */
423      public void setUser( String user )
424      {
425        this.user = user;
426      }
427    }