/*
* (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.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using MSNP.CommandParsers;
namespace MSNP{
/// <summary>
/// Provides a base for classes that deal with the various connections of MSNP.
/// </summary>
/// <remarks>
/// This class provides functionality such as socket handling, reading/writing, and basic
/// protocol menutia. The implementer needs only implement the <see cref="ProcessConnection"/> method.
/// Responses and Requests can be retrieved and sent using <see cref="GetResponses"/> and <see cref="AddRequest"/>
/// respectively.
/// </remarks>
internal abstract class ConnectionHandler
{
/**********************************************/
// Fields
/**********************************************/
IPAddress m_Host; // The host with whom to establish the connection
int m_Port; // The TCP port on which to connect
String m_User; // The username with which to authenticate
String m_Pass; // The password of the user
Thread m_Thread; // Local reference to the thread for this connection
String m_ThreadName; // The name of this thread. Configurable for debug/trace purposes.
Queue m_RequestsQueue; // The queue for outgoing requests
Queue m_ResponsesQueue; // The queue for incoming responses/commands
Hashtable m_PendingRequests; // Requests that have been issued, but not yet responded to
Socket m_Socket; // The TCP Socket type which is used for basic protocol I/O
int m_TransactionID; // Counter for transaction IDs
CommandParser m_Parser = new CommandParser(); // Used for parsing responses from the server
bool m_CloseConnection; // Flag that implementers use to have the connection closed
IAsyncResult m_SocketReadResult; // Used for asynchronous reads from the socket
/**********************************************/
// Constructors
/**********************************************/
/// <summary>
/// Creates a new connection handler and initializes internal data.
/// </summary>
/// <remarks>
/// Nothing actually happens yet. To begin processing the connection, call
/// the start method.
/// </remarks>
/// <param name="host">A String that represents the hosts' DNS name or IPv4 (32-bit) IP address in the format of x.y.z.a</param>
/// <param name="port">An integer representing the port (0-65534) on which to connect</param>
/// <param name="user">The user handle with which to authenticate to the MSN servers</param>
/// <param name="pass">The password for the user</param>
public ConnectionHandler(String host, int port, String user, String pass)
{
m_Host = IPAddress.Parse(host);
m_Port = port;
m_User = user;
m_Pass = pass;
m_RequestsQueue = Queue.Synchronized(new Queue());
m_ResponsesQueue = Queue.Synchronized(new Queue());
m_PendingRequests = Hashtable.Synchronized(new Hashtable());
m_ThreadName = this.GetType().FullName;
m_CloseConnection = false;
Debug.AutoFlush=true;
}
/**********************************************/
// Properties
/**********************************************/
protected String Host
{
get{ return m_Host.ToString(); }
set{ m_Host = IPAddress.Parse(value); }
}
protected int Port
{
get{ return m_Port; }
set{ m_Port = value; }
}
protected String User
{
get{ return m_User; }
set{ m_User = value; }
}
protected String Pass
{
get{ return m_Pass; }
set{ m_Pass = value; }
}
protected Thread Thread{ get{ return m_Thread; } }
protected String ThreadName
{
get{ return m_ThreadName; }
set{ m_ThreadName = value; }
}
/**********************************************/
// Internal Methods
/**********************************************/
/// <summary>
/// Starts this connection.
/// </summary>
/// <remarks>
/// The current implementation of ConnectionHandler is single-use.
/// You cannot stop/disconnect/signout and then restart a connection
/// handler. You must create a new handler and start it.
/// </remarks>
internal virtual void Start()
{
if( m_Thread == null || ! m_Thread.IsAlive )
{
m_CloseConnection = false;
ThreadStart start = new ThreadStart(handleConnection);
m_Thread = new Thread(start);
m_Thread.Name = m_ThreadName;
m_Thread.Start();
}
else
{
throw new Exception("Connection already started");
}
}
/**********************************************/
// Protected Methods
/**********************************************/
/// <summary>
/// Implementer entry point
/// </summary>
/// <remarks>
/// <para>There is where the derriving class implements all its functionality.
/// This method is called each thread cycle.</para>
/// <para>Received responses can get checked using the <see cref="GetResponses"/> method.
/// Any requests that need to be sent out can be queued using the <see cref="AddRequest"/> method.
/// </para>
/// <para>It is important that ProcessConnection finish in a timely manner and not block
/// for long periods of time. Any lengthy processes should be spawned into sub-threads.</para>
/// <para>When the connection needs to be closed, call the <see cref="CloseConnection"/>
/// method to signal to the ConnectionHandler its time to close the connection and bail.</para>
/// </remarks>
protected abstract void ProcessConnection();
/// <summary>
/// Retrieves any responses that have been received since the last cycle.
/// </summary>
/// <remarks>
/// This method is thread-safe.
/// </remarks>
/// <returns>An array of responses to be processed.</returns>
protected Response[] GetResponses()
{
Response[] responses;
lock(m_ResponsesQueue.SyncRoot)
{
responses = new Response[m_ResponsesQueue.Count];
for( int i = 0; i < responses.Length; i++ )
{
responses[i] = (Response)m_ResponsesQueue.Dequeue();
}
}
return responses;
}
/// <summary>
/// Adds a request to the outgoing request queue
/// </summary>
/// <remarks>
/// <para>This method is thread-safe and uses a syncrhonized Queue collection.</para>
/// <para>Requests will actually be sent after the <see cref="ProcessConnection"/>
/// method finishes each cycle.</para>
/// </remarks>
/// <param name="request">The request to send.</param>
protected void AddRequest(Request request)
{
m_RequestsQueue.Enqueue(request);
}
/// <summary>
/// Called when the connection has been closed.
/// </summary>
/// <remarks>
/// This method is called after the socket has closed and
/// allows the implementer a chance to gracefully shut down
/// in the case of a forced disconnect or user-driven signout.
/// </remarks>
protected virtual void OnConnectionClosed(){}
/// <summary>
/// Called when a connection to a new host is necessary.
/// </summary>
/// <remarks>
/// <para>This is used mainly by Notification Server connections when an
/// XFR command is issued.</para>
/// <para>First set the new Host and Port properties and then call this method.</para>
/// </remarks>
protected void Reconnect()
{
Close();
Connect();
}
/// <summary>
/// Called by the implementer to signal that a disconnect is requested.
/// </summary>
/// <remarks>
/// The disconnect will not actually occur until the <see cref="ProcessConnection"/> method
/// has finished. Generally, this method should be called and then the ProcessConnection
/// method ended.
/// </remarks>
protected void CloseConnection()
{
m_CloseConnection = true;
}
/// <summary>
/// Convenience method used for sending the VER command
/// </summary>
/// <remarks>
/// <para>Since most MSN-related connections use this command, this saves implementers from having to rewrite this section of code.</para>
/// <para>Call this method and then use <see cref="LookForProtocolResponse"/> on the next cycle to determine if the proper response was received.</para>
/// </remarks>
/// <param name="Protocol">The supported protocol (usually MSNP2)</param>
protected void SetProtocol(string Protocol)
{
Request request = new Request(VER, Protocol);
AddRequest(request);
}
/// <summary>
/// Determines if a response to a VER command has been received.
/// </summary>
/// <param name="responses">Currently received responses. Use the <see cref="GetResponses"/> method to get these.</param>
/// <returns>The response, if found, or null if not found.</returns>
protected Response LookForProtocolResponse(Response[] responses)
{
return LookForSpecificResponse(responses, VER);
}
/// <summary>
/// Determines if a specific response has been recieved.
/// </summary>
/// <param name="responses">The responses in which to look. Use the <see cref="GetResponses"/> method to get these.</param>
/// <param name="command">The command part of the response to look for.</param>
/// <returns>The response, if found, or null if not found.</returns>
protected Response LookForSpecificResponse(Response[] responses, String command)
{
Response foundResponse = null;
for( int i = 0; i < responses.Length; i++ )
{
if( responses[i].Command == command )
{
foundResponse = responses[i];
break;
}
}
return foundResponse;
}
/**********************************************/
// Private Methods
/**********************************************/
/// <summary>
/// Main thread class.
/// </summary>
/// <remarks>
/// <para>This is the method passed to the ThreadStart for the thread.
/// It connects, runs the main thread loop and disconnects.</para>
/// <para>The main thread loop gathers any waiting responses/commands from the
/// server and then calls ProcessConnection() which allows the implementer to
/// perform various tasks and process the responses and issue any new requests. After
/// ProcessConnection finishes, any waiting requests are dispatched at this time.</para>
/// </remarks>
private void handleConnection()
{
Connect();
while(!m_CloseConnection)
{
CheckResponses();
ProcessConnection();
if( !m_CloseConnection )
SendRequests();
Thread.Sleep(200);
}
Close();
}
/// <summary>
/// Establishes the actual socket connection to the specified host and port.
/// </summary>
private void Connect()
{
if( m_Socket == null )
{
m_Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPEndPoint endpoint = new IPEndPoint(m_Host, m_Port);
m_Socket.Connect(endpoint);
}
else
throw new Exception("Already Connected");
}
/// <summary>
/// Closes and terminates the socket connection.
/// </summary>
private void Close()
{
if( m_Socket != null )
{
m_Socket.Shutdown(SocketShutdown.Both);
m_Socket.Close();
m_Socket = null;
OnConnectionClosed();
}
else
throw new Exception("Not connected");
}
/// <summary>
/// Dequeues queued requests and sends them using <see cref="SendRequest"/>.
/// </summary>
private void SendRequests()
{
Request[] requests;
lock(m_RequestsQueue.SyncRoot)
{
requests = new Request[m_RequestsQueue.Count];
for( int i = 0; i < requests.Length; i++ )
{
requests[i] = (Request)m_RequestsQueue.Dequeue();
}
}
for( int i = 0; i < requests.Length; i++ )
{
SendRequest(requests[i]);
}
}
/// <summary>
/// Sends a request over the opened socket.
/// </summary>
/// <param name="request">The request to send.</param>
private void SendRequest(Request request)
{
//Debug.WriteLine("Sending request: " + request);
int transID = GetTransactionID();
String messageToSend;
if( request.Parameters != null )
messageToSend = request.Command + " " + transID.ToString() + " " + request.Parameters + "\r\n";
else
messageToSend = request.Command + " " + transID.ToString() + "\r\n";
byte[] bytesToSend = Encoding.ASCII.GetBytes(messageToSend);
Debug.WriteLine("Sending request (raw): " + messageToSend);
m_Socket.Send(bytesToSend);
}
/// <summary>
/// Checks the socket for any awaiting messages
/// </summary>
/// <remarks>
/// <para>Initiates an asynchronous read from the socket which calls <see cref="FinishedRead"/> when finished.</para>
/// </remarks>
private void CheckResponses()
{
/* This doesn't seem to be working as documented.
* there doesn't seem to be a good way to determine if the
* socket is closed or not. It just kind of "is" at any
* given point in time.
if( m_Socket.Poll(200, SelectMode.SelectRead) )
{
// The socket was closed or terminated
//CloseConnection();
//return;
}*/
try
{
// Only start a read if there isn't one pending already.
if( m_SocketReadResult == null ||
m_SocketReadResult.IsCompleted )
{
byte[] responseBytes = new byte[8024];
m_SocketReadResult = m_Socket.BeginReceive(
responseBytes, 0, responseBytes.Length,
SocketFlags.None, new AsyncCallback(this.FinishedRead),
responseBytes);
}
}
catch(Exception e)
{
Debug.WriteLine("Socket exception while checking responses: " + e.Message);
CloseConnection();
}
}
/// <summary>
/// Asynchronous callback from the socket read operation.
/// </summary>
/// <remarks>
/// Takes the raw message from the server and parses it using the <see cref="CommandParser"/>.
/// </remarks>
/// <param name="ar">The async result from the read operation</param>
private void FinishedRead(IAsyncResult ar)
{
string rawResponse;
if( m_Socket != null )
{
// AsyncState contains the byte array that was passed in to the BeginRead method
rawResponse = Encoding.ASCII.GetString((byte[]) ar.AsyncState);
Debug.WriteLine("Got response: " + rawResponse);
// Check for null, because it's possible the socket could've closed in the meantime.
if( m_Socket != null )
{
try{ m_Socket.EndReceive(ar); }
catch(Exception e)
{
// This usually occurs because the session was forcibly terminated.
Debug.WriteLine("Socket Exception: " + e.Message);
CloseConnection();
}
}
if( rawResponse.Length > 0 )
{
// Have the command parser parse the responses
Response[] responses = m_Parser.ParseResponses(rawResponse);
lock(m_ResponsesQueue.SyncRoot)
{
for( int i = 0; i < responses.Length; i++ )
{
if( responses[i].TransactionID != -1 )
{
lock(m_PendingRequests.SyncRoot)
{
if( m_PendingRequests.ContainsKey(responses[i].TransactionID) )
{
// Determine if this response was to any pending requests and match them up
responses[i].SetOriginatingRequest((Request) m_PendingRequests[responses[i].TransactionID]);
m_PendingRequests.Remove(responses[i].TransactionID);
}
}
}
// Add this to received responses
m_ResponsesQueue.Enqueue(responses[i]);
}
}
}
}
}
/// <summary>
/// Retrieves the next transaction ID
/// </summary>
/// <remarks>
/// This is a thread-safe atomic operation and garauntees unique transaction IDs across threads
/// </remarks>
/// <returns>The next transaction ID</returns>
private int GetTransactionID()
{
return Interlocked.Increment(ref m_TransactionID);
}
/// <summary>
/// Gets a description for an error number
/// </summary>
/// <param name="errorNum">The error to describe</param>
/// <returns>The description for the error</returns>
protected string GetErrorDescription(int errorNum)
{
switch(errorNum)
{
case ERR_SYNTAX_ERROR:
return "Syntax error in request";
case ERR_INVALID_PARAMETER:
return "Invalid parameter in request";
case ERR_INVALID_USER:
return "Invalid user";
case ERR_FQDN_MISSING:
return "Fully qualified domain name missing";
case ERR_ALREADY_LOGIN:
return "Already logged in";
case ERR_INVALID_USERNAME:
return "Invalid username";
case ERR_INVALID_FRIENDLY_NAME:
return "Invalid friendly name";
case ERR_LIST_FULL:
return "List full";
case ERR_ALREADY_THERE:
return "Already there";
case ERR_NOT_ON_LIST:
return "User not on list";
case ERR_USER_NOT_ONLINE:
return "User not online";
case ERR_ALREADY_IN_THE_MODE:
return "Already in that mode";
case ERR_ALREADY_IN_OPPOSITE_LIST:
return "Already in opposite list";
case ERR_SWITCHBOARD_FAILED:
return "Switchboard connection failed";
case ERR_NOTIFY_XFR_FAILED:
return "Notification transfer failed";
case ERR_REQUIRED_FIELDS_MISSING:
return "Required fields missing";
case ERR_NOT_LOGGED_IN:
return "Not logged in";
case ERR_INTERNAL_SERVER:
return "Internal server error";
case ERR_DB_SERVER:
return "DB server error";
case ERR_FILE_OPERATION:
return "Invalid file operation";
case ERR_MEMORY_ALLOC:
return "Error allocating memory";
case ERR_SERVER_BUSY:
return "Server too busy";
case ERR_SERVER_UNAVAILABLE:
return "Server unavailable";
case ERR_PEER_NS_DOWN:
return "Peer notification server is down";
case ERR_DB_CONNECT:
return "Error connecting to DB";
case ERR_SERVER_GOING_DOWN:
return "Server is going down";
case ERR_CREATE_CONNECTION:
return "Cannot create connection";
case ERR_BLOCKING_WRITE:
return "Write operation blockings";
case ERR_SESSION_OVERLOAD:
return "Session overload";
case ERR_USER_TOO_ACTIVE:
return "User too active";
case ERR_TOO_MANY_SESSIONS:
return "Too many active sessions";
case ERR_NOT_EXPECTED:
return "Not expected";
case ERR_BAD_FRIEND_FILE:
return "Bad friend file";
case ERR_AUTHENTICATION_FAILED:
return "Authentication failed";
case ERR_NOT_ALLOWED_WHEN_OFFLINE:
return "Not allowed while offline";
case ERR_NOT_ACCEPTING_NEW_USERS:
return "Not accepting new users";
default:
return "Unknown error";
}
}
/**********************************************/
// Constants
/**********************************************/
// MSN default port
public const int MSN_PORT = 1863;
// Protocol Command Constants
public const string ACK = "ACK";
public const string ADD = "ADD";
public const string ANS = "ANS";
public const string BLP = "BLP";
public const string BYE = "BYE";
public const string CAL = "CAL";
public const string CHG = "CHG";
public const string FLN = "FLN";
public const string GTC = "GTC";
public const string INF = "INF";
public const string ILN = "ILN";
public const string IRO = "IRO";
public const string JOI = "JOI";
public const string LST = "LST";
public const string MSG = "MSG";
public const string NAK = "NAK";
public const string NLN = "NLN";
public const string HDN = "HDN";
public const string OUT = "OUT";
public const string REM = "REM";
public const string RNG = "RNG";
public const string SYN = "SYN";
public const string USR = "USR";
public const string VER = "VER";
public const string XFR = "XFR";
public const string ERR = "ERR"; // This is not defined in the standard. This is for this application's use only
// Protocol Error Constants
public const int ERR_SYNTAX_ERROR = 200;
public const int ERR_INVALID_PARAMETER = 201;
public const int ERR_INVALID_USER = 205;
public const int ERR_FQDN_MISSING = 206;
public const int ERR_ALREADY_LOGIN = 207;
public const int ERR_INVALID_USERNAME = 208;
public const int ERR_INVALID_FRIENDLY_NAME = 209;
public const int ERR_LIST_FULL = 210;
public const int ERR_ALREADY_THERE = 215;
public const int ERR_NOT_ON_LIST = 216;
public const int ERR_USER_NOT_ONLINE = 217;
public const int ERR_ALREADY_IN_THE_MODE = 218;
public const int ERR_ALREADY_IN_OPPOSITE_LIST = 219;
public const int ERR_SWITCHBOARD_FAILED = 280;
public const int ERR_NOTIFY_XFR_FAILED = 281;
public const int ERR_REQUIRED_FIELDS_MISSING = 300;
public const int ERR_NOT_LOGGED_IN = 302;
public const int ERR_INTERNAL_SERVER = 500;
public const int ERR_DB_SERVER = 501;
public const int ERR_FILE_OPERATION = 510;
public const int ERR_MEMORY_ALLOC = 520;
public const int ERR_SERVER_BUSY = 600;
public const int ERR_SERVER_UNAVAILABLE = 601;
public const int ERR_PEER_NS_DOWN = 602;
public const int ERR_DB_CONNECT = 603;
public const int ERR_SERVER_GOING_DOWN = 604;
public const int ERR_CREATE_CONNECTION = 707;
public const int ERR_BLOCKING_WRITE = 711;
public const int ERR_SESSION_OVERLOAD = 712;
public const int ERR_USER_TOO_ACTIVE = 713;
public const int ERR_TOO_MANY_SESSIONS = 714;
public const int ERR_NOT_EXPECTED = 715;
public const int ERR_BAD_FRIEND_FILE = 717;
public const int ERR_AUTHENTICATION_FAILED = 911;
public const int ERR_NOT_ALLOWED_WHEN_OFFLINE = 913;
public const int ERR_NOT_ACCEPTING_NEW_USERS = 920;
// Protocol Dialect Constants
public const string DEFAULT_PROTOCOL = "MSNP2";
// Policies
public const string MD5 = "MD5";
public const string CKI = "CKI";
// Referral Types
public const string NS = "NS";
public const string SB = "SB";
// Online substates
public const string BSY = "BSY"; // Busy
public const string IDL = "IDL"; // Idle
public const string BRB = "BRB"; // Be right back
public const string AWY = "AWY"; // Away from computer
public const string PHN = "PHN"; // On the Phone
public const string LUN = "LUN"; // Out to lunch
// List types
public const string FL = "FL"; // Forward List
public const string RL = "RL"; // Reverse List
public const string AL = "AL"; // Access List
public const string BL = "BL"; // Block List
}
}
|