/*
* Thresher IRC client library
* Copyright (C) 2002 Aaron Hunter <thresher@sharkbite.org>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*
* See the gpl.txt file located in the top-level-directory of
* the archive of this library for complete text of license.
*/
using System.Runtime.InteropServices;
using System;
using System.Collections;
using System.Text;
using System.Threading;
using System.Net.Sockets;
using System.IO;
using System.Globalization;
using System.Diagnostics;
using System.Text.RegularExpressions;
#if SSL
using Org.Mentalis.Security.Ssl;
#endif
[assembly:CLSCompliant(true)]
[assembly:ComVisible(false)]
namespace Sharkbite.Irc{
/// <summary>
/// This class manages the connection to the IRC server and provides
/// access to all the objects needed to send and receive messages.
/// </summary>
public sealed class Connection
{
/// <summary>
/// Receive all the messages, unparsed, sent by the IRC server. This is not
/// normally needed but provided for those who are interested.
/// </summary>
public event RawMessageReceivedEventHandler OnRawMessageReceived;
/// <summary>
/// Receive all the raw messages sent to the IRC from this connection
/// </summary>
public event RawMessageSentEventHandler OnRawMessageSent;
#if SSL
private SecureTcpClient client;
#else
private TcpClient client;
#endif
private readonly Regex propertiesRegex;
private Listener listener;
private Sender sender;
private CtcpListener ctcpListener;
private CtcpSender ctcpSender;
private CtcpResponder ctcpResponder;
private bool ctcpEnabled;
private bool dccEnabled;
private Thread socketListenThread;
private StreamReader reader;
private DateTime timeLastSent;
//Connected and registered with IRC server
private bool registered;
//TCP/IP connection established with IRC server
private bool connected;
private bool handleNickFailure;
private ArrayList parsers;
private ServerProperties properties;
private Encoding encoding;
internal StreamWriter writer; //Access is internal for testing
internal ConnectionArgs connectionArgs;
/// <summary>
/// Used for internal test purposes only.
/// </summary>
internal Connection( ConnectionArgs args )
{
connectionArgs = args;
sender = new Sender( this );
listener = new Listener();
timeLastSent = DateTime.Now;
EnableCtcp = true;
EnableDcc = true;
TextEncoding = Encoding.Default;
}
/// <summary>
/// Prepare a connection to an IRC server but do not open it. This sets the text Encoding to Default.
/// </summary>
/// <param name="args">The set of information need to connect to an IRC server</param>
/// <param name="enableCtcp">True if this Connection should support CTCP.</param>
/// <param name="enableDcc">True if this Connection should support DCC.</param>
public Connection( ConnectionArgs args, bool enableCtcp, bool enableDcc )
{
propertiesRegex = new Regex("([A-Z]+)=([^\\s]+)", RegexOptions.Compiled | RegexOptions.Singleline);
registered = false;
connected = false;
handleNickFailure = true;
connectionArgs = args;
parsers = new ArrayList();
sender = new Sender( this );
listener = new Listener( );
RegisterDelegates();
timeLastSent = DateTime.Now;
EnableCtcp = enableCtcp;
EnableDcc = enableDcc;
TextEncoding = Encoding.Default;
}
/// <summary>
/// Prepare a connection to an IRC server but do not open it.
/// </summary>
/// <param name="args">The set of information need to connect to an IRC server</param>
/// <param name="enableCtcp">True if this Connection should support CTCP.</param>
/// <param name="enableDcc">True if this Connection should support DCC.</param>
/// <param name="textEncoding">The text encoding for the incoming stream.</param>
public Connection( Encoding textEncoding, ConnectionArgs args, bool enableCtcp, bool enableDcc ) : this( args,enableCtcp, enableDcc)
{
TextEncoding = textEncoding;
}
/// <summary>
/// Sets the text encoding used by the read and write streams.
/// Must be set before Connect() is called and should not be changed
/// while the connection is processing messages.
/// </summary>
/// <value>An Encoding constant.</value>
public Encoding TextEncoding
{
get
{
return encoding;
}
set
{
encoding = value;
}
}
/// <summary>
/// A read-only property indicating whether the connection
/// has been opened with the IRC server and the
/// client has been successfully registered.
/// </summary>
/// <value>True if the client is connected and registered.</value>
public bool Registered
{
get
{
return registered;
}
}
/// <summary>
/// A read-only property indicating whether a connection
/// has been opened with the IRC server (but not whether
/// registration has succeeded).
/// </summary>
/// <value>True if the client is connected.</value>
public bool Connected
{
get
{
return connected;
}
}
/// <summary>
/// By default the connection itself will handle the case
/// where, while attempting to register the client's nick
/// is already in use. It does this by simply appending
/// 2 random numbers to the end of the nick.
/// </summary>
/// <remarks>
/// The NickError event is shows that the nick collision has happened
/// and it is fixed by calling Sender's Register() method passing
/// in the replacement nickname.
/// </remarks>
/// <value>True if the connection should handle this case and
/// false if the client will handle it itself.</value>
public bool HandleNickTaken
{
get
{
return handleNickFailure;
}
set
{
handleNickFailure = value;
}
}
/// <summary>
/// A user friendly name of this Connection in the form 'nick@host'
/// </summary>
/// <value>Read only string</value>
public string Name
{
get
{
return connectionArgs.Nick + "@" + connectionArgs.Hostname;
}
}
/// <summary>
/// Whether Ctcp commands should be processed and if
/// Ctcp events will be raised.
/// </summary>
/// <value>True will enable the CTCP sender and listener and
/// false will cause their property calls to return null.</value>
public bool EnableCtcp
{
get
{
return ctcpEnabled;
}
set
{
if( value && !ctcpEnabled )
{
ctcpListener = new CtcpListener( this );
ctcpSender = new CtcpSender( this );
}
else if(!value )
{
ctcpListener = null;
ctcpSender = null;
}
ctcpEnabled = value;
}
}
/// <summary>
/// Whether DCC requests should be processed or ignored
/// by this Connection. Since the DccListener is a singleton and
/// shared by all Connections, listeners to DccListener events should
/// be manually removed when no longer needed.
/// </summary>
/// <value>True to process DCC requests.</value>
public bool EnableDcc
{
get
{
return dccEnabled;
}
set
{
dccEnabled = value;
}
}
/// <summary>
/// Sets an automatic responder to Ctcp queries.
/// </summary>
/// <value>Once this is set it can be removed by setting it to null.</value>
public CtcpResponder CtcpResponder
{
get
{
return ctcpResponder;
}
set
{
if( value == null && ctcpResponder != null )
{
ctcpResponder.Disable();
}
ctcpResponder = value;
}
}
/// <summary>
/// The amount of time that has passed since the client
/// sent a command to the IRC server.
/// </summary>
/// <value>Read only TimeSpan</value>
public TimeSpan IdleTime
{
get
{
return DateTime.Now - timeLastSent;
}
}
/// <summary>
/// The object used to send commands to the IRC server.
/// </summary>
/// <value>Read-only Sender.</value>
public Sender Sender
{
get
{
return sender;
}
}
/// <summary>
/// The object that parses messages and notifies the appropriate delegate.
/// </summary>
/// <value>Read only Listener.</value>
public Listener Listener
{
get
{
return listener;
}
}
/// <summary>
/// The object used to send CTCP commands to the IRC server.
/// </summary>
/// <value>Read only CtcpSender. Null if CtcpEnabled is false.</value>
public CtcpSender CtcpSender
{
get
{
return ctcpSender;
}
}
/// <summary>
/// The object that parses CTCP messages and notifies the appropriate delegate.
/// </summary>
/// <value>Read only CtcpListener. Null if CtcpEnabled is false.</value>
public CtcpListener CtcpListener
{
get
{
if( ctcpEnabled )
{
return ctcpListener;
}
else
{
return null;
}
}
}
/// <summary>
/// The collection of data used to establish this connection.
/// </summary>
/// <value>Read only ConnectionArgs.</value>
public ConnectionArgs ConnectionData
{
get
{
return connectionArgs;
}
}
/// <summary>
/// A read-only collection of string key/value pairs
/// representing IRC server proprties.
/// </summary>
/// <value>This connection's ServerProperties obejct or null if it
/// has not been created.</value>
public ServerProperties ServerProperties
{
get
{
return properties;
}
}
private bool CustomParse( string line )
{
foreach( IParser parser in parsers )
{
if( parser.CanParse( line ) )
{
parser.Parse( line );
return true;
}
}
return false;
}
/// <summary>
/// Respond to IRC keep-alives.
/// </summary>
/// <param name="message">The message that should be echoed back</param>
private void KeepAlive(string message)
{
sender.Pong( message );
}
/// <summary>
/// Update the ConnectionArgs object when the user
/// changes his nick.
/// </summary>
/// <param name="user">Who changed their nick</param>
/// <param name="newNick">The new nick name</param>
private void MyNickChanged(UserInfo user, string newNick)
{
if ( connectionArgs.Nick == user.Nick )
{
connectionArgs.Nick = newNick;
}
}
private void OnRegistered()
{
registered = true;
listener.OnRegistered -= new RegisteredEventHandler( OnRegistered );
}
/// <summary>
///
/// </summary>
/// <param name="badNick"></param>
/// <param name="reason"></param>
private void OnNickError( string badNick, string reason )
{
//If this is our initial connection attempt
if( !registered && handleNickFailure )
{
NameGenerator generator = new NameGenerator();
string nick;
do
{
nick = generator.MakeName();
}
while( !Rfc2812Util.IsValidNick( nick) || nick.Length == 1 );
//Try to reconnect
Sender.Register( nick );
}
}
/// <summary>
/// Listen for the 005 info messages sent during registration so that the maximum lengths
/// of certain items (Nick, Away, Topic) can be determined dynamically.
/// </summary>
/// <param name="code">Reply code enum</param>
/// <param name="info">An info line</param>
private void OnReply( ReplyCode code, string info)
{
if( code == ReplyCode.RPL_BOUNCE ) //Code 005
{
//Lazy instantiation
if( properties == null )
{
properties = new ServerProperties();
}
//Populate properties from name/value matches
MatchCollection matches = propertiesRegex.Matches( info );
if( matches.Count > 0 )
{
foreach( Match match in matches )
{
properties.SetProperty(match.Groups[1].ToString(), match.Groups[2].ToString() );
}
}
//Extract ones we are interested in
ExtractProperties();
}
}
private void ExtractProperties()
{
//For the moment the only one we care about is NickLen
//In fact we don't cae about any but keep here as an example
/*
if( properties.ContainsKey("NICKLEN") )
{
try
{
maxNickLength = int.Parse( properties[ "NICKLEN" ] );
}
catch( Exception e )
{
}
}
*/
}
private void RegisterDelegates()
{
listener.OnPing += new PingEventHandler( KeepAlive );
listener.OnNick += new NickEventHandler( MyNickChanged );
listener.OnNickError += new NickErrorEventHandler( OnNickError );
listener.OnReply += new ReplyEventHandler( OnReply );
listener.OnRegistered += new RegisteredEventHandler( OnRegistered );
}
#if SSL
private void ConnectClient( SecureProtocol protocol )
{
lock ( this )
{
if( connected )
{
throw new Exception("Connection with IRC server already opened.");
}
Debug.WriteLineIf( Rfc2812Util.IrcTrace.TraceInfo,"[" + Thread.CurrentThread.Name +"] Connection::Connect()");
SecurityOptions options = new SecurityOptions( protocol );
options.Certificate = null;
options.Entity = ConnectionEnd.Client;
options.VerificationType = CredentialVerification.None;
options.Flags = SecurityFlags.Default;
options.AllowedAlgorithms = SslAlgorithms.SECURE_CIPHERS;
client = new SecureTcpClient( options );
client.Connect( connectionArgs.Hostname, connectionArgs.Port );
connected = true;
writer = new StreamWriter( client.GetStream(), TextEncoding );
writer.AutoFlush = true;
reader = new StreamReader( client.GetStream(), TextEncoding );
socketListenThread = new Thread(new ThreadStart( ReceiveIRCMessages ) );
socketListenThread.Name = Name;
socketListenThread.Start();
sender.RegisterConnection( connectionArgs );
}
}
#endif
/// <summary>
/// Read in message lines from the IRC server
/// and send them to a parser for processing.
/// Discard CTCP and DCC messages if these protocols
/// are not enabled.
/// </summary>
internal void ReceiveIRCMessages()
{
Debug.WriteLineIf( Rfc2812Util.IrcTrace.TraceInfo, "[" + Thread.CurrentThread.Name +"] Connection::ReceiveIRCMessages()");
string line;
try
{
while ( (line = reader.ReadLine() ) != null )
{
try
{
Debug.WriteLineIf( Rfc2812Util.IrcTrace.TraceVerbose, "[" + Thread.CurrentThread.Name +"] Connection::ReceiveIRCMessages() rec'd:" + line );
//Try any custom parsers first
if( CustomParse( line ) )
{
//One of the custom parsers handled this message so
//we go back to listening
continue;
}
if( DccListener.IsDccRequest( line ) )
{
if( dccEnabled )
{
DccListener.DefaultInstance.Parse( this, line );
}
}
else if( CtcpListener.IsCtcpMessage( line) )
{
if( ctcpEnabled )
{
ctcpListener.Parse( line );
}
}
else
{
listener.Parse( line );
}
if( OnRawMessageReceived != null )
{
OnRawMessageReceived( line );
}
}
catch( ThreadAbortException e )
{
Thread.ResetAbort();
//This exception is raised when the Thread
//is stopped at user request, i.e. via Disconnect()
//This will stop the read loop and close the connection.
break;
}
}
}
catch (IOException e)
{
//Trap a connection failure
Debug.WriteLineIf( Rfc2812Util.IrcTrace.TraceWarning, "[" + Thread.CurrentThread.Name +"] Connection::ReceiveIRCMessages() IO Error while listening for messages " + e);
listener.Error( ReplyCode.ConnectionFailed, "Connection to server unexpectedly failed.");
}
//The connection to the IRC server has been closed either
//by client request or the server itself closed the connection.
client.Close();
registered = false;
connected = false;
listener.Disconnected();
}
/// <summary>
/// Send a message to the IRC server and clear the command buffer.
/// </summary>
internal void SendCommand( StringBuilder command)
{
try
{
writer.WriteLine( command.ToString() );
Debug.WriteLineIf( Rfc2812Util.IrcTrace.TraceVerbose,"[" + Thread.CurrentThread.Name +"] Connection::SendCommand() sent= " + command);
timeLastSent = DateTime.Now;
}
catch( Exception e )
{
Debug.WriteLineIf( Rfc2812Util.IrcTrace.TraceWarning,"[" + Thread.CurrentThread.Name +"] Connection::SendCommand() exception=" + e);
}
if( OnRawMessageSent != null )
{
OnRawMessageSent( command.ToString() );
}
command.Remove(0, command.Length );
}
/// <summary>
/// Send a message to the IRC server which does
/// not affect the client's idle time. Used for automatic replies
/// such as PONG or Ctcp repsones.
/// </summary>
internal void SendAutomaticReply( StringBuilder command)
{
try
{
writer.WriteLine( command.ToString() );
Debug.WriteLineIf( Rfc2812Util.IrcTrace.TraceVerbose,"[" + Thread.CurrentThread.Name +"] Connection::SendAutomaticReply() message=" + command);
}
catch( Exception e )
{
Debug.WriteLineIf( Rfc2812Util.IrcTrace.TraceWarning,"[" + Thread.CurrentThread.Name +"] Connection::SendAutomaticReply() exception=" + e );
}
command.Remove(0, command.Length );
}
#if SSL
///<summary>
/// Connect to the IRC server and start listening for messages
/// on a new thread.
/// </summary>
/// <exception cref="SocketException">If a connection cannot be established with the IRC server</exception>
public void Connect()
{
Debug.WriteLineIf( Rfc2812Util.IrcTrace.TraceInfo,"Connecting over clear socket");
ConnectClient( SecureProtocol.None );
}
///<summary>
/// Connect to the IRC server over an encrypted connection using TLS.
/// </summary>
/// <exception cref="SocketException">If a connection cannot be established with the IRC server</exception>
public void SecureConnect()
{
Debug.WriteLineIf( Rfc2812Util.IrcTrace.TraceInfo,"Connecting over encrypted socket");
ConnectClient( SecureProtocol.Tls1 );
}
#else
/// <summary>
/// Connect to the IRC server and start listening for messages
/// on a new thread.
/// </summary>
/// <exception cref="SocketException">If a connection cannot be established with the IRC server</exception>
public void Connect()
{
lock ( this )
{
if( connected )
{
throw new Exception("Connection with IRC server already opened.");
}
Debug.WriteLineIf( Rfc2812Util.IrcTrace.TraceInfo,"[" + Thread.CurrentThread.Name +"] Connection::Connect()");
client = new TcpClient();
client.Connect( connectionArgs.Hostname, connectionArgs.Port );
connected = true;
writer = new StreamWriter( client.GetStream(), TextEncoding );
writer.AutoFlush = true;
reader = new StreamReader( client.GetStream(), TextEncoding );
socketListenThread = new Thread(new ThreadStart( ReceiveIRCMessages ) );
socketListenThread.Name = Name;
socketListenThread.Start();
sender.RegisterConnection( connectionArgs );
}
}
#endif
/// <summary>
/// Sends a 'Quit' message to the server, closes the connection,
/// and stops the listening thread.
/// </summary>
/// <remarks>The state of the connection will remain the same even after a disconnect,
/// so the connection can be reopened. All the event handlers will remain registered.</remarks>
/// <param name="reason">A message displayed to IRC users upon disconnect.</param>
public void Disconnect( string reason )
{
lock ( this )
{
if( !connected )
{
throw new Exception("Not connected to IRC server.");
}
Debug.WriteLineIf( Rfc2812Util.IrcTrace.TraceInfo,"[" + Thread.CurrentThread.Name +"] Connection::Disconnect()");
listener.Disconnecting();
sender.Quit( reason );
listener.Disconnected();
//Thanks to Thomas for this next block
if ( socketListenThread.Join( TimeSpan.FromSeconds( 1 ) ) == false )
socketListenThread.Abort();
}
}
/// <summary>
/// A friendly name for this connection.
/// </summary>
/// <returns>The Name property</returns>
public override string ToString()
{
return this.Name;
}
/// <summary>
/// Adds a parser class to a list of custom parsers.
/// Any number can be added. The custom parsers
/// will be tested using <c>CanParse()</c> before
/// the default parsers. The last parser to be added
/// will be the first to process a message.
/// </summary>
/// <param name="parser">Any class that implements IParser.</param>
public void AddParser( IParser parser )
{
parsers.Insert(0, parser );
}
/// <summary>
/// Remove a custom parser class.
/// </summary>
/// <param name="parser">Any class that implements IParser.</param>
public void RemoveParser( IParser parser )
{
parsers.Remove( parser );
}
}
}
|