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

CryptoPhoneApp.java

Go to the documentation of this file.
00001 
00002 import java.awt.Dimension;
00003 import java.awt.Font;
00004 import java.awt.Image;
00005 import java.awt.Point;
00006 import java.awt.Toolkit;
00007 import java.awt.event.ActionEvent;
00008 import java.awt.event.ActionListener;
00009 import java.awt.event.FocusEvent;
00010 import java.awt.event.FocusListener;
00011 import java.awt.event.KeyEvent;
00012 import java.awt.event.KeyListener;
00013 import java.awt.event.WindowEvent;
00014 import java.io.BufferedReader;
00015 import java.io.BufferedWriter;
00016 import java.io.FileWriter;
00017 import java.io.IOException;
00018 import java.io.InputStream;
00019 import java.io.InputStreamReader;
00020 import java.net.InetAddress;
00021 import java.net.MalformedURLException;
00022 import java.net.URL;
00023 import java.net.UnknownHostException;
00024 import java.text.SimpleDateFormat;
00025 import java.util.Calendar;
00026 import java.util.LinkedList;
00027 import java.util.List;
00028 
00029 import javax.swing.GroupLayout;
00030 import javax.swing.JCheckBox;
00031 import javax.swing.JEditorPane;
00032 import javax.swing.JFrame;
00033 import javax.swing.JLabel;
00034 import javax.swing.JScrollPane;
00035 import javax.swing.JSeparator;
00036 import javax.swing.JTextField;
00037 import javax.swing.Timer;
00038 import javax.swing.UIManager;
00039 import javax.swing.WindowConstants;
00040 import javax.swing.plaf.ColorUIResource;
00041 
00042 import crypto.CipherEngine;
00043 import crypto.PublicEncryptor;
00044 import crypto.SymmetricCipher;
00045 
00046 import pbx.PBXClient;
00047 
00048 import protocol.CallContext;
00049 import protocol.DatagramChannel;
00050 import protocol.RemotePeer;
00051 import protocol.VoicePDU;
00052 
00053 import audio.AudioInterface;
00054 
00055 import ui.JImageButton;
00056 import ui.JSecState;
00057 import utils.Log;
00058 
00059 /**
00060  *  The Swing based GUI front-end of the Kryptofon application that
00061  *  implements simple VoIP phone and chat client with encrypted peer-to-peer
00062  *  communication.
00063  * 
00064  *  @author Mikica B Kocic
00065  */
00066 public class CryptoPhoneApp extends JFrame 
00067        implements ActionListener, KeyListener, PBXClient.Context, Log.AttentionContext
00068 {
00069     /**
00070      *  Implements java.io.Serializable interface
00071      */
00072     private static final long serialVersionUID = -1830703904673318918L;
00073 
00074     /**
00075      *  The common application title prefix.
00076      */
00077     private static final String appTitle = "IP1-10: Kryptofon";
00078 
00079     /**
00080      *  The initial message content of the input text message field.  
00081      */
00082     private static final String defaultInputMsg = 
00083         "<type in message, command or command arguments here>";
00084 
00085     /**
00086      *  The default file name where to dump log area contents
00087      *  with <code>:dump</code> command
00088      */
00089     private static final String defaultLogAreaDumpFilename = "mykf-log-area-";
00090 
00091     /**
00092      *  The host name or IP address of the remote chat server 
00093      */
00094     private String serverName = "atlas.dsv.su.se"; // Connection defaults
00095     
00096     /**
00097      *  The TCP port where to connect to on remote chat server 
00098      */
00099     private int serverPort = 9494; // Connection defaults
00100     
00101     /**
00102      *  The instance of PBX client connected to remote chat server.
00103      */
00104     private PBXClient pbxChannel = null;
00105 
00106     /**
00107      *  The last status message of the PBX channel (posted by the setPbxStatus()).
00108      */
00109     private String pbxChannelStatus = "";
00110     
00111     /**
00112      *  Main timer (elapses every 1000 ms)
00113      */
00114     private Timer mainTimer = null;
00115 
00116     /**
00117      *  The reconnect delay timer (for timing delay between two reconnections). 
00118      *  Value -1 means 'disabled'.
00119      */
00120     private int reconnectTimeout = -1;
00121     
00122     /**
00123      *  Retry counter of number of failed reconnecting attempts.
00124      */
00125     private int reconnectRetryCount = 0;
00126 
00127     /**
00128      *  The default local UDP port.
00129      */
00130     private int localUdpPort = 47000;
00131     
00132     /**
00133      *  The instance of the UDP transceiver responsible for  
00134      *  peer-to-peer communication between two Kryptofons. 
00135      */
00136     private DatagramChannel udpChannel = null;
00137     
00138     /**
00139      *  The instance of the Audio interface used to access microphone and speaker.
00140      */
00141     private AudioInterface audioInterface = null;
00142 
00143     /**
00144      *  The last PBX control message sent to us (waiting to be handled)
00145      */
00146     private PBXClient.ControlMessage lastMessageFromPBX = null;
00147 
00148     /**
00149      *  The last public key received from remote peer
00150      */
00151     PublicEncryptor remotePublicKey = null;
00152     
00153     /**
00154      *  The remote peer (its user id) that we are currently inviting to a call.
00155      *  Null if we are not inviting anyone.
00156      */
00157     private String currentInvite = null;
00158     
00159     /**
00160      *  Timer used to detect unresolved invite (i.e. invite to non-existing peer). 
00161      *  Value -1 means 'disabled'.
00162      */
00163     private int inviteTimeout = -1;
00164     
00165     /**
00166      *  Indicates whether to monitor if peer is sending voice PDUs to us.
00167      *  Should be set to 'true' always when set call established. 
00168      */
00169     private boolean monitorIfPeerIsSendingVoice = false;
00170 
00171     /**
00172      *  The log area formatted in HTML
00173      */
00174     private JEditorPane logArea;
00175     
00176     /*  The GUI components
00177      */
00178     private JSecState    securityState;
00179     private JImageButton sendButton;
00180     private JImageButton listPeersButton;
00181     private JImageButton dialButton;
00182     private JImageButton secureDialButton;
00183     private JImageButton hangupButton;
00184     private JLabel       imsgLabel;
00185     private JTextField   inputMsg;
00186     private JLabel       idLabel;
00187     private JTextField   userId;
00188     private JCheckBox    autoAnswer;
00189 
00190     /**
00191      *  Creates a new instance of the <code>CryptoPhoneApp</code>.
00192      *  
00193      *  @param args the command line arguments passed to main
00194      */
00195     public CryptoPhoneApp( String args[] )
00196     {
00197         super( appTitle );
00198         
00199         setDefaultCloseOperation( WindowConstants.EXIT_ON_CLOSE );
00200 
00201         //////////////////////////////////////////////////////// Create GUI elements /////
00202         
00203         /* Fonts and colors
00204          */
00205         Font textFont = new Font( Font.SANS_SERIF, Font.PLAIN, 14 );
00206         UIManager.put( "ToolTip.background", new ColorUIResource( 0xFF, 0xFF, 0xC7 ) );
00207 
00208         /* Application icons
00209          */
00210         List<Image> icons = new LinkedList<Image> ();
00211         icons.add( JImageButton.loadIcon( this, "favicon48.png" ).getImage () );
00212         icons.add( JImageButton.loadIcon( this, "favicon24.png" ).getImage () );
00213         icons.add( JImageButton.loadIcon( this, "favicon16.png" ).getImage () );
00214         setIconImages( icons );
00215          
00216         /* Components
00217          */
00218         inputMsg = new JTextField( defaultInputMsg, 30 );
00219         inputMsg.setFont( textFont );
00220         inputMsg.selectAll ();
00221         inputMsg.setToolTipText( 
00222                 "<html><head></head><body><p>"
00223                 + "Enter a text message or command here, then press <tt>Enter</tt><br/>"
00224                 + "If you are going to use command buttons or command mnemonics,<br/>"
00225                 + "then put command arguments here (or leave this field empty)."
00226                 + "</p></body></html>"
00227                 );
00228         
00229         imsgLabel = new JLabel (); // without the text; holds only mnemonic for the field
00230         imsgLabel.setFont( textFont );
00231         imsgLabel.setDisplayedMnemonic( KeyEvent.VK_I );
00232         imsgLabel.setLabelFor( inputMsg );
00233 
00234         securityState = new JSecState( this );
00235         
00236         sendButton = new JImageButton( this,
00237                 "Send message", "chat.png", "chat2.png" );
00238         sendButton.setMnemonic( KeyEvent.VK_ENTER );
00239         
00240         listPeersButton = new JImageButton( this,
00241                 "List kryptofon users (Alt + L)", "listPeers.png", "listPeers2.png" );
00242         listPeersButton.setMnemonic( KeyEvent.VK_L );
00243         
00244         dialButton = new JImageButton( this,
00245                 "Make a call (Alt+C)", "dial.png", "dial2.png" );
00246         dialButton.setMnemonic( KeyEvent.VK_C );
00247         
00248         secureDialButton = new JImageButton( this,
00249                 "Make a secure call (Alt+S)", "secureDial.png", "secureDial2.png" );
00250         secureDialButton.setMnemonic( KeyEvent.VK_S );
00251         
00252         hangupButton = new JImageButton( this,
00253                 "Clear or reject the call (Alt+H)", "hangup.png", "hangup2.png" );
00254         hangupButton.setMnemonic( KeyEvent.VK_H );
00255         
00256         userId = new JTextField ();
00257         userId.setFont( textFont );
00258         userId.setText( getUserId () ); // set default user id
00259         userId.setToolTipText( 
00260                 "<html><head></head><body><p>"
00261                 + "Enter name, which will be used to identify you to other kryptofon peers.<br/>"
00262                 + "The name should be unique and possibly without whitespaces."
00263                 + "</p></body></html>"
00264                 );
00265         
00266         idLabel = new JLabel( "My Name:" );
00267         idLabel.setFont( textFont );
00268         idLabel.setDisplayedMnemonic( KeyEvent.VK_N );
00269         idLabel.setLabelFor( userId );
00270 
00271         autoAnswer = new JCheckBox( "Autoanswer" );
00272         autoAnswer.setMnemonic( KeyEvent.VK_A );
00273         autoAnswer.setToolTipText( "Toggle auto-answer mode (Alt+A)" );
00274 
00275         logArea = new JEditorPane ();
00276         logArea.setFont( textFont );
00277         logArea.setEditable( false );
00278         logArea.setToolTipText( 
00279                 "<html><head></head><body><p>"
00280                 + "<strong>Log Area</strong><br/>"
00281                 + "Use <tt>:dump</tt> command to save contents into a file"
00282                 + "</p></body></html>"
00283                 );
00284 
00285         clearLogArea ();
00286 
00287         //////////////////////////////////////////////////////// GUI Layout //////////////
00288 
00289         createLayout ();
00290 
00291         /* Set inputMsg field ready for user to type in something...
00292          */
00293         inputMsg.requestFocus ();
00294 
00295         //////////////////////////////////////////////////////// Event listeners /////////
00296         
00297         createEventListeners ();
00298 
00299         //////////////////////////////////////////////////////// Window Size /////////////
00300         
00301         /* Adjust window dimensions not to exceed screen dimensions ...
00302          */
00303         Dimension win = new Dimension( 1024, 600 );
00304         Dimension scsz = Toolkit.getDefaultToolkit().getScreenSize();
00305         win.width  = Math.min( win.width, scsz.width );
00306         win.height = Math.min( win.height, scsz.height - 40 );
00307         setSize( win );
00308         setMinimumSize( new Dimension( 600, 400 ) );
00309         
00310         /* ... then center window on the screen.
00311          */
00312         setLocation( ( scsz.width - win.width )/2, ( scsz.height - 40 - win.height )/2 );
00313 
00314         //////////////////////////////////////////////////////// Enable Logging //////////
00315 
00316         /* Enable Log
00317          */
00318         Log.setEnabled( Log.ALL   , false );
00319         Log.setEnabled( Log.VERB  , false );
00320         Log.setEnabled( Log.PDU   , false );
00321         Log.setEnabled( Log.AUDIO , false );
00322         Log.setEnabled( Log.DEBUG , false );
00323         Log.setEnabled( Log.ATTN  , true  );
00324         Log.setEnabled( Log.TRACE , true  );
00325         Log.setEnabled( Log.INFO  , true  );
00326         Log.setEnabled( Log.WARN  , true  );
00327         Log.setEnabled( Log.ERROR , true  );
00328 
00329         Log.attn = this; // catch messages in 'attention' channel
00330 
00331         /* Display initial contents in the log area
00332          */
00333         displayUsage ();
00334 
00335         //////////////////////////////////////////////////////// Start services //////////
00336         
00337         /* Parse arguments: [ <host> [ <port> ] ]
00338          */
00339         if ( args.length >= 1 )
00340         {
00341             serverName = args[ 0 ];
00342             if( args.length >= 2 ) try {
00343                 serverPort = Integer.parseInt( args[ 1 ] );
00344             } catch ( NumberFormatException e ) {
00345                 Log.warn( "Using the default destination TCP port: " + serverPort );
00346             }
00347         }
00348 
00349         /* Load authorized public keys and initializes asymmetric and symmetric 
00350          * ciphering engines
00351          */
00352         CipherEngine.initialize ();
00353         
00354         /* Start UDP listener and audio interface...
00355          */
00356         startKryptofonServices ();
00357         
00358         /* Open communication link to server...
00359          */
00360         pbxChannel = new PBXClient( serverName, serverPort, this );
00361         pbxChannel.start ();
00362 
00363         /* Instantiate connection monitor timer (for reconnect supervision)
00364          */
00365         mainTimer = new Timer( 1000, this );
00366         mainTimer.start ();
00367 
00368         Log.trace( "Created instance of the " + this.getClass().toString () );
00369     }
00370 
00371     /////////////////////////////////////////////////////////// GUI LAYOUT ///////////////
00372     
00373     /**
00374      *  Creates layout of the GUI components
00375      */
00376     private void createLayout ()
00377     {
00378         /*  Upper: status, inputMsg | buttons | idLabel, userId 
00379          *  Lower: scrolled logArea 
00380          */
00381         GroupLayout layout = new GroupLayout( getContentPane () );
00382         getContentPane().setLayout( layout );
00383         layout.setAutoCreateContainerGaps( true );
00384         layout.setAutoCreateGaps( true );
00385         
00386         JScrollPane logPane = new JScrollPane ();
00387         logPane.setViewportView( logArea );
00388 
00389         JSeparator vertS1 = new JSeparator( JSeparator.VERTICAL );
00390         JSeparator vertS2 = new JSeparator( JSeparator.VERTICAL );
00391 
00392         int textH = 26; // fixed text field height
00393         int iconH = 32; // fixed icon height
00394         int iconW = 32; // fixed icon width
00395         
00396         layout.setHorizontalGroup
00397         (
00398             layout
00399                 .createParallelGroup( GroupLayout.Alignment.LEADING )
00400                 .addGroup
00401                 ( 
00402                     layout
00403                         .createSequentialGroup ()
00404                         .addGroup
00405                         ( 
00406                             layout
00407                                 .createParallelGroup( GroupLayout.Alignment.LEADING )
00408                                 .addGroup
00409                                 (
00410                                     GroupLayout.Alignment.TRAILING, 
00411                                     layout
00412                                         .createSequentialGroup ()
00413                                         .addComponent( securityState )
00414                                         .addGap( 5 )
00415                                         .addComponent( imsgLabel )
00416                                         .addGap( 0 )
00417                                         .addComponent( inputMsg )
00418                                         .addComponent( sendButton, iconW, iconW, iconW )
00419                                         .addGap( 15 )
00420                                         .addComponent( vertS1, 4, 4, 4 )
00421                                         .addGap( 10 )
00422                                         .addComponent( listPeersButton, iconW, iconW, iconW )
00423                                         .addComponent( dialButton, iconW, iconW, iconW )
00424                                         .addComponent( secureDialButton, iconW, iconW, iconW )
00425                                         .addComponent( hangupButton, iconW, iconW, iconW )
00426                                         .addGap( 15 )
00427                                         // .addComponent( autoAnswer )
00428                                         // .addGap( 10 )
00429                                         .addComponent( vertS2, 4, 4, 4 )
00430                                         .addGap( 10 )
00431                                         .addComponent( idLabel )
00432                                         .addGap( 5 )
00433                                         .addComponent( userId )
00434                                 )
00435                                 .addComponent( logPane )
00436                         )
00437                 )
00438         );
00439         
00440         layout.setVerticalGroup
00441         (
00442             layout
00443                 .createParallelGroup( GroupLayout.Alignment.LEADING )
00444                 .addGroup
00445                 (
00446                     layout
00447                         .createSequentialGroup ()
00448                         .addGroup
00449                         (
00450                             layout
00451                                 .createParallelGroup( GroupLayout.Alignment.CENTER )
00452                                 .addComponent( securityState )
00453                                 .addComponent( imsgLabel )
00454                                 .addComponent( inputMsg, textH, textH, textH )
00455                                 .addComponent( sendButton, iconH, iconH, iconH )
00456                                 .addComponent( vertS1, iconH, iconH, iconH )
00457                                 .addComponent( listPeersButton, iconH, iconH, iconH )
00458                                 .addComponent( dialButton, iconH, iconH, iconH )
00459                                 .addComponent( secureDialButton, iconH, iconH, iconH )
00460                                 .addComponent( hangupButton, iconH, iconH, iconH )
00461                                 .addComponent( vertS2, iconH, iconH, iconH )
00462                                 .addComponent( idLabel )
00463                                 .addComponent( userId, textH, textH, textH )
00464                                 //.addComponent( autoAnswer )
00465                         )
00466                         .addComponent( logPane )
00467                 )
00468         );
00469         
00470         pack();
00471     }
00472 
00473     /////////////////////////////////////////////////////////// Event Listeners //////////
00474     
00475     /**
00476      *  Creates event listeners
00477      */
00478     private void createEventListeners ()
00479     {
00480         addWindowListener( new java.awt.event.WindowAdapter () {
00481             public void windowClosing( java.awt.event.WindowEvent evt ) {
00482                 formWindowClosing( evt );
00483             }
00484         } );
00485 
00486         inputMsg.addKeyListener( this );
00487         userId.addKeyListener( this );
00488         autoAnswer.addKeyListener( this );
00489         logArea.addKeyListener( this );
00490 
00491         sendButton.addKeyListener( this );
00492         listPeersButton.addKeyListener( this );
00493         dialButton.addKeyListener( this );
00494         secureDialButton.addKeyListener( this );
00495         hangupButton.addKeyListener( this );
00496         
00497         sendButton.addActionListener( this );
00498         listPeersButton.addActionListener( this );
00499         dialButton.addActionListener( this );
00500         secureDialButton.addActionListener( this );
00501         hangupButton.addActionListener( this );
00502 
00503         inputMsg.addFocusListener(
00504                 new FocusListener () {
00505                     public void focusGained( FocusEvent evt ) {
00506                         inputMsg.selectAll ();
00507                     }
00508                     public void focusLost( FocusEvent evt ) {
00509                     }
00510                 }
00511             );
00512 
00513         userId.addFocusListener(
00514                 new FocusListener () {
00515                     public void focusGained( FocusEvent evt ) {
00516                         userId.selectAll ();
00517                     }
00518                     public void focusLost( FocusEvent evt ) {
00519                         synchronized( userId ) {
00520                             userId.setText( getUserId () );
00521                         }
00522                     }
00523                 }
00524             );
00525     }
00526 
00527     /////////////////////////////////////////////////////////// Audio & UDP services /////
00528     
00529     /**
00530      *  Starts audio interface and UDP peer-to-peer channel.
00531      */
00532     public void startKryptofonServices ()
00533     {
00534         audioInterface = new audio.AudioInterfacePCM ();
00535         udpChannel = new DatagramChannel( localUdpPort );
00536         
00537         /* Now, check if UDP channel is bound to the the local port that we requested.
00538          * If not, it means that we have multiple instance of the CryptoPhoneApp 
00539          * running possibly with the same user ID. To fix this, we're going to 
00540          * adjust our user ID with the suffix containing distance from the originally
00541          * requested UDP port, e.g. if we've requested port 47000 and now bound to 47001,
00542          * then we will adjust original 'username' to be 'username-2'.
00543          * This helps testing when starting multiple instances of the CryptoPhoneApp
00544          * on the same single-user machine (like Windows or Mac). 
00545          */
00546         int udpDifference = udpChannel.getLocalPort () - localUdpPort;
00547         if ( udpDifference > 0 ) {
00548             userId.setText( getUserId() + "-" + ( udpDifference + 1 ) );
00549             Point winPos = getLocation ();
00550             winPos.x += 40 * udpDifference; winPos.y += 40 * udpDifference;
00551             setLocation( winPos );
00552         }
00553     }
00554     
00555     /**
00556      *  Stops audio interface and UDP peer-to-peer channel.
00557      */
00558     public void stopKryptofonServices ()
00559     {
00560         /* Clear existing call, if any
00561          */
00562         executeCommand( ":bye", null );
00563 
00564         /* Stop listening UDP
00565          */
00566         if ( udpChannel != null ) {
00567             udpChannel.stop ();
00568         }
00569 
00570         /* Stop audio services
00571          */
00572         if ( audioInterface != null ) {
00573             audioInterface.stopPlay ();
00574             audioInterface.stopRecording ();
00575             audioInterface.cleanUp ();
00576         }
00577 
00578         /* Done
00579          */
00580         udpChannel = null;
00581         audioInterface = null;
00582     }
00583 
00584     /////////////////////////////////////////////////////////// GUI Events ///////////////
00585     
00586     /**
00587      *  Parses input text entered by the user in inputMsg.
00588      */
00589     private void sendButton_Clicked () 
00590     {
00591         if ( defaultInputMsg.equalsIgnoreCase( inputMsg.getText () ) ) { 
00592             return; // ignore default input message
00593         }
00594 
00595         parseInputMessage ();
00596     }
00597 
00598     /**
00599      *  Performs :list command.
00600      */
00601     private void listPeersButton_Clicked () 
00602     {
00603         executeCommand( ":list", null );
00604     }
00605 
00606     /**
00607      *  Performs :invite command.
00608      */
00609     private void dialButton_Clicked () 
00610     {
00611         /* If there is outstanding INVITE waiting to be accepted,
00612          * do accept incoming call instead of making new outgoing call.
00613          */
00614         if ( this.lastMessageFromPBX == null ) {
00615             executeCommand( ":invite", tokenizeInputMessage () );
00616         } else {
00617             acceptIncomingCall( /*secured*/ false );
00618         }
00619     }
00620 
00621     /**
00622      *  Performs :invite+ command.
00623      */
00624     private void secureDialButton_Clicked () 
00625     {
00626         /* If there is outstanding INVITE waiting to be accepted,
00627          * do accept incoming call instead of making new outgoing call.
00628          */
00629         if ( this.lastMessageFromPBX == null ) {
00630             executeCommand( ":invite+", tokenizeInputMessage () );
00631         } else {
00632             acceptIncomingCall( /*secured*/ true );
00633         }
00634     }
00635 
00636     /**
00637      *  Performs :bye command.
00638      */
00639     private void hangupButton_Clicked () 
00640     {
00641         executeCommand( ":bye", tokenizeInputMessage () );
00642     }
00643 
00644     /**
00645      *  Closes application by gracefully terminating all threads.
00646      */
00647     private void formWindowClosing( WindowEvent evt )
00648     {
00649         stopKryptofonServices ();
00650         
00651         if ( pbxChannel != null ) {
00652             pbxChannel.close ();
00653         }
00654 
00655         System.exit( 0 );
00656     }
00657 
00658     /**
00659      *  Implements <code>KeyListener</code>'s key pressed event.
00660      *  Parses input message on ENTER (the same action as it was sendButton_Clicked).
00661      */
00662     public void keyPressed( KeyEvent ke ) {
00663         
00664         int keyCode = ke.getKeyCode ();
00665         
00666         if( ke.getSource () == inputMsg && keyCode == KeyEvent.VK_ENTER )
00667         {
00668             if ( defaultInputMsg.equalsIgnoreCase( inputMsg.getText () ) ) { 
00669                 return; // ignore default input message
00670             }
00671             
00672             parseInputMessage ();
00673         }
00674         else if( ke.getSource () == userId && keyCode == KeyEvent.VK_ENTER )
00675         {
00676             inputMsg.requestFocus ();
00677         }
00678         else if( keyCode == KeyEvent.VK_F1 )
00679         {
00680             executeCommand( ":help", null );
00681         }
00682     }
00683 
00684     /**
00685      *  Implements <code>KeyListener</code>'s key released event.
00686      */
00687     public void keyReleased( KeyEvent ke )
00688     {
00689         /* unused */
00690     }
00691     
00692     /**
00693      *  Implements <code>KeyListener</code>'s key typed event.
00694      */
00695     public void keyTyped( KeyEvent ke )
00696     {
00697         /* unused */
00698     }
00699 
00700     /**
00701      *  Handles events from Swing timer and buttons.
00702      */
00703     public void actionPerformed( ActionEvent ae )
00704     {
00705         if ( ae.getSource () == mainTimer ) {
00706             mainTimerEvent ();
00707         } else if ( ae.getSource () == sendButton ) {
00708             sendButton_Clicked ();
00709         } else if ( ae.getSource () == listPeersButton ) {
00710             listPeersButton_Clicked ();
00711         } else if ( ae.getSource () == dialButton ) {
00712             dialButton_Clicked ();
00713         } else if ( ae.getSource () == secureDialButton ) {
00714             secureDialButton_Clicked ();
00715         } else if ( ae.getSource () == hangupButton ) {
00716             hangupButton_Clicked ();
00717         }
00718     }
00719 
00720     /**
00721      *  Handles events from application's mainTimer (an instance of the Swing timer).
00722      *  It monitors status of:
00723      *  
00724      *   -#  PBXClient connection 
00725      *   -#  connection to remote peer (if any)
00726      *   -#  awaiting acknowledgment for our last invite message (if any) 
00727      *  
00728      *  If PBXClient connection is detected to be down, the procedure will try to 
00729      *  reconnect to chat server after some period of time. If reconnection retry 
00730      *  count exceeded maximum, timer will stop retrying.
00731      *  
00732      *  In case of dead remote peer (not sending UDP packets to us), 
00733      *  udpChannel.isPearDead() timer will clear down the call.
00734      *  
00735      *  In case of unacknowledged invite message, inviteTimeout timer will 
00736      *  cancel current inviting.
00737      */
00738     private void mainTimerEvent ()
00739     {
00740         //////////////////////////////////////////////////////////////////////////////////
00741         /* Monitor remote peer's status
00742          */
00743         RemotePeer peer = udpChannel.getRemotePeer();
00744         if ( monitorIfPeerIsSendingVoice 
00745                 && peer != null && udpChannel.isPearDead( /*timeout-millis*/ 2500 ) ) 
00746         {
00747             monitorIfPeerIsSendingVoice = false;
00748             
00749             report( "logWarn", "Warning: Not receiving voice from '" 
00750                     + peer.getRemoteUserId () + "'; Maybe it's dead?" );
00751         }
00752 
00753         //////////////////////////////////////////////////////////////////////////////////
00754         /* Monitor the last issued invite
00755          */
00756         if ( inviteTimeout >= 0 ) // invite timeout is active
00757         {
00758             if ( --inviteTimeout < 0 ) // invite timeout reached 
00759             {
00760                 report( "logError", "It seems that kryptofon user '" + currentInvite 
00761                         + "' is not connected." );
00762                 report( "logInfo", "Use :list to query available users..." );
00763                 logMessage( "<hr/>" );
00764 
00765                 inviteTimeout = -1;
00766                 currentInvite = null;
00767             }
00768         }
00769         
00770         //////////////////////////////////////////////////////////////////////////////////
00771         /* Monitor current connection.
00772          */
00773         final int maxRetryCount = 3;
00774         final int reconnectDelay = 2;
00775         
00776         if ( pbxChannel != null && pbxChannel.isAlive () ) {
00777             reconnectTimeout = -1; // disables timer
00778             return;
00779         }
00780 
00781         if ( reconnectRetryCount >= maxRetryCount ) {
00782 
00783             if ( reconnectRetryCount == maxRetryCount ) {
00784                 
00785                 ++reconnectRetryCount;
00786                 
00787                 logMessage( "<hr/><div class='logDiv'>"
00788                     + "<span class='logError'>Press ENTER to quit or type<br/><br/>"
00789                     + "&nbsp;&nbsp; :open [ &lt;hostname&gt; [ &lt;port&gt; ] ]<br/><br/>"
00790                     + "to open new connection...</span><br/><br/></div>" 
00791                     );
00792                 setPbxStatus( "Dead" );
00793                 
00794                 /* Reset defaults */
00795                 serverName = "atlas.dsv.su.se";
00796                 serverPort = 9494;
00797             }
00798             
00799             return; // Leave to the user to quit by pressing ENTER
00800         } 
00801             
00802         if ( reconnectTimeout < 0 )  {
00803             setPbxStatus( "Disconnected" );
00804             logMessage( "<hr/><div class='logDiv'>Reconnecting in " 
00805                     + reconnectDelay + " seconds...</div>"
00806                     );
00807             reconnectTimeout = reconnectDelay; // start timer
00808             return;
00809         }
00810         
00811         if ( --reconnectTimeout > 0 ) {
00812             return;
00813         }
00814         
00815         logMessage( "<div class='logDiv'>Retry #"
00816                + ( ++reconnectRetryCount ) 
00817                + " of max " 
00818                + maxRetryCount
00819                + ":<br/></div>"
00820                );
00821 
00822         reconnectTimeout = -1; // disables timer and restarts new connection
00823 
00824         pbxChannel = new PBXClient( serverName, serverPort , this );
00825         pbxChannel.start ();
00826     }
00827 
00828     /////////////////////////////////////////////////////////// Log Area Messages ////////
00829 
00830     /**
00831      *  Logs message formated with limited HTML (limited because of JEditorPane)
00832      */
00833     public void logMessage( final String str )
00834     {
00835         java.awt.EventQueue.invokeLater( 
00836                 new Runnable() {
00837                     public void run() {
00838                         synchronized( logArea )
00839                         {
00840                             /* Append the string to the end of <body> element...
00841                              */
00842                             String html = logArea.getText ();
00843                             html = html.replace( "</body>", str + "\n</body>" );
00844                             logArea.setText( html );
00845                         }
00846                     }
00847                 }
00848             );
00849     }
00850 
00851     /////////////////////////////////////////////////////////// PBXClient.Context ////////
00852     
00853     /**
00854      *  Displays attention messages to the user from Log subsystem.
00855      */
00856     @Override
00857     public void attention( String message ) 
00858     {
00859         if ( message.startsWith( "Error" ) ) {
00860             report( "logError", message );
00861         } else {
00862             report( "logInfo", message );
00863         }
00864     }
00865     
00866     /////////////////////////////////////////////////////////// PBXClient.Context ////////
00867 
00868     /**
00869      *  Returns configured user ID (username).
00870      */
00871     @Override
00872     public String getUserId ()
00873     {
00874         String newId = null;
00875         
00876         synchronized( userId )
00877         {
00878             String oldId = userId.getText ().trim();
00879 
00880             /* Default user id, if not specified */
00881             if ( oldId.isEmpty () ) {
00882                 oldId = System.getProperty( "user.name" );
00883             }
00884             
00885             /* Ouch. Again empty. Then we are really an anonymous */
00886             if ( oldId.isEmpty () ) {
00887                 oldId = "[Anonymous]";
00888             }
00889 
00890             /* Discard spaces from the local userId */
00891             newId = oldId.replaceAll( "\\s{1,}", "-" );
00892         }
00893         
00894         return newId;
00895     }
00896 
00897     /**
00898      *  Updates status message by updating window title
00899      */
00900     @Override
00901     public void setPbxStatus( String str )
00902     {
00903         final String strCopy = new String( str );
00904         
00905         java.awt.EventQueue.invokeLater( 
00906                 new Runnable() {
00907                     public void run() {
00908                         synchronized( pbxChannelStatus )
00909                         {
00910                             pbxChannelStatus = appTitle + "; " + strCopy;
00911                             setTitle( pbxChannelStatus );
00912                         }
00913                     }
00914                 }
00915             );
00916     }
00917 
00918     /**
00919      *  Reports a system message to log 
00920      */
00921     @Override
00922     public void report( String cssClass, String str )
00923     {
00924         logMessage(
00925             "<div class='logDiv'>" + Log.nowMillis () 
00926                      + "&nbsp;&nbsp;<span class='" + cssClass + "'>"
00927                      + Log.EscapeHTML( str )
00928                      + "</span></div>"
00929             );
00930     }
00931 
00932     /**
00933      *  Reports incoming message
00934      */
00935     @Override
00936     public void reportIncomingTextMessage( String cssClass, 
00937             String userId, String message )
00938     {
00939         logMessage(
00940                 "<div class='logDiv'>" + Log.nowMillis () 
00941                          + "&nbsp;&nbsp;" 
00942                          + Log.EscapeHTML( userId ).trim () 
00943                          + ": <span class='" + cssClass + "'>"
00944                          + Log.EscapeHTML( message ).trim ()
00945                          + "</span></div>"
00946                 );
00947     }
00948     
00949     /**
00950      *  Handles on INVITE call-back from the PBXClient.
00951      */
00952     @Override
00953     public void onInvite( final PBXClient.ControlMessage m )
00954     {
00955         java.awt.EventQueue.invokeLater( 
00956                 new Runnable() {
00957                     public void run() {
00958                         deferredOnInvite( m );
00959                     }
00960                 }
00961             );
00962     }
00963     
00964     /**
00965      *  Handles on RING call-back from the PBXClient.
00966      */
00967     @Override
00968     public void onRing( final PBXClient.ControlMessage m )
00969     {
00970         java.awt.EventQueue.invokeLater( 
00971                 new Runnable() {
00972                     public void run() {
00973                         deferredOnRing( m );
00974                     }
00975                 }
00976             );
00977     }
00978     
00979     /**
00980      *  Handles on ACCEPT call-back from the PBXClient.
00981      */
00982     @Override
00983     public void onAccept( final PBXClient.ControlMessage m )
00984     {
00985         java.awt.EventQueue.invokeLater( 
00986                 new Runnable() {
00987                     public void run() {
00988                         deferredOnAccept( m );
00989                     }
00990                 }
00991             );
00992     }
00993     
00994     /**
00995      *  Handles on BYE call-back from the PBXClient.
00996      */
00997     @Override
00998     public void onBye( final PBXClient.ControlMessage m )
00999     {
01000         java.awt.EventQueue.invokeLater( 
01001                 new Runnable() {
01002                     public void run() {
01003                         deferredOnBye( m );
01004                     }
01005                 }
01006             );
01007     }
01008     
01009     /**
01010      *  Handles on IMSG call-back from the PBXClient.
01011      */
01012     @Override
01013     public void onInstantMessage( final PBXClient.ControlMessage m )
01014     {
01015         java.awt.EventQueue.invokeLater( 
01016                 new Runnable() {
01017                     public void run() {
01018                         deferredOnInstantMessage( m );
01019                     }
01020                 }
01021             );
01022     }
01023     
01024     /////////////////////////////////////////////////////////// Command Line parsers /////
01025 
01026     /**
01027      *  Tokenizes inputMsg into words and returns array of strings.
01028      */
01029     private String[] tokenizeInputMessage ()
01030     {
01031         String message = inputMsg.getText();
01032         
01033         if ( defaultInputMsg.equalsIgnoreCase( message ) ) { 
01034             return new String[0]; // ignore default input message
01035         }
01036 
01037         /* Split string into words, removing all leading, trailing 
01038          * and superfluous white-spaces between words
01039          */
01040         String[] words = message.trim().split( "\\s{1,}" );
01041         
01042         if ( words.length >= 1 && words[0].isEmpty () ) {
01043             return new String[0]; // if contains empty words, then it is really empty
01044         }
01045 
01046         return words;
01047     }
01048 
01049     /**
01050      *  Parses input message from inputMsg and sends it to chat server. If it is 
01051      *  a command (the first word is prefixed with ':' character) it spawns 
01052      *  executeCommand() for further parsing.
01053      */
01054     private void parseInputMessage ()
01055     {
01056         /* Split string into words, removing all leading, trailing 
01057          * and superfluous white-spaces between words
01058          */
01059         String[] words = tokenizeInputMessage ();
01060         
01061         String cmd = words.length >= 1 ? words[0].toLowerCase () : "";
01062 
01063         /* If it is a command, parse it separately
01064          */
01065         if ( cmd.startsWith( ":" ) ) 
01066         {
01067             int argCount = words.length - 1;
01068             int argOffset = 1;
01069             
01070             /* If ":" is alone (i.e. separated by whitespace from the next word), 
01071              * then join it with the next word.
01072              */
01073             if ( cmd.equals( ":" ) && words.length >= 2 ) {
01074                 cmd = cmd + words[1];
01075                 argCount = words.length - 2;
01076                 argOffset = 2;
01077             }
01078 
01079             /* Prepare command arguments... */
01080             String[] args = new String[ argCount ];
01081             System.arraycopy( words, argOffset, args, 0, argCount );
01082             
01083             /* ... then execute command */
01084             if ( executeCommand( cmd, args ) ) {
01085                 inputMsg.setText( "" );
01086             }
01087             return;
01088         }
01089 
01090         if ( pbxChannel == null || ! pbxChannel.isAlive () ) // confirms dead-server question
01091         {
01092             formWindowClosing( null );
01093             System.exit( 0 );
01094         }
01095 
01096         /* Default task: send message to chat server
01097          */
01098         String inputInstantMessage = inputMsg.getText ();
01099 
01100         if ( defaultInputMsg.equalsIgnoreCase( inputInstantMessage ) ) 
01101         { 
01102             /* ignore default input message */
01103             return;
01104         }
01105         else if ( getUserId().isEmpty () ) // no user id
01106         {
01107             pbxChannel.send( inputInstantMessage );
01108             inputMsg.setText( "" );
01109             return;
01110         }
01111 
01112         sendInstantMessage( inputInstantMessage, /*forceUnencrypted*/ false );
01113 
01114         inputMsg.setText( "" );
01115     }
01116 
01117     /**
01118      *  Sends chat message (encrypted if we have established secure channel
01119      *  with the user).
01120      */
01121     private void sendInstantMessage( String message, boolean forceUnencrypted )
01122     {
01123         /* Get symmetric ciphering engine for PDUs and messages, if any.
01124          */
01125         RemotePeer remotePeer = udpChannel.getRemotePeer ();
01126         SymmetricCipher cipher = udpChannel.getUsedSymmetricCipher ();
01127         if ( forceUnencrypted || remotePeer == null || cipher == null ) 
01128         {
01129             /* Send un-encrypted message if there is no common ciphering engine 
01130              */
01131             pbxChannel.send( message, getUserId () );
01132             inputMsg.setText( "" );
01133             return;
01134         }
01135 
01136         /* Send encrypted message to peer and echo the highlighted message back to our 
01137          * user. Wwe must echo the message because our PBXClient will not catch chat 
01138          * servers broadcast with the encrypted message, as the instant message will 
01139          * be explicitly directed to the remote peer. 
01140          */
01141         String secret = cipher.encrypt( message );
01142         if ( secret != null )
01143         {
01144             pbxChannel.sendInstantMessage( remotePeer.getRemoteUserId(), secret );
01145             
01146             reportIncomingTextMessage( "secureMessage", 
01147                     getUserId() + " [encrypted]", message ); 
01148         }
01149     }
01150 
01151     /**
01152      *  Parses commands prefixed with ':' character. Recognized commands are:
01153      *  <pre>
01154      *  VoIP Calls:
01155      *  
01156      *     :inv[ite]    username             aliases: :ca[ll]
01157      *     :inv[ite]+   username             aliases: :ca[ll]+
01158      *     :acc[ept]                         aliases: :ans[wer]
01159      *     :by[e]                            aliases: :ha[ngup]
01160      *     :shk[ey]
01161      *     
01162      *  VoIP Peers:
01163      *  
01164      *     :li[st]    [ username-regex ]
01165      *     
01166      *  Chat Messages:
01167      *
01168      *     :br[oadcast]  message
01169      *     
01170      *  Connection to Chat Server:
01171      *  
01172      *     :clo[se]
01173      *     :op[en]    [ host  [ port ] ] 
01174      *     
01175      *  Application:
01176      *     :cl[ear]s[creen]
01177      *     :reauth
01178      *     :newsecret  [ algorithm [ keysize ] ]
01179      *     :du[mp]
01180      *     :ex[it]                           aliases: :qu[it]
01181      *     :he[lp]
01182      *  </pre>
01183      *  
01184      *  @param cmd  command name; must begin with ":"
01185      *  @param args command arguments; may be null or empty array
01186      *  @return true if command is parsed and executed
01187      */
01188     private boolean executeCommand( String cmd, String[] args )
01189     {
01190         cmd = cmd.trim().toLowerCase();
01191 
01192         boolean executed = false;
01193 
01194         if ( args == null ) {
01195             args = new String[0];
01196         }
01197 
01198         /* Note bellow that we use both equals() and matches() to resolve command.
01199          * We need equals() to speed-up lookup for those commands that are executed
01200          * internally by directly calling executeCommand() with hard-coded argument
01201          * (contrary to when executeCommand() is called with commnands entered by
01202          * the user).
01203          */
01204 
01205         /*------------------------------------------------------------------------------*/
01206         if ( cmd.equals( ":list" ) || cmd.matches( "^:li(st?)?$" ) )
01207         {
01208             pbxChannel.sendListPeers( args.length >= 1 ? args[0] : null );
01209 
01210             executed = true;
01211         }
01212         /*------------------------------------------------------------------------------*/
01213         else if ( cmd.equals( ":who" ) )
01214         {
01215             pbxChannel.send( "wwhhoo" );
01216 
01217             executed = true;
01218         }
01219         /*------------------------------------------------------------------------------*/
01220         else if ( cmd.equals( ":bye" ) 
01221                || cmd.matches( "^:by(e)?$" ) 
01222                || cmd.matches( "^:ha(n(g(up?)?)?)?$" ) )
01223         {
01224             RemotePeer remotePeer = udpChannel.getRemotePeer ();
01225 
01226             if ( remotePeer != null ) 
01227             {
01228                 report( "logInfo", "***** Call Ended *****" );
01229                 logMessage( "<hr/>" );
01230 
01231                 /* Send 'bye' message to remote peer
01232                  */
01233                 pbxChannel.sendBye( remotePeer.getRemoteUserId (),  
01234                         pbxChannel.getLocalAddress (), udpChannel.getLocalPort () );
01235             }
01236             else if ( currentInvite != null )
01237             {
01238                 report( "logInfo", "Invite cancelled." );
01239                 logMessage( "<hr/>" );
01240 
01241                 /* Send 'bye' message to remote peer
01242                  */
01243                 pbxChannel.sendBye( currentInvite,  
01244                         pbxChannel.getLocalAddress (), udpChannel.getLocalPort () );
01245             }
01246             else if ( this.lastMessageFromPBX != null ) // call waiting to be accepted
01247             {
01248                 PBXClient.ControlMessage m = this.lastMessageFromPBX;
01249                 
01250                 String verboseRemote = m.getVerboseRemote ();
01251                 
01252                 report( "logInfo", "Rejecting invite from " + verboseRemote );
01253                 logMessage( "<hr/>" );
01254 
01255                 /* Send 'bye' message to remote peer
01256                  */
01257                 pbxChannel.sendBye( m.peerUserId,  
01258                         pbxChannel.getLocalAddress (), udpChannel.getLocalPort () );
01259                 
01260                 this.lastMessageFromPBX = null;
01261             }
01262 
01263             udpChannel.removePeer ();
01264             audioInterface.stopRinging ();
01265             userId.setEnabled( true ); // enable back changing user ID
01266             
01267             lastMessageFromPBX = null;
01268             remotePublicKey = null;
01269 
01270             securityState.setState( JSecState.State.UNSECURED );
01271             setTitle( pbxChannelStatus );
01272             
01273             inviteTimeout = -1;
01274             currentInvite = null;
01275 
01276             executed = true;
01277         }
01278         /*------------------------------------------------------------------------------*/
01279         else if ( cmd.equals( ":invite+" )
01280                || cmd.matches( "^:inv(i(te?)?)?\\+$" ) 
01281                || cmd.matches( "^:ca(ll?)?\\+$" ) )
01282         {
01283             /* Calls another user WITH encryption enabled 
01284              */
01285             if ( udpChannel.hasRemotePeer () || currentInvite != null )
01286             {
01287                 report( "logError", "Call in progress. Hang up first!" );
01288                 
01289                 executed = true;
01290             }
01291             else if ( args.length >= 1 && pbxChannel.isAlive () )
01292             {
01293                 logMessage( "<hr/>" );
01294                 report( "logInfo", "Inviting '" + args[0] 
01295                         + "' to encrypted voice call..." );
01296 
01297                 currentInvite = args[0];
01298                 inviteTimeout = 3;
01299                 
01300                 userId.setEnabled( false ); // disable changing user ID
01301 
01302                 pbxChannel.sendInvite( currentInvite, pbxChannel.getLocalAddress (),
01303                         udpChannel.getLocalPort (), CipherEngine.getSignedPublicKey () );
01304 
01305                 executed = true;
01306             }
01307         }
01308         /*------------------------------------------------------------------------------*/
01309         else if ( cmd.equals( ":invite" )
01310                || cmd.matches( "^:inv(i(te?)?)?$" ) 
01311                || cmd.matches( "^:ca(ll?)?$" ) )
01312         {
01313             /* Calls another user WITHOUT encryption enabled 
01314              */
01315             if ( udpChannel.hasRemotePeer () || currentInvite != null )
01316             {
01317                 report( "logError", "Call in progress. Hang up first!" );
01318                 
01319                 executed = true;
01320             }
01321             else if ( args.length >= 1 && pbxChannel.isAlive () )
01322             {
01323                 logMessage( "<hr/>" );
01324                 report( "logInfo", "Inviting '" + args[0]
01325                         + "' to un-encrypted voice call..." );
01326                 
01327                 currentInvite = args[0];
01328                 inviteTimeout = 3;
01329                 
01330                 userId.setEnabled( false ); // disable changing user ID
01331 
01332                 pbxChannel.sendInvite( args[0], pbxChannel.getLocalAddress (),
01333                         udpChannel.getLocalPort (), null );
01334 
01335                 executed = true;
01336             }
01337         }
01338         /*------------------------------------------------------------------------------*/
01339         else if ( cmd.equals( ":accept" )
01340                || cmd.matches( "^:acc(e(pt?)?)?$" ) 
01341                || cmd.matches( "^:ans(w(er?)?)?$" ) )
01342         {
01343             acceptIncomingCall( /*secured*/ true );
01344 
01345             executed = true;
01346         }
01347         /*------------------------------------------------------------------------------*/
01348         else if ( cmd.equals( ":broadcast" )
01349                || cmd.matches( "^:br(o(a(d(c(a(st?)?)?)?)?)?)?$" ) )
01350         {
01351             if ( args.length >= 1 ) 
01352             {
01353                 StringBuffer msg = new StringBuffer ();
01354                 for ( String s : args ) {
01355                     if ( msg.length () > 0 ) {
01356                         msg.append( " " );
01357                     }
01358                     msg.append( s );
01359                 }
01360                 
01361                 sendInstantMessage( msg.toString () , /*forceUnencrypted*/ true );
01362     
01363                 executed = true;
01364             }
01365         }
01366         /*------------------------------------------------------------------------------*/
01367         else if ( cmd.equals( ":mykey" )
01368                || cmd.matches( "^:my(k(ey?)?)?$" ) )
01369         {
01370             sendInstantMessage( "\f========= BEGIN PUBLIC KEY =========\f\f"
01371                     + CipherEngine.getNamedPublicKey ()
01372                     + "\f\f========= END PUBLIC KEY ==========="
01373                     , /*forceUnencrypted*/ false );
01374         }
01375         /*------------------------------------------------------------------------------*/
01376         else if ( cmd.equals( ":close" )
01377                || cmd.matches( "^:clo(se?)?$" ) )
01378         {
01379             logMessage( "<hr/>" );
01380             reconnectRetryCount = Integer.MAX_VALUE; // suppresses reconnection
01381             pbxChannel.close ();
01382 
01383             executed = true;
01384         }
01385         /*------------------------------------------------------------------------------*/
01386         else if ( cmd.equals( ":open" )
01387                || cmd.matches( "^:op(en?)?$" ) )
01388         {
01389             /* Opens new connection to chat server
01390              */
01391             if ( args.length >= 1 )
01392             {
01393                 /* Parse arguments first: host name and port
01394                  */
01395                 serverName = args[0];
01396                 serverPort = 2000; // the default port
01397                 
01398                 if( args.length >= 2 ) try {
01399                     serverPort = Integer.parseInt( args[1] );
01400                 } catch ( NumberFormatException e ) {
01401                     report( "logError", "The port must be integer." );
01402                     return false;
01403                 }
01404             }
01405             
01406             logMessage( "<hr/>" );
01407 
01408             reconnectRetryCount = Integer.MAX_VALUE; // suppresses reconnection
01409             pbxChannel.close ();
01410             
01411             pbxChannel = new PBXClient( serverName, serverPort , this );
01412             pbxChannel.start ();
01413             reconnectRetryCount = 0;
01414 
01415             executed = true;
01416         }
01417         /*------------------------------------------------------------------------------*/
01418         else if ( cmd.equals( ":exit" )
01419                || cmd.matches( "^:ex(it?)?$" ) || cmd.matches( "^:qu(it?)?$" ) )
01420         {
01421             stopKryptofonServices ();
01422             pbxChannel.close ();
01423             System.exit( 0 );
01424         }
01425         /*------------------------------------------------------------------------------*/
01426         else if ( cmd.equals( ":reauth" ) )
01427         {
01428             CipherEngine.reloadAuthorizedPublicKeys ();
01429             
01430             executed = true;
01431         }
01432         /*------------------------------------------------------------------------------*/
01433         else if ( cmd.equals( ":newsecret" ) )
01434         {
01435             String algorithm = "Blowfish";
01436             int keySize = 32;
01437             
01438             if ( args.length >= 1 ) {
01439                 algorithm = args[0];
01440             }
01441 
01442             if ( args.length >= 2 ) {
01443                 try {
01444                     keySize = Integer.parseInt( args[1] );
01445                 } catch ( NumberFormatException e ) {
01446                     report( "logError", "The key size must be integer." );
01447                     return false;
01448                 }
01449             }
01450 
01451             if ( ! CipherEngine.generateNewSecret( algorithm, keySize, /*verbose*/true ) ) 
01452             {
01453                 /* fail back to blowfish (not to leave user without symmetric cipher)
01454                  */
01455                 CipherEngine.generateNewSecret( "Blowfish", 32, /*verbose*/false ); 
01456                 return false;
01457             }
01458 
01459             executed = true;
01460         }
01461         /*------------------------------------------------------------------------------*/
01462         else if ( cmd.equals( ":cls" )
01463                || cmd.matches( "^:cl(ear)?s(c(r(e(en?)?)?)?)?$" ) )
01464         {
01465             clearLogArea (); // clears screen
01466             displayUsage ();
01467 
01468             executed = true;
01469         }
01470         /*------------------------------------------------------------------------------*/
01471         else if ( cmd.equals( ":help" )
01472                || cmd.matches( "^:he(lp?)?$" ) )
01473         {
01474             displayHelp ();
01475 
01476             executed = true;
01477         }
01478         /*------------------------------------------------------------------------------*/
01479         else if ( cmd.equals( ":dump" )
01480                || cmd.matches( "^:du(mp?)?$" ) )
01481         {
01482             dumpLogArea( args.length >= 1 ? args[0] : null );
01483 
01484             executed = true;
01485         }
01486         /*------------------------------------------------------------------------------*/
01487         
01488         return executed;
01489     }
01490 
01491     //////////////////////////////////////////////////////////// PBX functionality ///////
01492     
01493     /**
01494      *  On INVITE call-back is triggered when PBXClient receives inviting message
01495      *  (indicating that someone is calling us).
01496      *  
01497      *  It starts ringing and if the application is in auto-answer mode, it
01498      *  accepts incoming call immediately. Otherwise it presents alerting message
01499      *  to the user with information about who's calling.
01500      */
01501     public void deferredOnInvite( final PBXClient.ControlMessage m )
01502     {
01503         if ( m.peerPort < 1 || m.peerPort > 65535 ) {
01504             return;
01505         }
01506         
01507         String verboseRemote = m.getVerboseRemote ();
01508 
01509         /* Reject new calls if we already have the call in progress
01510          */
01511         if ( udpChannel.hasRemotePeer () ) 
01512         {
01513             /* Send 'bye' message to remote peer who is inviting us
01514              */
01515             pbxChannel.sendBye( m.peerUserId, "0.0.0.0", 0 );  
01516             return;
01517         }
01518 
01519         this.lastMessageFromPBX = m;
01520         
01521         logMessage( "<hr/>" );
01522         report( "logInfo", "User " + verboseRemote + " is inviting us..." );
01523 
01524         if ( m.secret == null ) {
01525             setTitle( appTitle + "; Incoming PLAIN call from " + verboseRemote ); 
01526         } else {
01527             setTitle( appTitle + "; Incoming ENCRYPTED call from " + verboseRemote ); 
01528         }
01529         
01530         this.audioInterface.startRinging ();
01531         userId.setEnabled( false ); // disable changing user ID
01532 
01533         if ( autoAnswer.isSelected () )
01534         {
01535             report( "logInfo", "Auto-answering the call..." );
01536             acceptIncomingCall( /*secured*/ true );
01537         }
01538         else
01539         {
01540             tryToVerifyInvitingCall( /*silent*/ false );
01541             report( "logInfo", "Respond with :accept to answer the call!" );
01542             
01543             /* Send 'ringing' message to remote peer with our public key
01544              */
01545             pbxChannel.sendRing( m.peerUserId,  pbxChannel.getLocalAddress (), 
01546                     udpChannel.getLocalPort (), CipherEngine.getSignedPublicKey () );
01547         }
01548     }
01549 
01550     /**
01551      *  On RING call-back is triggered when PBXClient receives ringing message
01552      *  (indicating that the peer is alerting end-user). 
01553      */
01554     public void deferredOnRing( final PBXClient.ControlMessage m )
01555     {
01556         if ( m.peerPort < 1 || m.peerPort > 65535 ) {
01557             return;
01558         }
01559         
01560         String verboseRemote = m.getVerboseRemote ();
01561         
01562         /* Ignore the message if we already have the call in progress.
01563          */
01564         if ( udpChannel.hasRemotePeer () ) {
01565             return;
01566         }
01567         
01568         /* Ignore the message if the remote peer is not that we are inviting to a call
01569          */
01570         if ( currentInvite == null || ! currentInvite.equalsIgnoreCase( m.peerUserId ) ) {
01571             return;
01572         }
01573 
01574         /* Deserialize remote public key.
01575          * Deserialization also verifies the public key against authorized keys.
01576          * \see constructor of the PublicEncryptor
01577          */
01578         remotePublicKey = null;
01579 
01580         if ( m.secret != null ) 
01581         {
01582             remotePublicKey = new PublicEncryptor( m.secret, m.peerUserId );
01583             if ( ! remotePublicKey.isActive () ) {
01584                 remotePublicKey = null;
01585             }
01586         }
01587 
01588         /* Cancel inviteTimeout timer and give information and ringing tone to our user
01589          */
01590         report( "logInfo", "User " + verboseRemote + " is alerted..." );
01591         
01592         if ( remotePublicKey != null && remotePublicKey.isActive () )
01593         {
01594             if ( ! remotePublicKey.isVerified () ) 
01595             {
01596                 report( "logError", "Reply from " + verboseRemote
01597                         + " could not be authenticated." );
01598             }
01599             else 
01600             {
01601                 report( "logOk", "Reply from " + verboseRemote
01602                         + " authenticated with public key '" 
01603                         + remotePublicKey.getVerificatorName () + "'" );
01604             }
01605         }
01606 
01607         inviteTimeout = -1; // Reset only invite timeout (do not reset currentInvite)
01608 
01609         this.audioInterface.startRinging ();
01610         userId.setEnabled( false ); // disable changing user ID
01611     }
01612 
01613     /**
01614      *  On ACCEPT call-back is triggered when PBXClient receives accepting message
01615      *  (indicating that the peer has accepted our invite).
01616      *  
01617      *  It deserializes encrypted secret key used for peer-to-peer communication
01618      *  (if any) and creates instance for the call (CallContext class) as well the remote 
01619      *  peer (RemotePeer class), and binds them to specific CODEC (of the audio interface) 
01620      *  and the UDP channel (for voice transmission). 
01621      *  
01622      *  The call is finally established at this point.
01623      */
01624     public void deferredOnAccept( final PBXClient.ControlMessage m )
01625     {
01626         if ( m.peerPort < 1 || m.peerPort > 65535 ) {
01627             return;
01628         }
01629         
01630         String verboseRemote = m.getVerboseRemote ();
01631         
01632         /* Ignore message if we already have the call in progress.
01633          */
01634         if ( udpChannel.hasRemotePeer () ) {
01635             return;
01636         }
01637         
01638         inviteTimeout = -1;
01639         currentInvite = null;
01640         
01641         /* Resolve peers IP address
01642          */
01643         InetAddress peerAddr = null;
01644         try {
01645             peerAddr = InetAddress.getByName( m.peerAddr );
01646         } catch( UnknownHostException e ) {
01647             Log.exception( Log.ERROR, e );
01648             this.lastMessageFromPBX = null;
01649             report( "logError", "Unknown remote host '" + m.peerAddr 
01650                     + "'; clearing the call..." );
01651             return;
01652         }
01653 
01654         report( "logInfo", "User " + verboseRemote + " has accepted our invite" );
01655         
01656         /* Deserialize encrypted remote secret key and decrypt it with our private key
01657          */
01658         SymmetricCipher cipher = null;
01659         udpChannel.useSymmetricCipher( null );
01660 
01661         if ( m.secret != null ) 
01662         {
01663             cipher = CipherEngine.deserializeEncryptedSecretKey( m.secret );
01664             
01665             if ( cipher.isActive () ) {
01666                 udpChannel.useSymmetricCipher( cipher );
01667             } else {
01668                 cipher = null;
01669             }
01670         }
01671 
01672         /* Create necessary objects needed to establish the call
01673          */
01674         RemotePeer remotePeer = new RemotePeer( this.udpChannel, m.peerUserId, 
01675                 peerAddr, m.peerPort );
01676         
01677         AudioInterface codec = this.audioInterface.getByFormat( VoicePDU.ALAW );
01678         
01679         CallContext call = new CallContext( remotePeer, codec );
01680         call.setCallEstablished( true );
01681         monitorIfPeerIsSendingVoice = true;
01682 
01683         if ( cipher != null && cipher.isActive () )
01684         {
01685             if ( ! cipher.isVerified () ) 
01686             {
01687                 securityState.setState( JSecState.State.UNVERIFIED );
01688                 report( "logError", "Secret key from " + verboseRemote
01689                         + " could not be authenticated." );
01690             }
01691             else 
01692             {
01693                 securityState.setState( JSecState.State.VERIFIED );
01694                 report( "logOk", "Secret key from " + verboseRemote
01695                         + " authenticated with public key '" 
01696                         + cipher.getVerificatorName () + "'" );
01697             }
01698             
01699             report( "logOk", "***** Encrypted call established *****" );
01700             setTitle( appTitle + "; Established ENCRYPTED call with " + verboseRemote );
01701         }
01702         else
01703         {
01704             securityState.setState( JSecState.State.UNSECURED );
01705             report( "logError", "***** Un-encrypted call established *****" );
01706             setTitle( appTitle + "; Established PLAIN call with " + verboseRemote ); 
01707         }
01708     }
01709     
01710     /**
01711      *  On BYE call-back is triggered when PBXClient receives 'bye' message
01712      *  (indicating that the peer is clearing i.e. hanging-up the call). 
01713      */
01714     public void deferredOnBye( final PBXClient.ControlMessage m )
01715     {
01716         String verboseRemote = m.getVerboseRemote ();
01717         
01718         /* If we do not have a call, than remote has rejected our earlier invite
01719          */
01720         if ( ! udpChannel.hasRemotePeer () && currentInvite != null ) 
01721         {
01722             report( "logInfo", "User " + verboseRemote + " rejected our invite" );
01723         }
01724         else /* otheresie, it is a hang-up of the existing call */
01725         {
01726             report( "logInfo", "User " + verboseRemote + " is clearing the call" );
01727             udpChannel.removePeer ();
01728         }
01729 
01730         audioInterface.stopRinging ();
01731         userId.setEnabled( true ); // enable back changing user ID
01732         
01733         lastMessageFromPBX = null;
01734         remotePublicKey = null;
01735         
01736         report( "logInfo", "***** Call Ended *****" );
01737         logMessage( "<hr/>" );
01738         
01739         securityState.setState( JSecState.State.UNSECURED );
01740         setTitle( pbxChannelStatus );
01741         
01742         inviteTimeout = -1;
01743         currentInvite = null;
01744     }
01745     
01746     /**
01747      *  On IMSG call-back is triggered when PBXClient receives private
01748      *  instant message (encrypted with session's secret key).
01749      */
01750     public void deferredOnInstantMessage( final PBXClient.ControlMessage m )
01751     {
01752         /* Get symmetric ciphering engine for PDUs and messages (if any).
01753          */
01754         SymmetricCipher cipher = udpChannel.getUsedSymmetricCipher ();
01755         if ( cipher == null ) {
01756             return;
01757         }
01758 
01759         /* Decrypt instant message and display highlighted contents to the user
01760          */
01761         String clearText = cipher.decrypt( m.secret );
01762         if ( clearText != null ) 
01763         {
01764             /* 
01765              */
01766             reportIncomingTextMessage( "secureMessage", 
01767                     m.peerUserId + " [encrypted]", clearText ); 
01768         }
01769     }
01770     
01771     /**
01772      *  Verifies invitor's public key (signed by invitor's private key) 
01773      *  against the public keys from authorized keys file.
01774      */
01775     private PublicEncryptor tryToVerifyInvitingCall( boolean silent )
01776     {
01777         if ( this.lastMessageFromPBX == null ) { // There is no INVITE to accept
01778             return null;
01779         }
01780         
01781         PBXClient.ControlMessage m = this.lastMessageFromPBX;
01782         String remoteId = m.getVerboseRemote ();
01783 
01784         /* Can accept only if there is no call in progress.
01785          */
01786         if ( udpChannel.hasRemotePeer () ) {
01787             return null;
01788         }
01789         
01790         /* Deserialize remote public key.
01791          * Deserialization also verifies the public key against authorized keys.
01792          * \see constructor of the PublicEncryptor
01793          */
01794         PublicEncryptor pubKey = null;
01795         
01796         if ( m.secret != null ) 
01797         {
01798             pubKey = new PublicEncryptor( m.secret, m.peerUserId );
01799             if ( ! pubKey.isActive () ) {
01800                 pubKey = null;
01801             }
01802         }
01803 
01804         if ( silent ) {
01805             return pubKey;
01806         }
01807         
01808         /* If not silent, be loud...
01809          */
01810         if ( pubKey != null && pubKey.isActive () )
01811         {
01812             if ( ! pubKey.isVerified () ) 
01813             {
01814                 securityState.setState( JSecState.State.UNVERIFIED );
01815                 report( "logError", "Invite from " + remoteId
01816                         + " could not be authenticated." );
01817             }
01818             else 
01819             {
01820                 securityState.setState( JSecState.State.VERIFIED );
01821                 report( "logOk", "Invite from " + remoteId
01822                         + " authenticated with public key '" 
01823                         + pubKey.getVerificatorName () + "'" );
01824             }
01825         }
01826         else
01827         {
01828             securityState.setState( JSecState.State.UNSECURED );
01829             report( "logError", "The call will be without encryption." );
01830         }
01831 
01832         return pubKey;
01833     }
01834 
01835     /**
01836      *  Accepts incoming invite. Procedure is called either automatically from 
01837      *  deferredOnInvite() when the auto-answer is turned on, 
01838      *  or manually by the user from :accept command).
01839      *  
01840      *  It first verifies invitor's public key (signed by invitor's private key) 
01841      *  against the public keys from authorized keys file.
01842      *  
01843      *  It then signs local secret key with the local private key, then 
01844      *  encrypts signed secret key with our with verified invitor's public key 
01845      *  and serializes it as Base64 string.
01846      *  
01847      *  It sends ACCEPTING message to the peer with the
01848      *  information how to rich us (local IP address and UDP port as well serialized
01849      *  encrypted secret key) and signed/encrypted/encoded secret key. 
01850      *  
01851      *  At the end it creates instance for the call (CallContext class) and the remote 
01852      *  peer (RemotePeer class), and binds them to specific CODEC of the audio interface 
01853      *  and UDP channel.
01854      *  
01855      *  The call is finally established at this point.
01856      */
01857     private void acceptIncomingCall( boolean securedIfPossible )
01858     {
01859         if ( this.lastMessageFromPBX == null ) { // There is no INVITE to accept
01860             return;
01861         }
01862         
01863         PBXClient.ControlMessage m = this.lastMessageFromPBX;
01864         String verboseRemote = m.getVerboseRemote ();
01865 
01866         /* Can accept only if there is no call in progress.
01867          */
01868         if ( udpChannel.hasRemotePeer () ) {
01869             return;
01870         }
01871         
01872         /* Resolve peers IP address
01873          */
01874         InetAddress peerAddr = null;
01875         try {
01876             peerAddr = InetAddress.getByName( m.peerAddr );
01877         } catch( UnknownHostException e ) {
01878             Log.exception( Log.ERROR, e );
01879             this.lastMessageFromPBX = null;
01880             report( "logError", "Unknown remote host '" + m.peerAddr 
01881                     + "'; clearing the call..." );
01882             return;
01883         }
01884 
01885         /* Deserialize remote public key and encrypt our secret key with it.
01886          * Deserialization also verifies the public key against authorized keys.
01887          * \see constructor of the PublicEncryptor
01888          */
01889         String mySecret = null;
01890         remotePublicKey = null;
01891         udpChannel.useSymmetricCipher( null );
01892         
01893         if ( securedIfPossible )
01894         {
01895             remotePublicKey = tryToVerifyInvitingCall( /*silent*/ true );
01896             
01897             if ( remotePublicKey != null && remotePublicKey.isActive () ) 
01898             {
01899                 mySecret = remotePublicKey.encryptAndSerialize( CipherEngine.getSignedSecretKey() );
01900                 
01901                 udpChannel.useSymmetricCipher( CipherEngine.getCipher () );
01902             }
01903         }
01904 
01905         /* Send accepting message to remote peer
01906          */
01907         pbxChannel.sendAccept( m.peerUserId,  pbxChannel.getLocalAddress (), 
01908                 udpChannel.getLocalPort (), mySecret );
01909 
01910         /* Create necessary objects needed to establish the call:
01911          * instances of the RemotePeer and CallContext. 
01912          */
01913         RemotePeer remotePeer = new RemotePeer( this.udpChannel, 
01914                 m.peerUserId, peerAddr, m.peerPort );
01915         
01916         AudioInterface codec = this.audioInterface.getByFormat( VoicePDU.ALAW );
01917         
01918         CallContext call = new CallContext( remotePeer, codec );
01919         call.setCallEstablished( true );
01920         monitorIfPeerIsSendingVoice = true;
01921         
01922         /* Report what we've done.
01923          */
01924         if ( remotePublicKey != null && remotePublicKey.isActive () )
01925         {
01926             if ( ! remotePublicKey.isVerified () ) {
01927                 securityState.setState( JSecState.State.UNVERIFIED );
01928             } else  {
01929                 securityState.setState( JSecState.State.VERIFIED );
01930             }
01931             
01932             report( "logOk", "***** Encrypted call established *****" );
01933             setTitle( appTitle + "; Established ENCRYPTED call with " + verboseRemote ); 
01934         }
01935         else
01936         {
01937             securityState.setState( JSecState.State.UNSECURED );
01938             report( "logError", "***** Un-encrypted call established *****" );
01939             setTitle( appTitle + "; Established PLAIN call with " + verboseRemote ); 
01940         }
01941 
01942         this.lastMessageFromPBX = null;
01943     }
01944 
01945     //////////////////////////////////////////////////////////////////////////////////////
01946     
01947     /**
01948      *  Dumps log area into html file
01949      */
01950     public void dumpLogArea( String fileName )
01951     {
01952         if ( fileName == null || fileName.isEmpty () ) {
01953             Calendar cal = Calendar.getInstance ();
01954             SimpleDateFormat sdf = new SimpleDateFormat( "yyyy-MM-dd-HHmmssSSS" );
01955             fileName = defaultLogAreaDumpFilename + sdf.format( cal.getTime () ) + ".html";
01956         }
01957 
01958         try 
01959         {
01960             BufferedWriter out = new BufferedWriter( new FileWriter( fileName ) );
01961             
01962             synchronized( logArea )
01963             {
01964                 out.write( logArea.getText () );
01965             }
01966 
01967             out.close ();
01968             
01969             report( "logInfo", "Dumped log area into '" + fileName + "'" );
01970         }
01971         catch( IOException e )
01972         {
01973             report( "logError", "Failed to dump log area: " + e.getMessage () );
01974         }
01975     }
01976 
01977     /**
01978      *  Reads contents of the text file using URL class.
01979      *  
01980      *  @return string buffer containing file contents; null in case of error
01981      */
01982     public StringBuffer getContentsFromResourceOrFile( String path )
01983     {
01984         StringBuffer sb = new StringBuffer ();
01985 
01986         try 
01987         {
01988             /* First, try to read resource from JAR
01989              */
01990             URL url = getClass().getResource( path );
01991             
01992             /* Then fail to local file 
01993              */
01994             if ( url == null ) {
01995                 url = new URL( "file:" + path );
01996             }
01997             
01998             /* Append contents of the file to string buffer
01999              */
02000             if ( url != null ) 
02001             {
02002                 InputStream in = url.openStream ();
02003                 BufferedReader dis = new BufferedReader( new InputStreamReader( in ) );
02004 
02005                 String line;
02006                 while ( ( line = dis.readLine () ) != null )
02007                 {
02008                     sb.append( line + "\n" );
02009                 }
02010 
02011                 in.close ();
02012             }
02013         }
02014         catch( MalformedURLException e ) 
02015         {
02016             Log.exception( Log.TRACE, e );
02017             sb = null;
02018         }
02019         catch( IOException e )
02020         {
02021             Log.exception( Log.TRACE, e );
02022             sb = null;
02023         }
02024 
02025         return sb;
02026     }
02027 
02028     /**
02029      *  Clears log area (with the contents of the "empty" template)
02030      */
02031     public void clearLogArea ()
02032     {
02033         logArea.setContentType( "text/html" );
02034         StringBuffer sb = getContentsFromResourceOrFile( "resources/empty.html" );
02035         if ( sb != null ) {
02036             logArea.setText( sb.toString () );
02037         } else {
02038             logArea.setText( "<html><head></head><body></body></html>" );
02039         }
02040     }
02041 
02042     /**
02043      *  Displays usage information
02044      */
02045     public void displayUsage ()
02046     {
02047         StringBuffer sb = getContentsFromResourceOrFile( "resources/usage.html" );
02048         
02049         if ( sb != null && sb.length () > 0 ) {
02050             logMessage( sb.toString () );
02051         }
02052     }
02053 
02054     /**
02055      *  Displays help
02056      */
02057     public void displayHelp ()
02058     {
02059         StringBuffer sb = getContentsFromResourceOrFile( "resources/help.html" );
02060         
02061         if ( sb != null && sb.length () > 0 ) {
02062             logMessage( sb.toString () );
02063         }
02064     }
02065 
02066     /**
02067      *  Main entry point. 
02068      *  Creates GUI instance of the CryptoPhoneApp application.
02069      *  
02070      *  @param args the command line arguments
02071      */
02072     public static void main( final String args[] ) 
02073     {
02074         // System.getProperties().list( System.out );
02075         
02076         java.awt.EventQueue.invokeLater( 
02077                 new Runnable() {
02078                     public void run() {
02079                         new CryptoPhoneApp( args ).setVisible( true );
02080                     }
02081                 }
02082             );
02083     }
02084 }
02085 
02086 /*! \mainpage The Kryptofon - Secure VoIP Phone
02087 
02088   \section s_intro Introduction
02089   
02090   Welcome to Kryptofon, a Java based application for secured voice and short message 
02091   communication between internet users.  Kryptofon was written as a part of final 
02092   project (\ref p_task) of the 
02093   <a href="http://dsv.su.se/utbildning/distans/ip1" target="_blank"><b>SU/IP1 
02094   course</b></a>.
02095   
02096   \section s_desc Documentation
02097   
02098    - <a href="../description/Kryptofon_UsersGuide.pdf" target="_top">
02099        Kryptofon User's Guide (PDF)</a>
02100    - <a href="../description/Kryptofon_SysInternals.pdf" target="_top">
02101        Kryptofon System Internals (PDF)</a>
02102    - <a href="../docs/index.html" target="_top">
02103        Javadoc</a> (instead of Doxygen)
02104  
02105   \section s_jar Executable
02106   
02107   The jar file of the package can be found <a href="../kryptofon.jar"><b>here</b></a>.
02108   
02109   \image html kryptofon.png
02110 */
02111 
02112 /*!
02113   \page p_task IP1 - Gesällprov
02114 
02115   Gesällprovet är en uppgift för den egna kreativiteten och är ett fritt valt 
02116   (men obligatoriskt) arbete där du får chansen att visa vad du går för 
02117   konstruktionsmässigt (istället för minnesmässigt som på en tenta) i "lugn och ro". 
02118 
02119    - Ska baseras på de tekniker som kursen bygger på men får givetvis även i
02120      nnehålla tekniker från andra områden
02121    - Ska vara nätverksbaserat
02122    - Ska vara bra både till form och funktion
02123    - Ska inte vara en av de tidigare uppgifterna på kursen men gärna 
02124      en utökning av en eller flera av dessa 
02125 */

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