/* * Copyright (c) Contributors, http://opensimulator.org/ * See CONTRIBUTORS.TXT for a full list of copyright holders. * * 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 the OpenSimulator Project 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 DEVELOPERS ``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 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.Timers; using System.Collections.Generic; using System.IO; using System.Net.Sockets; using System.Reflection; using System.Text.RegularExpressions; using System.Threading; using OpenMetaverse; using log4net; using Nini.Config; using OpenSim.Framework; using OpenSim.Framework.Monitoring; using OpenSim.Region.Framework.Interfaces; using OpenSim.Region.Framework.Scenes; namespace OpenSim.Region.OptionalModules.Avatar.Chat { public class IRCConnector { #region Global (static) state private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); // Local constants // This computation is not the real region center if the region is larger than 256. // This computation isn't fixed because there is not a handle back to the region. private static readonly Vector3 CenterOfRegion = new Vector3(((int)Constants.RegionSize * 0.5f), ((int)Constants.RegionSize * 0.5f), 20); private static readonly char[] CS_SPACE = { ' ' }; private const int WD_INTERVAL = 1000; // base watchdog interval private static int PING_PERIOD = 15; // WD intervals per PING private static int ICCD_PERIOD = 10; // WD intervals between Connects private static int L_TIMEOUT = 25; // Login time out interval private static int _idk_ = 0; // core connector identifier private static int _pdk_ = 0; // ping interval counter private static int _icc_ = ICCD_PERIOD; // IRC connect counter // List of configured connectors private static List m_connectors = new List(); // Watchdog state private static System.Timers.Timer m_watchdog = null; // The watch-dog gets started as soon as the class is instantiated, and // ticks once every second (WD_INTERVAL) static IRCConnector() { m_log.DebugFormat("[IRC-Connector]: Static initialization started"); m_watchdog = new System.Timers.Timer(WD_INTERVAL); m_watchdog.Elapsed += new ElapsedEventHandler(WatchdogHandler); m_watchdog.AutoReset = true; m_watchdog.Start(); m_log.DebugFormat("[IRC-Connector]: Static initialization complete"); } #endregion #region Instance state // Connector identity internal int idn = _idk_++; // How many regions depend upon this connection // This count is updated by the ChannelState object and reflects the sum // of the region clients associated with the set of associated channel // state instances. That's why it cannot be managed here. internal int depends = 0; // This variable counts the number of resets that have been performed // on the connector. When a listener thread terminates, it checks to // see of the reset count has changed before it schedules another // reset. internal int m_resetk = 0; private Object msyncConnect = new Object(); internal bool m_randomizeNick = true; // add random suffix internal string m_baseNick = null; // base name for randomizing internal string m_nick = null; // effective nickname public string Nick // Public property { get { return m_nick; } set { m_nick = value; } } private bool m_enabled = false; // connector enablement public bool Enabled { get { return m_enabled; } } private bool m_connected = false; // connection status private bool m_pending = false; // login disposition private int m_timeout = L_TIMEOUT; // login timeout counter public bool Connected { get { return m_connected; } } private string m_ircChannel; // associated channel id public string IrcChannel { get { return m_ircChannel; } set { m_ircChannel = value; } } private uint m_port = 6667; // session port public uint Port { get { return m_port; } set { m_port = value; } } private string m_server = null; // IRC server name public string Server { get { return m_server; } set { m_server = value; } } private string m_password = null; public string Password { get { return m_password; } set { m_password = value; } } private string m_user = "USER OpenSimBot 8 * :I'm an OpenSim to IRC bot"; public string User { get { return m_user; } } // Network interface private TcpClient m_tcp; private NetworkStream m_stream = null; private StreamReader m_reader; private StreamWriter m_writer; // Channel characteristic info (if available) internal string usermod = String.Empty; internal string chanmod = String.Empty; internal string version = String.Empty; internal bool motd = false; #endregion #region connector instance management internal IRCConnector(ChannelState cs) { // Prepare network interface m_tcp = null; m_writer = null; m_reader = null; // Setup IRC session parameters m_server = cs.Server; m_password = cs.Password; m_baseNick = cs.BaseNickname; m_randomizeNick = cs.RandomizeNickname; m_ircChannel = cs.IrcChannel; m_port = cs.Port; m_user = cs.User; if (m_watchdog == null) { // Non-differentiating ICCD_PERIOD = cs.ConnectDelay; PING_PERIOD = cs.PingDelay; // Smaller values are not reasonable if (ICCD_PERIOD < 5) ICCD_PERIOD = 5; if (PING_PERIOD < 5) PING_PERIOD = 5; _icc_ = ICCD_PERIOD; // get started right away! } // The last line of defense if (m_server == null || m_baseNick == null || m_ircChannel == null || m_user == null) throw new Exception("Invalid connector configuration"); // Generate an initial nickname if (m_randomizeNick) m_nick = m_baseNick + Random.Shared.Next(1, 99); else m_nick = m_baseNick; m_log.InfoFormat("[IRC-Connector-{0}]: Initialization complete", idn); } ~IRCConnector() { m_watchdog.Stop(); Close(); } // Mark the connector as connectable. Harmless if already enabled. public void Open() { if (!m_enabled) { if (!Connected) { Connect(); } lock (m_connectors) m_connectors.Add(this); m_enabled = true; } } // Only close the connector if the dependency count is zero. public void Close() { m_log.InfoFormat("[IRC-Connector-{0}] Closing", idn); lock (msyncConnect) { if ((depends == 0) && Enabled) { m_enabled = false; if (Connected) { m_log.DebugFormat("[IRC-Connector-{0}] Closing interface", idn); // Cleanup the IRC session try { m_writer.WriteLine(String.Format("QUIT :{0} to {1} wormhole to {2} closing", m_nick, m_ircChannel, m_server)); m_writer.Flush(); } catch (Exception) { } m_connected = false; try { m_writer.Close(); } catch (Exception) { } try { m_reader.Close(); } catch (Exception) { } try { m_stream.Close(); } catch (Exception) { } try { m_tcp.Close(); } catch (Exception) { } } lock (m_connectors) m_connectors.Remove(this); } } m_log.InfoFormat("[IRC-Connector-{0}] Closed", idn); } #endregion #region session management // Connect to the IRC server. A connector should always be connected, once enabled public void Connect() { if (!m_enabled) return; // Delay until next WD cycle if this is too close to the last start attempt if(_icc_ < ICCD_PERIOD) return; m_log.DebugFormat("[IRC-Connector-{0}]: Connection request for {1} on {2}:{3}", idn, m_nick, m_server, m_ircChannel); _icc_ = 0; lock (msyncConnect) { try { if (m_connected) return; m_connected = true; m_pending = true; m_timeout = L_TIMEOUT; m_tcp = new TcpClient(m_server, (int)m_port); m_stream = m_tcp.GetStream(); m_reader = new StreamReader(m_stream); m_writer = new StreamWriter(m_stream); m_log.InfoFormat("[IRC-Connector-{0}]: Connected to {1}:{2}", idn, m_server, m_port); WorkManager.StartThread(ListenerRun, "IRCConnectionListenerThread", false, false); // This is the message order recommended by RFC 2812 if (m_password != null) m_writer.WriteLine(String.Format("PASS {0}", m_password)); m_writer.WriteLine(String.Format("NICK {0}", m_nick)); m_writer.Flush(); m_writer.WriteLine(String.Format("USER {0} 0 * :OpenSim Relay",m_user)); m_writer.Flush(); } catch (Exception e) { m_log.ErrorFormat("[IRC-Connector-{0}] cannot connect {1} to {2}:{3}: {4}", idn, m_nick, m_server, m_port, e.Message); // It might seem reasonable to reset connected and pending status here // Seeing as we know that the login has failed, but if we do that, then // connection will be retried each time the interconnection interval // expires. By leaving them as they are, the connection will be retried // when the login timeout expires. Which is preferred. } } return; } // Reconnect is used to force a re-cycle of the IRC connection. Should generally // be a transparent event public void Reconnect() { m_log.DebugFormat("[IRC-Connector-{0}]: Reconnect request for {1} on {2}:{3}", idn, m_nick, m_server, m_ircChannel); // Don't do this if a Connect is in progress... lock (msyncConnect) { if (m_connected) { m_log.InfoFormat("[IRC-Connector-{0}] Resetting connector", idn); // Mark as disconnected. This will allow the listener thread // to exit if still in-flight. // The listener thread is not aborted - it *might* actually be // the thread that is running the Reconnect! Instead just close // the socket and it will disappear of its own accord, once this // processing is completed. try { m_writer.Close(); } catch (Exception) { } try { m_reader.Close(); } catch (Exception) { } try { m_tcp.Close(); } catch (Exception) { } m_connected = false; m_pending = false; m_resetk++; } } Connect(); } #endregion #region Outbound (to-IRC) message handlers public void PrivMsg(string pattern, string from, string region, string msg) { // m_log.DebugFormat("[IRC-Connector-{0}] PrivMsg to IRC from {1}: <{2}>", idn, from, // String.Format(pattern, m_ircChannel, from, region, msg)); // One message to the IRC server try { m_writer.WriteLine(pattern, m_ircChannel, from, region, msg); m_writer.Flush(); // m_log.DebugFormat("[IRC-Connector-{0}]: PrivMsg from {1} in {2}: {3}", idn, from, region, msg); } catch (IOException) { m_log.ErrorFormat("[IRC-Connector-{0}]: PrivMsg I/O Error: disconnected from IRC server", idn); Reconnect(); } catch (Exception ex) { m_log.ErrorFormat("[IRC-Connector-{0}]: PrivMsg exception : {1}", idn, ex.Message); m_log.Debug(ex); } } public void Send(string msg) { // m_log.DebugFormat("[IRC-Connector-{0}] Send to IRC : <{1}>", idn, msg); try { m_writer.WriteLine(msg); m_writer.Flush(); // m_log.DebugFormat("[IRC-Connector-{0}] Sent command string: {1}", idn, msg); } catch (IOException) { m_log.ErrorFormat("[IRC-Connector-{0}] Disconnected from IRC server.(Send)", idn); Reconnect(); } catch (Exception ex) { m_log.ErrorFormat("[IRC-Connector-{0}] Send exception trap: {0}", idn, ex.Message); m_log.Debug(ex); } } #endregion public void ListenerRun() { string inputLine; int resetk = m_resetk; try { while (m_enabled && m_connected) { if ((inputLine = m_reader.ReadLine()) == null) throw new Exception("Listener input socket closed"); Watchdog.UpdateThread(); // m_log.Info("[IRCConnector]: " + inputLine); if (inputLine.Contains("PRIVMSG")) { Dictionary data = ExtractMsg(inputLine); // Any chat ??? if (data != null) { OSChatMessage c = new OSChatMessage { Message = data["msg"], Type = ChatTypeEnum.Region, From = data["nick"] + "@IRC" }; // Is message "\001ACTION foo bar\001"? // Then change to: "/me foo bar" if ((1 == c.Message[0]) && c.Message.Substring(1).StartsWith("ACTION")) c.Message = String.Format("/me {0}", c.Message.Substring(8, c.Message.Length - 9)); ChannelState.OSChat(this, c, false); } } else { ProcessIRCCommand(inputLine); } } } catch (Exception /*e*/) { // m_log.ErrorFormat("[IRC-Connector-{0}]: ListenerRun exception trap: {1}", idn, e.Message); // m_log.Debug(e); } // This is potentially circular, but harmless if so. // The connection is marked as not connected the first time // through reconnect. if (m_enabled && (m_resetk == resetk)) Reconnect(); Watchdog.RemoveThread(); } private Regex RE = new Regex(@":(?[\w-]*)!(?\S*) PRIVMSG (?\S+) :(?.*)", RegexOptions.Multiline); private Dictionary ExtractMsg(string input) { //examines IRC commands and extracts any private messages // which will then be reboadcast in the Sim // m_log.InfoFormat("[IRC-Connector-{0}]: ExtractMsg: {1}", idn, input); Dictionary result = null; MatchCollection matches = RE.Matches(input); // Get some direct matches $1 $4 is a if ((matches.Count == 0) || (matches.Count != 1) || (matches[0].Groups.Count != 5)) { // m_log.Info("[IRCConnector]: Number of matches: " + matches.Count); // if (matches.Count > 0) // { // m_log.Info("[IRCConnector]: Number of groups: " + matches[0].Groups.Count); // } return null; } result = new Dictionary(); result.Add("nick", matches[0].Groups[1].Value); result.Add("user", matches[0].Groups[2].Value); result.Add("channel", matches[0].Groups[3].Value); result.Add("msg", matches[0].Groups[4].Value); return result; } public void BroadcastSim(string sender, string format, params string[] args) { try { OSChatMessage c = new OSChatMessage { From = sender, Message = String.Format(format, args), Type = ChatTypeEnum.Region }; ChannelState.OSChat(this, c, true); } catch (Exception ex) // IRC gate should not crash Sim { m_log.ErrorFormat("[IRC-Connector-{0}]: BroadcastSim Exception Trap: {1}\n{2}", idn, ex.Message, ex.StackTrace); } } #region IRC Command Handlers public void ProcessIRCCommand(string command) { string[] commArgs; string c_server = m_server; string pfx = String.Empty; string cmd = String.Empty; string parms = String.Empty; // ":" indicates that a prefix is present // There are NEVER more than 17 real // fields. A parameter that starts with // ":" indicates that the remainder of the // line is a single parameter value. commArgs = command.Split(CS_SPACE, 2); if (commArgs[0].StartsWith(":")) { pfx = commArgs[0].Substring(1); commArgs = commArgs[1].Split(CS_SPACE, 2); } cmd = commArgs[0]; parms = commArgs[1]; // m_log.DebugFormat("[IRC-Connector-{0}] prefix = <{1}> cmd = <{2}>", idn, pfx, cmd); switch (cmd) { // Messages 001-004 are always sent // following signon. case "001": // Welcome ... case "002": // Server information case "003": // Welcome ... break; case "004": // Server information m_log.DebugFormat("[IRC-Connector-{0}] [{1}] parms = <{2}>", idn, cmd, parms); commArgs = parms.Split(CS_SPACE); c_server = commArgs[1]; m_server = c_server; version = commArgs[2]; usermod = commArgs[3]; chanmod = commArgs[4]; m_writer.WriteLine(String.Format("JOIN {0}", m_ircChannel)); m_writer.Flush(); m_log.InfoFormat("[IRC-Connector-{0}]: sent request to join {1} ", idn, m_ircChannel); break; case "005": // Server information break; case "042": case "250": case "251": case "252": case "254": case "255": case "265": case "266": case "332": // Subject case "333": // Subject owner (?) case "353": // Name list case "366": // End-of-Name list marker case "372": // MOTD body case "375": // MOTD start // m_log.InfoFormat("[IRC-Connector-{0}] [{1}] {2}", idn, cmd, parms.Split(CS_SPACE,2)[1]); break; case "376": // MOTD end // m_log.InfoFormat("[IRC-Connector-{0}] [{1}] {2}", idn, cmd, parms.Split(CS_SPACE,2)[1]); motd = true; break; case "451": // Not registered break; case "433": // Nickname in use // Gen a new name m_nick = m_baseNick + Random.Shared.Next(1, 99); m_log.ErrorFormat("[IRC-Connector-{0}]: [{1}] IRC SERVER reports NicknameInUse, trying {2}", idn, cmd, m_nick); // Retry m_writer.WriteLine(String.Format("NICK {0}", m_nick)); m_writer.Flush(); m_writer.WriteLine(m_user); m_writer.Flush(); m_writer.WriteLine(String.Format("JOIN {0}", m_ircChannel)); m_writer.Flush(); break; case "479": // Bad channel name, etc. This will never work, so disable the connection m_log.ErrorFormat("[IRC-Connector-{0}] [{1}] {2}", idn, cmd, parms.Split(CS_SPACE, 2)[1]); m_log.ErrorFormat("[IRC-Connector-{0}] [{1}] Connector disabled", idn, cmd); m_enabled = false; m_connected = false; m_pending = false; break; case "NOTICE": // m_log.WarnFormat("[IRC-Connector-{0}] [{1}] {2}", idn, cmd, parms.Split(CS_SPACE,2)[1]); break; case "ERROR": m_log.ErrorFormat("[IRC-Connector-{0}] [{1}] {2}", idn, cmd, parms.Split(CS_SPACE, 2)[1]); if (parms.Contains("reconnect too fast")) ICCD_PERIOD++; m_pending = false; Reconnect(); break; case "PING": m_log.DebugFormat("[IRC-Connector-{0}] [{1}] parms = <{2}>", idn, cmd, parms); m_writer.WriteLine(String.Format("PONG {0}", parms)); m_writer.Flush(); break; case "PONG": break; case "JOIN": m_log.DebugFormat("[IRC-Connector-{0}] [{1}] parms = <{2}>", idn, cmd, parms); eventIrcJoin(pfx, cmd, parms); break; case "PART": m_log.DebugFormat("[IRC-Connector-{0}] [{1}] parms = <{2}>", idn, cmd, parms); eventIrcPart(pfx, cmd, parms); break; case "MODE": m_log.DebugFormat("[IRC-Connector-{0}] [{1}] parms = <{2}>", idn, cmd, parms); eventIrcMode(pfx, cmd, parms); break; case "NICK": m_log.DebugFormat("[IRC-Connector-{0}] [{1}] parms = <{2}>", idn, cmd, parms); eventIrcNickChange(pfx, cmd, parms); break; case "KICK": m_log.DebugFormat("[IRC-Connector-{0}] [{1}] parms = <{2}>", idn, cmd, parms); eventIrcKick(pfx, cmd, parms); break; case "QUIT": m_log.DebugFormat("[IRC-Connector-{0}] [{1}] parms = <{2}>", idn, cmd, parms); eventIrcQuit(pfx, cmd, parms); break; default: m_log.DebugFormat("[IRC-Connector-{0}] Command '{1}' ignored, parms = {2}", idn, cmd, parms); break; } // m_log.DebugFormat("[IRC-Connector-{0}] prefix = <{1}> cmd = <{2}> complete", idn, pfx, cmd); } public void eventIrcJoin(string prefix, string command, string parms) { string[] args = parms.Split(CS_SPACE, 2); string IrcUser = prefix.Split('!')[0]; string IrcChannel = args[0]; if (IrcChannel.StartsWith(":")) IrcChannel = IrcChannel.Substring(1); if(IrcChannel == m_ircChannel) { m_log.InfoFormat("[IRC-Connector-{0}] Joined requested channel {1} at {2}", idn, IrcChannel,m_server); m_pending = false; } else m_log.InfoFormat("[IRC-Connector-{0}] Joined unknown channel {1} at {2}", idn, IrcChannel,m_server); BroadcastSim(IrcUser, "/me joins {0}", IrcChannel); } public void eventIrcPart(string prefix, string command, string parms) { string[] args = parms.Split(CS_SPACE, 2); string IrcUser = prefix.Split('!')[0]; string IrcChannel = args[0]; m_log.DebugFormat("[IRC-Connector-{0}] Event: IRCPart {1}:{2}", idn, m_server, m_ircChannel); BroadcastSim(IrcUser, "/me parts {0}", IrcChannel); } public void eventIrcMode(string prefix, string command, string parms) { string[] args = parms.Split(CS_SPACE, 2); string UserMode = args[1]; m_log.DebugFormat("[IRC-Connector-{0}] Event: IRCMode {1}:{2}", idn, m_server, m_ircChannel); if (UserMode.Substring(0, 1) == ":") { UserMode = UserMode.Remove(0, 1); } } public void eventIrcNickChange(string prefix, string command, string parms) { string[] args = parms.Split(CS_SPACE, 2); string UserOldNick = prefix.Split('!')[0]; string UserNewNick = args[0].Remove(0, 1); m_log.DebugFormat("[IRC-Connector-{0}] Event: IRCNickChange {1}:{2}", idn, m_server, m_ircChannel); BroadcastSim(UserOldNick, "/me is now known as {0}", UserNewNick); } public void eventIrcKick(string prefix, string command, string parms) { string[] args = parms.Split(CS_SPACE, 3); string UserKicker = prefix.Split('!')[0]; string IrcChannel = args[0]; string UserKicked = args[1]; string KickMessage = args[2]; m_log.DebugFormat("[IRC-Connector-{0}] Event: IRCKick {1}:{2}", idn, m_server, m_ircChannel); BroadcastSim(UserKicker, "/me kicks kicks {0} off {1} saying \"{2}\"", UserKicked, IrcChannel, KickMessage); if (UserKicked == m_nick) { BroadcastSim(m_nick, "Hey, that was me!!!"); } } public void eventIrcQuit(string prefix, string command, string parms) { string IrcUser = prefix.Split('!')[0]; string QuitMessage = parms; m_log.DebugFormat("[IRC-Connector-{0}] Event: IRCQuit {1}:{2}", idn, m_server, m_ircChannel); BroadcastSim(IrcUser, "/me quits saying \"{0}\"", QuitMessage); } #endregion #region Connector Watch Dog // A single watch dog monitors extant connectors and makes sure that they // are re-connected as necessary. If a connector IS connected, then it is // pinged, but only if a PING period has elapsed. protected static void WatchdogHandler(Object source, ElapsedEventArgs args) { // m_log.InfoFormat("[IRC-Watchdog] Status scan, pdk = {0}, icc = {1}", _pdk_, _icc_); _pdk_ = (_pdk_ + 1) % PING_PERIOD; // cycle the ping trigger _icc_++; // increment the inter-consecutive-connect-delay counter lock (m_connectors) foreach (IRCConnector connector in m_connectors) { // m_log.InfoFormat("[IRC-Watchdog] Scanning {0}", connector); if (connector.Enabled) { if (!connector.Connected) { try { // m_log.DebugFormat("[IRC-Watchdog] Connecting {1}:{2}", connector.idn, connector.m_server, connector.m_ircChannel); connector.Connect(); } catch (Exception e) { m_log.ErrorFormat("[IRC-Watchdog] Exception on connector {0}: {1} ", connector.idn, e.Message); } } else { if (connector.m_pending) { if (connector.m_timeout == 0) { m_log.ErrorFormat("[IRC-Watchdog] Login timed-out for connector {0}, reconnecting", connector.idn); connector.Reconnect(); } else connector.m_timeout--; } // Being marked connected is not enough to ping. Socket establishment can sometimes take a long // time, in which case the watch dog might try to ping the server before the socket has been // set up, with nasty side-effects. else if (_pdk_ == 0) { try { connector.m_writer.WriteLine(String.Format("PING :{0}", connector.m_server)); connector.m_writer.Flush(); } catch (Exception e) { m_log.ErrorFormat("[IRC-PingRun] Exception on connector {0}: {1} ", connector.idn, e.Message); m_log.Debug(e); connector.Reconnect(); } } } } } // m_log.InfoFormat("[IRC-Watchdog] Status scan completed"); } #endregion } }