/*
* (standard BSD license)
*
* Copyright (c) 2002, Chad Myers, et al.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer. Redistributions in binary form
* must reproduce the above copyright notice, this list of conditions and the
* following disclaimer in the documentation and/or other materials provided with
* the distribution. Neither the name of SourceForge, nor Microsoft nor the names
* of its contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
using System;
using System.Collections;
using System.Timers;
namespace MSNP{
/// <summary>
/// Handler for connections to Switchboard servers
/// </summary>
internal class SwitchboardConnectionHandler : ConnectionHandler
{
/**********************************************/
// Enumerations
/**********************************************/
/// <summary>
/// Determines which state the <see cref="ProcessConnection"/> method is in this cycle.
/// </summary>
private enum ConnectionState :int {STARTING, SENTAUTH, ONLINE, CLOSING}
/**********************************************/
// Fields
/**********************************************/
private static int m_SessionTimeout = 30000;
private int m_State = (int) ConnectionState.STARTING; // Current processing state
private String m_FriendlyName; // This user's friendly name
private String m_AuthChallenge; // Challenge string for initial connection
private String m_SessionID; // Session ID of this session
private ISessionHandler m_Handler; // Implementer provided session handler
private Session m_Session; // Current Session object for this session
private int m_RosterCount = 0; // Number of users in this session's user roster
private Timer m_Timer; // Timer for determining whether or not to terminate the session
private Queue m_RequestQueue = Queue.Synchronized(new Queue(10)); // Requests to be sent on next cycle
private object m_SessionIdentifier; // Implementer specified identifier for this session
/**********************************************/
// Events
/**********************************************/
/// <summary>
/// Thrown when this session is ending
/// </summary>
public event EventHandler SessionEndEvent;
/// <summary>
/// Thrown when this is a user-requested session and the session ID has finally been determined.
/// </summary>
public event EventHandler SessionIDObtained;
/**********************************************/
// Constructors
/**********************************************/
/// <summary>
/// Main constructor. Configures this handler.
/// </summary>
/// <remarks>
/// No connections or actual processing is done in the constructor. Use the <see cref="ConnectionHandler.Start"/> method to begin processing.
/// </remarks>
/// <param name="host">The DNS name or IP address of the SB to connect to.</param>
/// <param name="port">The port of the SB server</param>
/// <param name="user">The username with which to authenticate</param>
/// <param name="pass">The user's password</param>
/// <param name="friendlyName">This user's friendly name</param>
/// <param name="authChallengeInfo">The challenge info for connecting to an existing session</param>
/// <param name="sessionID">The ID of this session if joining an existing session</param>
/// <param name="sessionHandler">The implemented session handler for any new sessions</param>
/// <param name="sessionIdentifier">The implementer specified identifier for this session</param>
public SwitchboardConnectionHandler(String host, int port, String user,
String pass, String friendlyName,
String authChallengeInfo,
String sessionID,
ISessionHandler sessionHandler,
object sessionIdentifier) :
base(host, port, user, pass)
{
m_FriendlyName = friendlyName;
m_AuthChallenge = authChallengeInfo;
m_SessionID = sessionID;
m_Handler = sessionHandler;
m_Timer = new Timer(SwitchboardConnectionHandler.SessionTimeout);
m_Timer.Elapsed += new ElapsedEventHandler(OnTimerElapsed);
m_Timer.Enabled = true;
m_SessionIdentifier = sessionIdentifier;
}
/**********************************************/
// Properties
/**********************************************/
public String SessionID{ get{ return m_SessionID; } }
internal static int SessionTimeout
{
get{ return m_SessionTimeout; }
set{ m_SessionTimeout = value; }
}
/**********************************************/
// Protected Methods
/**********************************************/
/// <summary>
/// Implementation of the abstract <see cref="ConnectionHandler.ProcessConnection"/> method.
/// </summary>
/// <remarks>
/// <para>This is the main processing method for this class. This method is called
/// every cycle in the connection thread.</para>
/// <para>This thread will process the initial protocol initialization dialogue and then
/// enter the main switchboard phase and process any new messages or notifications.</para>
/// </remarks>
protected override void ProcessConnection()
{
Response[] responses = GetResponses();
switch(m_State)
{
// Just starting, send auth or challenge info
case (int) ConnectionState.STARTING:
m_Session = new Session(this, m_Handler, m_SessionIdentifier);
if( m_SessionID != null )
AddRequest( new Request(ANS, User + " " + m_AuthChallenge + " " + m_SessionID) );
else
AddRequest( new Request(USR, User + " " + m_AuthChallenge) );
m_State = (int) ConnectionState.SENTAUTH;
break;
// Check for auth response
case (int) ConnectionState.SENTAUTH:
Response response;
ProcessResponses(responses);
if( m_SessionID != null )
response = LookForSpecificResponse(responses, ANS);
else
response = LookForSpecificResponse(responses, USR);
if( response == null )
break;
// Different cases depending on whether we're joining a session or
// starting a new one
if( m_SessionID != null &&
(response.Parameters.Length != 1 || response.Parameters[0] != "OK" ) )
throw new Exception("Unexpected answer response from switchboard server");
else if( m_SessionID == null &
(response.Parameters.Length != 3 || response.Parameters[0] != "OK" ) )
throw new Exception("Unexpected answer response from switchboard server");
response = null;
m_Session.Start();
m_State = (int) ConnectionState.ONLINE;
break;
// Online, authenticated, go into main loop
case (int) ConnectionState.ONLINE:
ProcessResponses(responses);
ProcessSessionRequests();
break;
// Signout requested, shutdown gracefully
case (int) ConnectionState.CLOSING:
CloseConnection();
break;
}
}
/// <summary>
/// Called when the <see cref="ConnectionHandler"/> has actually
/// closed the connection.
/// </summary>
protected override void OnConnectionClosed()
{
m_State = (int) ConnectionState.CLOSING;
m_Session.Stop();
OnSessionEnd();
}
/**********************************************/
// Private Methods
/**********************************************/
/// <summary>
/// Process responses from server.
/// </summary>
/// <remarks>
/// This method handles situations such as a user joining the session,
/// leaving the session, or responses to commands such as CAL
/// </remarks>
/// <param name="responses">The list of responses to process</param>
private void ProcessResponses(Response[] responses)
{
for(int i = 0; i < responses.Length; i++ )
{
// Ignore ANS and USR since they're part of the authentication, but process everything else
if( responses[i].Command != ANS && responses[i].Command != USR )
{
// Roster update -- users already in this session
if( responses[i].Command == IRO )
{
System.Threading.Interlocked.Increment(ref m_RosterCount);
m_Session.AddRosterUser( responses[i].Parameters[2], responses[i].Parameters[3] );
}
// User joined
else if( responses[i].Command == JOI )
{
System.Threading.Interlocked.Increment(ref m_RosterCount);
m_Session.AddResponseToQueue(responses[i]);
}
// User departed
else if( responses[i].Command == BYE )
{
System.Threading.Interlocked.Decrement(ref m_RosterCount);
m_Session.AddResponseToQueue(responses[i]);
}
// Confirmation response to CAL command
// If this is a new session, the session ID will be passed back
// so we can remember it.
else if( responses[i].Command == CAL && m_SessionID == null )
{
m_SessionID = responses[i].Parameters[1];
OnSessionIDObtained();
}
// Otherwise, add it for the Session object to deal with
else
{
m_Session.AddResponseToQueue(responses[i]);
}
}
}
}
/// <summary>
/// Adds a request for this SB session to process
/// </summary>
/// <remarks>
/// This method is used by the convience methods to allow
/// other classes to request things like inviting a user
/// or sending a message.
/// </remarks>
/// <param name="request">The request to add</param>
private void AddSessionRequest(Request request)
{
m_RequestQueue.Enqueue(request);
}
/// <summary>
/// Processes any requests pending in the session queue
/// </summary>
private void ProcessSessionRequests()
{
ArrayList requests;
lock(m_RequestQueue.SyncRoot)
{
requests = new ArrayList(m_RequestQueue.Count);
for(int i = 0; i < m_RequestQueue.Count; i++)
{
Request request = (Request) m_RequestQueue.Dequeue();
if( ! (request.Command == ConnectionHandler.MSG && m_RosterCount == 0) )
requests.Add(request);
}
}
foreach(Request request in requests)
{
AddRequest(request);
}
}
/// <summary>
/// Used to determine is the session should terminate
/// </summary>
/// <remarks>
/// If no one has joined the session after 30 seconds, close it so
/// it doesn't linger around wasting resources.
/// </remarks>
/// <param name="sender">Type who rasied the event</param>
/// <param name="args">Arguments and data for this event delegate</param>
private void OnTimerElapsed(object sender, ElapsedEventArgs args)
{
if( m_RosterCount == 0 )
Stop();
}
/// <summary>
/// Throws SessionEndEvent.
/// </summary>
private void OnSessionEnd()
{
if( SessionEndEvent != null )
SessionEndEvent(this, new EventArgs());
}
/// <summary>
/// Throws a SessionIDObtained event.
/// </summary>
private void OnSessionIDObtained()
{
if( SessionIDObtained != null )
SessionIDObtained(this, new EventArgs());
}
/**********************************************/
// Internal methods
/**********************************************/
/// <summary>
/// Invites a user to this session.
/// </summary>
/// <param name="userHandle">The user handle (email address) of the user to invite</param>
internal void InviteContact(String userHandle)
{
AddSessionRequest(new Request(CAL, userHandle));
}
/// <summary>
/// Sends a message to the session.
/// </summary>
/// <param name="message">The message body to send</param>
internal void SendMessage(String message)
{
string headers = "MIME-Version: 1.0\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\n";
int length = headers.Length + message.Length + 2;
string commandParams = "U " + length.ToString() + "\r\n" + headers + message;
AddSessionRequest(new Request(MSG, commandParams));
}
/// <summary>
/// Sends a MIME message to the session
/// </summary>
/// <param name="message">The message to send</param>
internal void SendMessage(MimeMessage message)
{
string headers = "";
IDictionary headersDict = message.Headers;
foreach( DictionaryEntry entry in headersDict )
{
headers += entry.Key + ": " + entry.Value + "\r\n";
}
headers += "\r\n";
int length = headers.Length + message.Body.Length + 2;
string commandParams = "U " + length.ToString() + "\r\n" + headers + message;
AddSessionRequest(new Request(MSG, commandParams));
}
/// <summary>
/// Stop this connection and close out gracefully.
/// </summary>
internal void Stop()
{
lock(this)
{
AddRequest( new Request(BYE, null) );
m_State = (int) ConnectionState.CLOSING;
m_Session.Stop();
}
OnSessionEnd();
}
/// <summary>
/// Converts an error response from the server to a human-readable description
/// </summary>
/// <param name="errorCommand">The command for which to get the description</param>
/// <returns>A description of the error</returns>
internal string GetErrorDescriptionForCommand(string errorCommand)
{
return GetErrorDescription(System.Int32.Parse(errorCommand));
}
}
}
|