• Main Page
  • Related Pages
  • Packages
  • Classes
  • Files
  • File List

pbx/PBXClient.java

Go to the documentation of this file.
00001 
00002 /**
00003  *  Functionality of the private branch exchange (PBX) 
00004  */
00005 package pbx;
00006 
00007 import java.io.BufferedReader;
00008 import java.io.IOException;
00009 import java.io.InputStreamReader;
00010 import java.io.OutputStreamWriter;
00011 import java.io.PrintWriter;
00012 import java.net.Socket;
00013 import java.net.UnknownHostException;
00014 import java.util.regex.Pattern;
00015 
00016 import utils.Log;
00017 
00018 /**
00019  *  Encapsulates rudimentary functionality of a PBX to list and invite users (peers)
00020  *  to secure calls.
00021  *  
00022  *  The instances of PBXClient class expect to be connected to plain public chat server 
00023  *  that distributes (broadcasts their messages (terminated by the new-line) to all 
00024  *  other connected users (possible kryptofon peers).
00025  *  
00026  *  Communication with the upper layer (which owns instance of the PBXClient) is done
00027  *  using call-backs over the PBXClient.Context interface. 
00028  *
00029  *  @author Mikica B Kocic
00030  */
00031 public class PBXClient extends Thread 
00032 {
00033     /**
00034      *  PBX Signaling Messages' Types
00035      */
00036     public enum CMType
00037     {
00038         /** Invalid CMType */
00039         _INVALID_,
00040 
00041         /** The message sent to remote peer to start the call */
00042         INVITE, 
00043         
00044         /** The message sent back from remote peer informing about remote alerting status */
00045         RING, 
00046         
00047         /** The message sent back from the remote peer indicating accepted call */
00048         ACCEPT,
00049         
00050         /** Indicates call clear down -- either normal or abrupt (like call reject). */
00051         BYE, 
00052         
00053         /** Instant message exchanged between users with encrypted messages */
00054         INSTANTMESSAGE,
00055         
00056         /** Query all peers */
00057         LIST,
00058         
00059         /** Respond to Query all pears */
00060         ALIVE
00061     }
00062 
00063     /**
00064      *  PBX signaling message sent via call-back to the upper layer 
00065      */
00066     public class ControlMessage
00067     {
00068         public CMType msgType;      // invite, ring, ...
00069         public String peerUserId;   // remote peer's user name
00070         public String localUserId;  // local user name
00071         public String peerAddr;     // remote peer's IP address
00072         public int    peerPort;     // remote peer's UDP port
00073         public String secret;       // remote peer's public or secret key
00074         
00075         public ControlMessage( CMType msgToken, String peerUserId, String localUserId, 
00076                 String peerAddr, int peerPort, String secret ) 
00077         {
00078             this.msgType      = msgToken;
00079             this.peerUserId   = peerUserId;
00080             this.localUserId  = localUserId;
00081             this.peerAddr     = peerAddr;
00082             this.peerPort     = peerPort;
00083             this.secret       = secret;
00084         }
00085 
00086         public String getVerboseRemote () {
00087             if ( peerAddr.length () == 0 || peerPort == 0 ) {
00088                 return "'" + peerUserId + "'";
00089             } else {
00090                 return "'" + peerUserId + "' at " + peerAddr + ":" + peerPort;
00091             }
00092         }
00093     }
00094 
00095     //////////////////////////////////////////////////////////////////////////////////////
00096     
00097     /**
00098      *  Provides a call-back context for the instance of PBXClient.
00099      */
00100     public interface Context
00101     {
00102         /**
00103          *  Returns configured user ID.
00104          */
00105         public abstract String getUserId ();
00106 
00107         /**
00108          *  Updates PBX status message of the parent
00109          */
00110         public abstract void setPbxStatus( String str );
00111 
00112         /**
00113          *  Reports a system message to log 
00114          */
00115         public abstract void report( String style, String str );
00116         
00117         /**
00118          *  Reports incoming textual message
00119          */
00120         public void reportIncomingTextMessage( String cssClass, 
00121                 String userId, String message );
00122 
00123         /**
00124          *  Informs upper layer of incoming INVITE. 
00125          */
00126         public abstract void onInvite( final ControlMessage cmsg );
00127         
00128         /**
00129          *  Informs upper layer of incoming RING. 
00130          */
00131         public abstract void onRing( final ControlMessage cmsg );
00132         
00133         /**
00134          *  Informs upper layer of incoming ACCEPT. 
00135          */
00136         public abstract void onAccept( final ControlMessage cmsg );
00137         
00138         /**
00139          *  Informs upper layer of incoming BYE. 
00140          */
00141         public abstract void onBye( final ControlMessage cmsg ); 
00142         
00143         /**
00144          *  Informs upper layer of incoming IMSG. 
00145          */
00146         public abstract void onInstantMessage( final ControlMessage cmsg ); 
00147     }
00148 
00149     //////////////////////////////////////////////////////////////////////////////////////
00150 
00151     /**
00152      *  HTML CSS class for warning and error messages
00153      */
00154     private final static String WARN = "logWarn";
00155     
00156     /**
00157      *  HTML CSS class for info messages 
00158      */
00159     private final static String INFO = "logInfo";
00160 
00161     /**
00162      *  Host name or IP address of the remote chat server 
00163      */
00164     private String host = null;
00165     
00166     /**
00167      *  TCP port where to connect to on remote chat server 
00168      */
00169     private int port = -1;
00170 
00171     /**
00172      *  Chat client ID when presented to user (== host + ":" + port)
00173      */
00174     private String myID;
00175     
00176     /**
00177      *  Output stream to remote server
00178      */
00179     private PrintWriter out = null;
00180     
00181     /**
00182      *  Instance of the TCP socket to chat server.
00183      */
00184     private Socket socket = null;
00185     
00186     /**
00187      *  Indicates/enables the thread to be running
00188      */
00189     private volatile boolean running = false;
00190     
00191     /**
00192      *  Event (call-back) context for this instance of the PBXClient
00193      */
00194     private Context context = null;
00195 
00196     //////////////////////////////////////////////////////////////////////////////////////
00197 
00198     /**
00199      *  Creates new instance of <code>PBXClient</code> that posts messages 
00200      *  to specified <code>Context</code>. 
00201      *  
00202      *  @param host     host name or IP address of the chat server
00203      *  @param port     TCP port
00204      *  @param context  where to log messages (also error and info messages)
00205      */
00206     public PBXClient( String host, int port, Context context )
00207     {
00208         super( "Chat-" + host + ":" + port );
00209 
00210         this.host        = host;
00211         this.port        = port;
00212         this.context     = context;
00213         
00214         this.myID = host + ":" + port; 
00215     }
00216 
00217     /**
00218      *  Returns local IP address.
00219      */
00220     public String getLocalAddress ()
00221     {
00222         return socket.getLocalAddress ().getHostAddress ();
00223     }
00224 
00225     /**
00226      * Starts the thread.
00227      */
00228     @Override
00229     public void start ()
00230     {
00231         if ( isAlive () || running ) {
00232             return;  // Allow only one thread per instance
00233         }
00234 
00235         running = true;
00236         super.start ();
00237     }
00238 
00239     /**
00240      *  Sends message (appended with new-line) to chat server
00241      *  
00242      *  @param message   message to be sent
00243      */
00244     public void send( String message )
00245     {
00246         synchronized( this )
00247         {
00248             if ( out == null || message == null ) {
00249                 return;
00250             }
00251             
00252             out.println( message );
00253             out.flush ();
00254         }
00255     }
00256 
00257     /**
00258      *  Sends message (appended with new-line) to chat server prefixed with userId
00259      *  
00260      *  @param message    message to be sent
00261      *  @param userId    user identifier
00262      */
00263     public void send( String message, String userId )
00264     {
00265         /* Discard spaces from the userId first
00266          */
00267         userId = userId.trim().replaceAll( "\\s{1,}", "-" ); 
00268         
00269         /* Send message with userId as a prefix
00270          */
00271         send( userId + " :: " + message );
00272     }
00273 
00274     /**
00275      *  Closes the connection gracefully
00276      */
00277     public void close ()
00278     {
00279         synchronized( this )
00280         {
00281             running = false;
00282 
00283             if ( socket != null && ! socket.isClosed () ) {
00284                 try {
00285                     socket.close ();
00286                 } catch( IOException e ) {
00287                     /* ignore */
00288                 }
00289             }
00290         }
00291     }
00292     
00293     /**
00294      *  Reports a system message to log 
00295      */
00296     private void report( String style, String str )
00297     {
00298         context.report( style, str );
00299     }
00300 
00301     /**
00302      *  Reports incoming message
00303      */
00304     private void reportIncomingTextMessage( String userId, String message )
00305     {
00306         context.reportIncomingTextMessage( "chatMessage", userId, message );
00307     }
00308     
00309     /**
00310      *  Parses input message. 
00311      *  
00312      *  Syntax:
00313      *  <pre>
00314      *      [ [ &lt;userId&gt; ] : ] &lt;text-or-control&gt;
00315      *  </pre>
00316      *  where default \a userId is <code>[Anonymous]</code>.
00317      *  
00318      *  If the \a text-or-control begins with "[$]" it represents control message
00319      *  and it will not be displayed to the user.   
00320      */
00321     private void parseInputMessage( String message )
00322     {
00323         /* Parse input with syntax: [ [ <userId> ] ":: " ] <message>
00324          * where default userId is [Anonymous].
00325          */
00326         String[] parts = message.split( ":: ", 2 );
00327         String userId = "[Anonymous]";
00328         
00329         if ( message.startsWith( "WWHHOO: " ) ) {
00330             userId = "WWHHOO";
00331             message = message.substring( 8 );
00332         } else if ( parts.length == 0 ) {
00333             message = parts[0];
00334         } else if ( parts[0].trim().length () == 0 && parts.length >= 2 ) {
00335             message = parts[1];
00336         } else if ( parts.length >= 2 ) {
00337             userId  = parts[0].trim ();
00338             message = parts[1];
00339         } else {
00340             message = parts[0];
00341         }
00342 
00343         /* Now, check if we have a control message beginning with token [$]
00344          * but not comming from the anonymous user.
00345          */
00346         parts = message.trim().split( "\\s{1,}" );
00347 
00348         if ( ! userId.equals( "[Anonymous]" ) 
00349                 && parts.length >= 1 
00350                 && parts[0].equals( "[$]" ) )
00351         {
00352             parseControlMessage( userId, parts, message );
00353         }
00354         else
00355         {
00356             reportIncomingTextMessage( userId, message );
00357         }
00358     }
00359 
00360     /**
00361      *  Parses control messages.
00362      *  
00363      *  TODO Format messages in XML instead.
00364      *  (It would be too much to do for IP1 course.)
00365      *  
00366      *  Syntax:
00367      *  <pre>
00368      *     [$] INVITE    local-name   remote-ip-address   remote-udp-port  [ public-key ]   
00369      *     [$] RING      local-name   remote-ip-address   remote-udp-port  [ public-key ]
00370      *     [$] ACCEPT    local-name   remote-ip-address   remote-udp-port  [ secret-key ]
00371      *     [$] BYE       local-name [ remote-ip-address [ remote-udp-port ] ]
00372      *     [$] IMSG      local-name   encrypted-message
00373      *     [$] LIST    [ username-regex ]
00374      *     [$] ALIVE
00375      *  </pre>   
00376      */
00377     private void parseControlMessage( String remoteUserId, String[] args, String original )
00378     {
00379         assert args.length >= 1 && args[0].equals( "[$]" );
00380 
00381         /* Parse args[1] as CMType 
00382          */
00383         CMType cmType = CMType._INVALID_;
00384         
00385         if ( args[1].equalsIgnoreCase( "invite" ) ) {
00386             cmType = CMType.INVITE;
00387         } else if ( args[1].equalsIgnoreCase( "ring" ) ) {
00388             cmType = CMType.RING;
00389         } else if ( args[1].equalsIgnoreCase( "accept" ) ) {
00390             cmType = CMType.ACCEPT;
00391         } else if ( args[1].equalsIgnoreCase( "bye" ) ) {
00392             cmType = CMType.BYE;
00393         } else if ( args[1].equalsIgnoreCase( "imsg" ) ) {
00394             cmType = CMType.INSTANTMESSAGE;
00395         } else if ( args[1].equalsIgnoreCase( "list" ) ) {
00396             cmType = CMType.LIST;
00397         } else if ( args[1].equalsIgnoreCase( "alive" ) ) {
00398             cmType = CMType.ALIVE;
00399         } else {
00400             return; // ignore unknown types
00401         }
00402         
00403         String destinationUserId = null;
00404         
00405         /* Parse destination user id, then ignore loop messages and 
00406          * messages that are not explicitly for us.
00407          */
00408         if ( cmType != CMType.LIST && cmType != CMType.ALIVE && args.length >= 3 ) 
00409         {
00410             destinationUserId = args[2];
00411 
00412             if ( remoteUserId.equalsIgnoreCase( destinationUserId ) ) {
00413                 return;
00414             } else if ( ! destinationUserId.equalsIgnoreCase( context.getUserId () ) ) { 
00415                 return;
00416             }
00417         }
00418 
00419         //////////////////////////////////////////////////////////////////////////////////
00420         /* [$]  INVITE  local-name remote-ip-address remote-udp-port [ public-key ]
00421          *  0     1         2           3              4                  5 opt.      
00422          */
00423         if ( args.length >= 5  && args[1].equalsIgnoreCase( "invite" ) )
00424         {
00425             String publicKey = args.length >= 6 ? args[5] : null;
00426 
00427             try
00428             {
00429                 int port = Integer.parseInt( args[4] ); // remote port
00430 
00431                 context.onInvite( new ControlMessage( CMType.INVITE,
00432                         remoteUserId, destinationUserId, args[3], port, publicKey ) );
00433             }
00434             catch( NumberFormatException e )
00435             {
00436                 /* ignore message */
00437             }
00438         }
00439         //////////////////////////////////////////////////////////////////////////////////
00440         /* [$]  RING   local-name remote-ip-address remote-udp-port  [ public-key ]
00441          *  0     1         2           3              4                   5
00442          */
00443         else if ( args.length >= 5 && args[1].equalsIgnoreCase( "ring" ) ) 
00444         {
00445             String publicKey = args.length >= 6 ? args[5] : null;
00446 
00447             try
00448             {
00449                 int port = Integer.parseInt( args[4] ); // remote port
00450 
00451                 context.onRing( new ControlMessage( CMType.RING,
00452                         remoteUserId, destinationUserId, args[3], port, publicKey ) );
00453             }
00454             catch( NumberFormatException e )
00455             {
00456                 /* ignore message if port is not integer */
00457             }
00458         }
00459         //////////////////////////////////////////////////////////////////////////////////
00460         /* [$]  ACCEPT  local-name remote-ip-address remote-udp-port [ secret-key ]
00461          *  0     1         2           3                  4               5        
00462          */
00463         else if ( args.length >= 5 && args[1].equalsIgnoreCase( "accept" ) ) 
00464         {
00465             String secretKey = args.length >= 6 ? args[5] : null;
00466 
00467             try
00468             {
00469                 int port = Integer.parseInt( args[4] ); // remote port
00470                 
00471                 context.onAccept( new ControlMessage( CMType.ACCEPT, 
00472                         remoteUserId, destinationUserId, args[3], port, secretKey ) );
00473             }
00474             catch( NumberFormatException e )
00475             {
00476                 /* ignore message if port is not integer */
00477             }
00478         }
00479         //////////////////////////////////////////////////////////////////////////////////
00480         /* [$]   BYE   local-name [ remote-ip-address [ remote-udp-port ] ]
00481          *  0     1         2            3                   4      
00482          */
00483         else if ( args.length >= 3 && args[1].equalsIgnoreCase( "bye" ) ) 
00484         {
00485             try
00486             {
00487                 String host = args.length >= 4 ? args[3] : "";
00488                 int port = args.length >= 5 ? Integer.parseInt( args[4] ) : 0;
00489 
00490                 context.onBye( new ControlMessage( CMType.BYE,
00491                         remoteUserId, destinationUserId, host, port, null ) );
00492             }
00493             catch( NumberFormatException e )
00494             {
00495                 /* ignore message if port is not integer */
00496             }
00497         }
00498         //////////////////////////////////////////////////////////////////////////////////
00499         /* [$]   IMSG   local-name  encrypted-message
00500          *  0     1         2              3      
00501          */
00502         else if ( args.length >= 4 && args[1].equalsIgnoreCase( "imsg" ) ) 
00503         {
00504             context.onInstantMessage( new ControlMessage( CMType.INSTANTMESSAGE,
00505                     remoteUserId, destinationUserId, "", 0, args[3] ) );
00506         }
00507         //////////////////////////////////////////////////////////////////////////////////
00508         /* [$]  LIST   [ username-regex ]
00509          *  0     1          2 opt.
00510          */
00511         else if ( args.length >= 2 && args[1].equalsIgnoreCase( "list" ) )
00512         {
00513             String myUserId = context.getUserId ();
00514 
00515             report( INFO, "Listing users..." );
00516             
00517             if ( ! myUserId.isEmpty () )
00518             {
00519                 if ( args.length < 3 ) // query all users (without regex)
00520                 { 
00521                     /* Respond back to query
00522                      */
00523                     send( "[$] ALIVE", myUserId );
00524                 }
00525                 else  // case-insensitive query with regex
00526                 {
00527                     Pattern p = null;
00528                     try {
00529                         p = Pattern.compile( args[2], Pattern.CASE_INSENSITIVE );
00530                     } catch ( Throwable e ) {
00531                         /* ignored */
00532                     }
00533                     if ( p != null && p.matcher( myUserId ).find () )
00534                     {
00535                         /* Respond back to query
00536                          */
00537                         send( "[$] ALIVE", myUserId );
00538                     }
00539                 }
00540             }
00541         }
00542         //////////////////////////////////////////////////////////////////////////////////
00543         /* [$]  ALIVE
00544          *  0     1   
00545          */
00546         else if ( args.length >= 2 
00547                 && args[1].equalsIgnoreCase( "alive" ) )
00548         {
00549             report( INFO, "-- User '" + remoteUserId + "' is alive." );
00550             // TODO this might as well update some list of possible peers?
00551             // -- but that requires little more functionality on the PBX (chat) server
00552             // side.
00553         }
00554     }
00555 
00556     /**
00557      *  Broadcasts INVITE message
00558      */
00559     public void sendInvite( String remoteUserId, 
00560             String localIpAddress, int localUdpPort, String publicKey )
00561     {
00562         this.send( "[$] INVITE " + remoteUserId + " " 
00563                 + localIpAddress + " "  + localUdpPort 
00564                 + ( publicKey != null ? " " + publicKey : "" ),
00565                 context.getUserId () );
00566     }
00567 
00568     /**
00569      *  Broadcasts RING message
00570      */
00571     public void sendRing( String remoteUserId,
00572             String localIpAddress, int localUdpPort, String publicKey )
00573     {
00574         this.send( "[$] RING " + remoteUserId + " " 
00575                 + localIpAddress + " "  + localUdpPort 
00576                 + ( publicKey != null ? " " + publicKey : "" ),
00577                 context.getUserId () );
00578     }
00579 
00580     /**
00581      *  Broadcasts ACCEPT message
00582      */
00583     public void sendAccept( String remoteUserId, 
00584             String localIpAddress, int localUdpPort, String publicKey )
00585     {
00586         this.send( "[$] ACCEPT " + remoteUserId + " " 
00587                 + localIpAddress + " "  + localUdpPort 
00588                 + ( publicKey != null ? " " + publicKey : "" ),
00589                 context.getUserId () );
00590     }
00591 
00592     /**
00593      *  Broadcasts BYE message
00594      */
00595     public void sendBye( String remoteUserId,
00596             String localIpAddress, int localUdpPort )
00597     {
00598         this.send( "[$] BYE " + remoteUserId + " " 
00599                 + localIpAddress + " "  + localUdpPort, 
00600                 context.getUserId () );
00601     }
00602 
00603     /**
00604      *  Broadcasts IMSG message
00605      */
00606     public void sendInstantMessage( String remoteUserId, String encryptedMessage )
00607     {
00608         this.send( "[$] IMSG " + remoteUserId + " " + encryptedMessage, 
00609                 context.getUserId () );
00610     }
00611 
00612     /**
00613      *  Broadcasts LIST message (to list potential peers)
00614      */
00615     public void sendListPeers( String regex )
00616     {
00617         this.send( "[$] LIST" + ( regex != null ? " " + regex : "" ),
00618                 context.getUserId () );
00619     }
00620 
00621     /**
00622      *  Connects socket, then reads messages from server while <code>running</code> flag 
00623      *  is enabled. Finally, closes connection in graceful manner.
00624      *  
00625      *  Incoming messages are dispatched to the owner via the call-back context
00626      *  (see PBXClient.Context).
00627      */
00628     @Override
00629     public void run ()
00630     {
00631         Log.trace( "Thread started" );
00632 
00633         //////////////////////////////////////////////////////////////////////////////////
00634         /* Open connection
00635          */
00636         report( INFO, "Connecting to " + myID + "..." );
00637         context.setPbxStatus( "Connecting to " + myID + "..." );
00638 
00639         try
00640         {
00641             synchronized( this ) {
00642                 socket = new Socket( host, port );
00643             }
00644         }
00645         catch( UnknownHostException e )
00646         {
00647             report( WARN, "'Unknown host' exception while creating socket" );
00648             report( WARN, e.toString () );
00649             running = false;
00650         }
00651         catch( IOException e )
00652         {
00653             report( WARN, "I/O exception while connecting" );
00654             report( WARN, e.toString () );
00655             running = false;
00656         }
00657 
00658         //////////////////////////////////////////////////////////////////////////////////
00659         /* Get input stream. Consider input characters UTF-8 encoded.
00660          * TODO: It would be nice to have this as a parameter.
00661          */
00662         InputStreamReader reader = null;
00663         try
00664         {
00665             if ( socket != null ) {
00666                 reader = new InputStreamReader( socket.getInputStream (), "utf-8" );
00667             }
00668         }
00669         catch( IOException e )
00670         {
00671             report( WARN, "I/O exception while getting input stream" );
00672             report( WARN, e.toString () );
00673             running = false;
00674             reader = null;
00675         }
00676         
00677         //////////////////////////////////////////////////////////////////////////////////
00678         /* Get output stream. Encode our character strings as UTF-8.
00679          */
00680         try 
00681         {
00682             if ( socket != null ) {
00683                 out = new PrintWriter(
00684                         new OutputStreamWriter( socket.getOutputStream(), "utf-8" ),
00685                         /*autoflush*/ true );
00686             }
00687         }
00688         catch( IOException e )
00689         {
00690             report( WARN, "I/O exception while getting output stream" );
00691             report( WARN, e.toString () );
00692             running = false;
00693             out = null;
00694         }
00695 
00696         //////////////////////////////////////////////////////////////////////////////////
00697         /* Finally connected... (if running == true survived until here)
00698          */
00699         BufferedReader in = null;
00700         if ( running )
00701         {
00702             context.setPbxStatus( "Connected to " + myID );
00703             report( "logOk", "Connected to " + myID + ". Ready to communicate..." );
00704             
00705             in = new BufferedReader( reader );
00706         }
00707         
00708         //////////////////////////////////////////////////////////////////////////////////
00709         /* Read messages from the socket and dump them on log area
00710          */
00711         while( running )
00712         {
00713             try
00714             {
00715                 parseInputMessage( in.readLine () );
00716             }
00717             catch( IOException e )
00718             {
00719                 report( WARN, "Connection lost!" );
00720                 report( WARN, e.toString () );
00721                 running = false;
00722             }
00723         }
00724 
00725         report( INFO, "Closing connection " + myID + "..." );
00726 
00727         //////////////////////////////////////////////////////////////////////////////////
00728         /* Close connection gracefully
00729          */
00730         try
00731         {
00732             if ( out != null ) {
00733                 out.close ();
00734             }
00735             if ( in != null ) {
00736                 in.close ();
00737             }
00738             synchronized( this )
00739             {
00740                 if ( socket != null && ! socket.isClosed () ) {
00741                     socket.close ();
00742                 }
00743             }
00744         }
00745         catch( IOException e )
00746         {
00747             report( WARN, "I/O exception while closing connection" );
00748             report( WARN, e.toString () );
00749         }
00750         
00751         report( INFO, "... connection closed " + myID );
00752         Log.trace( "Thread completed" );
00753     }
00754 }

Generated on Thu Dec 16 2010 14:44:42 for VoIP Kryptofon by  doxygen 1.7.2