/*
* (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.Diagnostics;
using System.Security.Cryptography;
using System.Text;
namespace MSNP{
/// <summary>
/// Handles a Notification Server connection.
/// </summary>
/// <remarks>
/// <para>The Notification Server connection is the main conversation point for MSNP.
/// Requests for new sessions or contact list updates are made through here, and any
/// notifications received from other users or the MSN system itself are sent through here.</para>
/// </remarks>
internal class NotificationConnectionHandler : ConnectionHandler
{
/**********************************************/
// Enumerations
/**********************************************/
/// <summary>
/// Determines which state the <see cref="ProcessConnection"/> method is in this cycle.
/// </summary>
private enum ConnectionState :int {STARTING, SETPROTOCOL, AUTHMD5, AUTHENTICATED, GETLIST, SENTONLINE, ONLINE, CLOSING}
/// <summary>
/// Determines which phase of the MD5 authentication the connection is in.
/// </summary>
private enum MD5State :int {STARTING, POLICYREQUEST, AUTHREQUEST, AUTHRESPONSE}
/**********************************************/
// Fields
/**********************************************/
private int m_State = (int) ConnectionState.STARTING; // Current connection state
private int m_MD5State = (int) MD5State.STARTING; // Current MD5 auth state
private int m_TotalFLContacts = 0; // Total number of forward-list contacts
private int m_ReceivedFLContacts = 0; // Total number of received FL contacts
private String m_FriendlyName; // Local user's friendly name
private Hashtable m_FLContacts = Hashtable.Synchronized(new Hashtable(48)); // Contacts on the forward-list
private Hashtable m_Sessions = Hashtable.Synchronized(new Hashtable(16)); // Currently open sessions
private ISessionHandler m_SessionHandler; // API implementer session handler for new sessions
private Queue m_PendingSessions = Queue.Synchronized(new Queue(10)); // Pending requests for new sessions
/**********************************************/
// Delegates and Events
/**********************************************/
/// <summary>
/// Used for all Notification-related events
/// </summary>
internal delegate void NotificationEventHandler(object sender, MSNPEventArgs ne);
/// <summary>
/// Thrown when a contact has change its state (online to offline, or vice versa)
/// </summary>
internal event NotificationEventHandler ContactStateChangedEvent;
/// <summary>
/// Thrown when the NS has connected and authenticated.
/// </summary>
internal event EventHandler OnlineEvent;
/// <summary>
/// Thrown when this connection has been stopped or was terminated from the other side.
/// </summary>
internal event EventHandler SignedOutEvent;
/**********************************************/
// Constructors
/**********************************************/
/// <summary>
/// Configures this connection
/// </summary>
/// <param name="host">The DNS name or IP address of the NS to connect to.</param>
/// <param name="port">The port of the NS server</param>
/// <param name="user">The username with which to authenticate</param>
/// <param name="pass">The user's password</param>
/// <param name="handler">The implemented session handler for any new sessions</param>
internal NotificationConnectionHandler(String host, int port, String user, String pass, ISessionHandler handler) :
base(host, port, user, pass)
{
m_SessionHandler = handler;
}
/**********************************************/
// Properties
/**********************************************/
/// <summary>
/// Current forward contact list.
/// </summary>
/// <remarks>
/// This is a clone of the actual list, so any changes are not realized.
/// </remarks>
public ICollection FLContacts{ get{ return ((IDictionary)m_FLContacts.Clone()).Values; } }
/**********************************************/
// 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 notification phase and process any new notifications.</para>
/// </remarks>
protected override void ProcessConnection()
{
Response[] responses = GetResponses();
// Determine if a specific XFR was sent from the NS and process it immediately.
Response XFRResponse = LookForSpecificResponse(responses, XFR);
if( XFRResponse != null && XFRResponse.Parameters[0] == NS)
HandleXFR(XFRResponse);
switch(m_State)
{
// Initial connection, send the VER command
case (int) ConnectionState.STARTING:
SetProtocol(DEFAULT_PROTOCOL);
m_State = (int) ConnectionState.SETPROTOCOL;
break;
// Ensure proper VER reply was sent, then request policy information.
case (int) ConnectionState.SETPROTOCOL:
Response response = LookForProtocolResponse(responses);
if( response == null )
break;
if( response.Parameters.Length != 1 || response.Parameters[0] != DEFAULT_PROTOCOL )
throw new Exception("Unexpected version response");
response = null;
AddRequest( new Request(INF, null) );
m_State = (int) ConnectionState.AUTHMD5;
m_MD5State = (int) MD5State.POLICYREQUEST;
break;
/*
* Check INF response and ensure it's MD5 as that's all we support right now
* Handle the MD5 auth dialog in a seperate method with a seperate state.
* It's possible that on the final auth success that other messages could be
* tacked on to the end, so after ProcessAuthMD5 is done, process any other
* messages.
*/
case (int) ConnectionState.AUTHMD5:
ProcessAuthMD5(responses);
if( m_State == (int) ConnectionState.AUTHENTICATED )
ProcessResponses(responses);
break;
// Auth successful. Request forwarding list.
case (int) ConnectionState.AUTHENTICATED:
// Send LST command
AddRequest( new Request(LST, FL) );
m_State = (int) ConnectionState.GETLIST;
break;
// Check for LST replies and populate our contact list, then set ourselves ONLINE (NLN)
case (int) ConnectionState.GETLIST:
for( int i = 0; i < responses.Length; i++ )
{
if( responses[i].Command == "LST" && responses[i].Parameters.Length >= 3)
{
if( responses[i].Parameters[3] != "0" )
{
if( m_TotalFLContacts == 0 )
m_TotalFLContacts = Int32.Parse(responses[i].Parameters[3]);
m_ReceivedFLContacts++;
Contact newContact = new Contact(responses[i].Parameters[4], responses[i].Parameters[5]);
m_FLContacts.Add(newContact.UserName, newContact);
}
}
}
responses = null;
if( m_ReceivedFLContacts == m_TotalFLContacts )
{
AddRequest( new Request(CHG, NLN) );
m_State = (int) ConnectionState.SENTONLINE;
}
break;
// Check response to CHG command and process any messages that are stuffed
// in between the reply such as contact state and such.
case (int) ConnectionState.SENTONLINE:
ProcessResponses(responses);
response = LookForSpecificResponse(responses, CHG);
if( response == null )
{
break;
}
else
{
m_State = (int) ConnectionState.ONLINE;
OnOnlineEvent();
}
responses = null;
break;
// Online. Process each response
case (int) ConnectionState.ONLINE:
ProcessResponses(responses);
break;
// Signout requested, begin shutting down
case (int) ConnectionState.CLOSING:
CloseConnection();
// Get opened session
IDictionary sessions;
lock( m_Sessions.SyncRoot )
{
// Clone the sessions so we don't cause a deadlock
// as each session closes.
sessions = (IDictionary) m_Sessions.Clone();
}
// Close each session
foreach ( object session in sessions.Values )
{
((SwitchboardConnectionHandler)session).Stop();
}
// Signal the signout event
OnSignedOutEvent();
break;
}
}
/**********************************************/
// Internal Methods
/**********************************************/
/// <summary>
/// Signal this connection to stop and disconnect gracefully.
/// </summary>
internal void Stop()
{
lock(this)
{
AddRequest( new Request(CHG, FLN) );
AddRequest( new Request(OUT, null) );
m_State = (int) ConnectionState.CLOSING;
}
}
/// <summary>
/// Request a new switchboard connection for chat
/// </summary>
/// <param name="user">Initial user to invite</param>
/// <param name="sessionIdentifier">Implementer-specified identifier for the created session</param>
internal void StartSBSession(String user, object sessionIdentifier)
{
m_PendingSessions.Enqueue(new SessionStartInfo(user, sessionIdentifier));
AddRequest( new Request(XFR, "SB") );
}
/// <summary>
/// Changes how this client appears to other clients
/// </summary>
/// <param name="state">The state to set (currently NLN or FLN)</param>
/// <param name="substate">The substate (only valid for NLN)</param>
internal void ChangeState(String state, String substate)
{
if( state == NLN )
{
if( substate != null && substate.Length > 0 )
AddRequest( new Request(CHG, substate) );
else
AddRequest( new Request(CHG, state) );
}
else if( state == FLN || state == HDN )
AddRequest( new Request(CHG, state) );
else
throw new Exception("Unexpected state for state change: " + state);
}
/**********************************************/
// Private Methods
/**********************************************/
/// <summary>
/// Handle an XFR command from the server.
/// </summary>
/// <param name="response">The actual XFR response from the server.</param>
private void HandleXFR(Response response)
{
String[] hostAndPort = response.Parameters[1].Split(new char[]{':'});
Host = hostAndPort[0];
Port = Int32.Parse(hostAndPort[1]);
m_State = (int) ConnectionState.STARTING;
m_MD5State = (int) MD5State.STARTING;
Reconnect();
}
/// <summary>
/// Handle the MD5 authentication dialog
/// </summary>
/// <param name="responses">Current received responses</param>
private void ProcessAuthMD5(Response[] responses)
{
switch(m_MD5State)
{
// Policy sent, check to make sure the reply has MD5 as its supported policy
// Next, send USR command to begin the challenge/response phase.
case (int) MD5State.POLICYREQUEST:
Response response = LookForSpecificResponse(responses, INF);
if( response == null )
break;
if( response.Parameters.Length == 0 || response.Parameters[0] != "MD5" )
throw new Exception("Unexpected policy response from server");
response = null;
AddRequest( new Request(USR, "MD5 I " + User) );
m_MD5State = (int) MD5State.AUTHREQUEST;
break;
// Look for USR response and calculate the response to the auth challenge
case (int) MD5State.AUTHREQUEST:
response = LookForSpecificResponse(responses, USR);
if( response == null )
break;
if( response.Parameters.Length != 3 )
throw new Exception("Unexpected authentication response from server");
// the server-issued challenge string
string challenge = response.Parameters[2];
response = null;
MD5 md5 = new MD5CryptoServiceProvider();
// Compute a hash of the challenge + the user password
byte[] hash = md5.ComputeHash(Encoding.ASCII.GetBytes(challenge + Pass));
string hashHexString = "";
// Convert it to hex-string (i.e. "abcdef")
for(int i = 0; i < hash.Length; i++)
{
hashHexString += hash[i].ToString("x2");
}
// We send USR tid MD5 S responseinfo
AddRequest( new Request(USR, "MD5 S " + hashHexString) );
m_MD5State = (int) MD5State.AUTHRESPONSE;
break;
// Look for successful response to auth submittal
case (int) MD5State.AUTHRESPONSE:
response = LookForSpecificResponse(responses, USR);
if( response == null )
break;
if( response.Parameters.Length != 3 )
throw new Exception("Unexpected authentication response from server");
m_FriendlyName = response.Parameters[2];
response = null;
m_State = (int) ConnectionState.AUTHENTICATED;
break;
}
}
/// <summary>
/// Process current responses
/// </summary>
/// <remarks>
/// <para>This is the main guts of the NotificationConnectionHandler. Here is where
/// all the notifications from the server actually get processed. All notifications
/// pass through here and are handled such as contact status change (online/offline),
/// new session requests, property changes, etc.</para>
/// </remarks>
/// <param name="responses">Current received responses</param>
private void ProcessResponses(Response[] responses)
{
for(int i = 0; i < responses.Length; i++ )
{
//Debug.WriteLine("Processing response: " + responses[i]);
if( responses[i].Command == RNG )
{
String sessionID = responses[i].Parameters[0];
if( ! m_Sessions.ContainsKey(sessionID) )
{
String[] hostAndPort = responses[i].Parameters[1].Split(new char[]{':'});
String challenge = responses[i].Parameters[3];
String callerFriendly = responses[i].Parameters[4];
String caller = responses[i].Parameters[5];
SwitchboardConnectionHandler sch = new SwitchboardConnectionHandler(
hostAndPort[0], Int32.Parse(hostAndPort[1]), User, Pass, m_FriendlyName,
challenge, sessionID, m_SessionHandler, null);
sch.SessionEndEvent += new EventHandler(GotSessionEnd);
sch.Start();
m_Sessions.Add(sessionID, sch);
}
}
else if(
responses[i].Command == ILN ||
responses[i].Command == NLN ||
responses[i].Command == FLN )
{
setFLContactState(responses[i]);
}
else if( responses[i].Command == XFR && responses[i].Parameters[0] == SB)
{
SessionStartInfo sessionInfo = (SessionStartInfo) m_PendingSessions.Dequeue();
String[] hostAndPort = responses[i].Parameters[1].Split(new char[]{':'});
String challenge = responses[i].Parameters[3];
SwitchboardConnectionHandler sch = new SwitchboardConnectionHandler(
hostAndPort[0], Int32.Parse(hostAndPort[1]), User, Pass, null, challenge,
null, m_SessionHandler, sessionInfo.SessionIdentifier);
sch.SessionIDObtained += new EventHandler(GotSessionIDObtained);
sch.SessionEndEvent += new EventHandler(GotSessionEnd);
sch.Start();
sch.InviteContact(sessionInfo.UserHandle);
}
}
}
/// <summary>
/// Update or add a contact to the contact list with the specified state.
/// </summary>
/// <param name="response">The response containg the contact status change</param>
private void setFLContactState(Response response)
{
String state = response.Command;
String substate;
String handle;
String friendlyName;
if( state == FLN )
{
// If offline, there is no substate and
// we don't know the friendly name
substate = "";
handle = response.Parameters[0];
friendlyName = null;
}
else
{
substate = response.Parameters[0];
handle = response.Parameters[1];
friendlyName = response.Parameters[2];
}
// See if we already have the contact, otherwise
// create a new one
Contact contact = (Contact)m_FLContacts[handle];
if( contact == null )
{
contact = new Contact(handle, friendlyName);
m_FLContacts.Add(handle, contact);
}
// Set the state and substate
contact.setState(state);
contact.setSubstate(substate);
// Update the friendly name if necessary
if( friendlyName != null )
contact.setFriendlyName(friendlyName);
// Signal the event that the contact's status has changed
OnContactStateChange(contact);
}
/// <summary>
/// Triggers an <see cref="OnlineEvent"/>
/// </summary>
private void OnOnlineEvent()
{
if( OnlineEvent != null )
OnlineEvent(this, new EventArgs());
}
/// <summary>
/// Triggers a <see cref="SignedOutEvent"/>
/// </summary>
private void OnSignedOutEvent()
{
if( SignedOutEvent != null )
SignedOutEvent(this, new EventArgs());
}
/// <summary>
/// Triggers a <see cref="ContactStateChangedEvent"/>
/// </summary>
/// <param name="user">The contact whose status has changed</param>
private void OnContactStateChange(Contact user)
{
if( ContactStateChangedEvent != null )
ContactStateChangedEvent(this, new MSNPEventArgs(user));
}
/// <summary>
/// Called when a session has ended.
/// </summary>
/// <param name="sender">The <see cref="SwitchboardConnectionHandler"/> who threw the event.</param>
/// <param name="args">Not used by this event</param>
private void GotSessionEnd(object sender, EventArgs args)
{
SwitchboardConnectionHandler sch = (SwitchboardConnectionHandler) sender;
if( sch.SessionID != null )
m_Sessions.Remove(sch.SessionID);
}
/// <summary>
/// Called when an outgoing session has its ID set
/// </summary>
/// <remarks>
/// When a new session is created for the purposes of calling out,
/// the session ID is not immediately returned from MSN. Instead,
/// the response to the first CAL command gives you the current
/// session's ID. This event is called when that happens.
/// </remarks>
/// <param name="sender">The <see cref="SwitchboardConnectionHandler"/> which threw the event.</param>
/// <param name="args">Not used for this event.</param>
private void GotSessionIDObtained(object sender, EventArgs args)
{
SwitchboardConnectionHandler sch = (SwitchboardConnectionHandler) sender;
if( m_Sessions[sch.SessionID] == null )
{
m_Sessions.Add(sch.SessionID, sch);
}
}
/// <summary>
/// Class used to hold information for stating a new session.
/// </summary>
private class SessionStartInfo
{
private String m_UserHandle;
private object m_SessionIdentifier;
public SessionStartInfo(String userHandle, object sessionIdentifier)
{
m_UserHandle = userHandle;
m_SessionIdentifier = sessionIdentifier;
}
public String UserHandle{ get{ return m_UserHandle; } }
public object SessionIdentifier{ get{ return m_SessionIdentifier; } }
}
}
}
|