/* * 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.Collections.Generic; using System.Reflection; using System.Text.RegularExpressions; using log4net; using Nini.Config; using OpenSim.Framework; using OpenSim.Region.Framework.Interfaces; using OpenSim.Region.Framework.Scenes; namespace OpenSim.Region.OptionalModules.Avatar.Chat { // An instance of this class exists for each unique combination of // IRC chat interface characteristics, as determined by the supplied // configuration file. internal class ChannelState { private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); private static Regex arg = new Regex(@"\[[^\[\]]*\]"); private static int _idk_ = 0; private static int DEBUG_CHANNEL = 2147483647; // These are the IRC Connector configurable parameters with hard-wired // default values (retained for compatability). internal string Server = null; internal string Password = null; internal string IrcChannel = null; internal string BaseNickname = "OSimBot"; internal uint Port = 6667; internal string User = null; internal bool ClientReporting = true; internal bool RelayChat = true; internal bool RelayPrivateChannels = false; internal int RelayChannel = 1; internal List<int> ValidInWorldChannels = new List<int>(); // Connector agnostic parameters. These values are NOT shared with the // connector and do not differentiate at an IRC level internal string PrivateMessageFormat = "PRIVMSG {0} :<{2}> {1} {3}"; internal string NoticeMessageFormat = "PRIVMSG {0} :<{2}> {3}"; internal int RelayChannelOut = -1; internal bool RandomizeNickname = true; internal bool CommandsEnabled = false; internal int CommandChannel = -1; internal int ConnectDelay = 10; internal int PingDelay = 15; internal string DefaultZone = "Sim"; internal string _accessPassword = String.Empty; internal Regex AccessPasswordRegex = null; internal List<string> ExcludeList = new List<string>(); internal string AccessPassword { get { return _accessPassword; } set { _accessPassword = value; AccessPasswordRegex = new Regex(String.Format(@"^{0},\s*(?<avatar>[^,]+),\s*(?<message>.+)$", _accessPassword), RegexOptions.Compiled); } } // IRC connector reference internal IRCConnector irc = null; internal int idn = _idk_++; // List of regions dependent upon this connection internal List<RegionState> clientregions = new List<RegionState>(); // Needed by OpenChannel internal ChannelState() { } // This constructor is used by the Update* methods. A copy of the // existing channel state is created, and distinguishing characteristics // are copied across. internal ChannelState(ChannelState model) { Server = model.Server; Password = model.Password; IrcChannel = model.IrcChannel; Port = model.Port; BaseNickname = model.BaseNickname; RandomizeNickname = model.RandomizeNickname; User = model.User; CommandsEnabled = model.CommandsEnabled; CommandChannel = model.CommandChannel; RelayChat = model.RelayChat; RelayPrivateChannels = model.RelayPrivateChannels; RelayChannelOut = model.RelayChannelOut; RelayChannel = model.RelayChannel; ValidInWorldChannels = model.ValidInWorldChannels; PrivateMessageFormat = model.PrivateMessageFormat; NoticeMessageFormat = model.NoticeMessageFormat; ClientReporting = model.ClientReporting; AccessPassword = model.AccessPassword; DefaultZone = model.DefaultZone; ConnectDelay = model.ConnectDelay; PingDelay = model.PingDelay; } // Read the configuration file, performing variable substitution and any // necessary aliasing. See accompanying documentation for how this works. // If you don't need variables, then this works exactly as before. // If either channel or server are not specified, the request fails. internal static void OpenChannel(RegionState rs, IConfig config) { // Create a new instance of a channel. This may not actually // get used if an equivalent channel already exists. ChannelState cs = new ChannelState(); // Read in the configuration file and filter everything for variable // subsititution. m_log.DebugFormat("[IRC-Channel-{0}] Initial request by Region {1} to connect to IRC", cs.idn, rs.Region); cs.Server = Substitute(rs, config.GetString("server", null)); m_log.DebugFormat("[IRC-Channel-{0}] Server : <{1}>", cs.idn, cs.Server); cs.Password = Substitute(rs, config.GetString("password", null)); // probably not a good idea to put a password in the log file cs.User = Substitute(rs, config.GetString("user", null)); cs.IrcChannel = Substitute(rs, config.GetString("channel", null)); m_log.DebugFormat("[IRC-Channel-{0}] IrcChannel : <{1}>", cs.idn, cs.IrcChannel); cs.Port = Convert.ToUInt32(Substitute(rs, config.GetString("port", Convert.ToString(cs.Port)))); m_log.DebugFormat("[IRC-Channel-{0}] Port : <{1}>", cs.idn, cs.Port); cs.BaseNickname = Substitute(rs, config.GetString("nick", cs.BaseNickname)); m_log.DebugFormat("[IRC-Channel-{0}] BaseNickname : <{1}>", cs.idn, cs.BaseNickname); cs.RandomizeNickname = Convert.ToBoolean(Substitute(rs, config.GetString("randomize_nick", Convert.ToString(cs.RandomizeNickname)))); m_log.DebugFormat("[IRC-Channel-{0}] RandomizeNickname : <{1}>", cs.idn, cs.RandomizeNickname); cs.RandomizeNickname = Convert.ToBoolean(Substitute(rs, config.GetString("nicknum", Convert.ToString(cs.RandomizeNickname)))); m_log.DebugFormat("[IRC-Channel-{0}] RandomizeNickname : <{1}>", cs.idn, cs.RandomizeNickname); cs.User = Substitute(rs, config.GetString("username", cs.User)); m_log.DebugFormat("[IRC-Channel-{0}] User : <{1}>", cs.idn, cs.User); cs.CommandsEnabled = Convert.ToBoolean(Substitute(rs, config.GetString("commands_enabled", Convert.ToString(cs.CommandsEnabled)))); m_log.DebugFormat("[IRC-Channel-{0}] CommandsEnabled : <{1}>", cs.idn, cs.CommandsEnabled); cs.CommandChannel = Convert.ToInt32(Substitute(rs, config.GetString("commandchannel", Convert.ToString(cs.CommandChannel)))); m_log.DebugFormat("[IRC-Channel-{0}] CommandChannel : <{1}>", cs.idn, cs.CommandChannel); cs.CommandChannel = Convert.ToInt32(Substitute(rs, config.GetString("command_channel", Convert.ToString(cs.CommandChannel)))); m_log.DebugFormat("[IRC-Channel-{0}] CommandChannel : <{1}>", cs.idn, cs.CommandChannel); cs.RelayChat = Convert.ToBoolean(Substitute(rs, config.GetString("relay_chat", Convert.ToString(cs.RelayChat)))); m_log.DebugFormat("[IRC-Channel-{0}] RelayChat : <{1}>", cs.idn, cs.RelayChat); cs.RelayPrivateChannels = Convert.ToBoolean(Substitute(rs, config.GetString("relay_private_channels", Convert.ToString(cs.RelayPrivateChannels)))); m_log.DebugFormat("[IRC-Channel-{0}] RelayPrivateChannels : <{1}>", cs.idn, cs.RelayPrivateChannels); cs.RelayPrivateChannels = Convert.ToBoolean(Substitute(rs, config.GetString("useworldcomm", Convert.ToString(cs.RelayPrivateChannels)))); m_log.DebugFormat("[IRC-Channel-{0}] RelayPrivateChannels : <{1}>", cs.idn, cs.RelayPrivateChannels); cs.RelayChannelOut = Convert.ToInt32(Substitute(rs, config.GetString("relay_private_channel_out", Convert.ToString(cs.RelayChannelOut)))); m_log.DebugFormat("[IRC-Channel-{0}] RelayChannelOut : <{1}>", cs.idn, cs.RelayChannelOut); cs.RelayChannel = Convert.ToInt32(Substitute(rs, config.GetString("relay_private_channel_in", Convert.ToString(cs.RelayChannel)))); m_log.DebugFormat("[IRC-Channel-{0}] RelayChannel : <{1}>", cs.idn, cs.RelayChannel); cs.RelayChannel = Convert.ToInt32(Substitute(rs, config.GetString("inchannel", Convert.ToString(cs.RelayChannel)))); m_log.DebugFormat("[IRC-Channel-{0}] RelayChannel : <{1}>", cs.idn, cs.RelayChannel); cs.PrivateMessageFormat = Substitute(rs, config.GetString("msgformat", cs.PrivateMessageFormat)); m_log.DebugFormat("[IRC-Channel-{0}] PrivateMessageFormat : <{1}>", cs.idn, cs.PrivateMessageFormat); cs.NoticeMessageFormat = Substitute(rs, config.GetString("noticeformat", cs.NoticeMessageFormat)); m_log.DebugFormat("[IRC-Channel-{0}] NoticeMessageFormat : <{1}>", cs.idn, cs.NoticeMessageFormat); cs.ClientReporting = Convert.ToInt32(Substitute(rs, config.GetString("verbosity", cs.ClientReporting?"1":"0"))) > 0; m_log.DebugFormat("[IRC-Channel-{0}] ClientReporting : <{1}>", cs.idn, cs.ClientReporting); cs.ClientReporting = Convert.ToBoolean(Substitute(rs, config.GetString("report_clients", Convert.ToString(cs.ClientReporting)))); m_log.DebugFormat("[IRC-Channel-{0}] ClientReporting : <{1}>", cs.idn, cs.ClientReporting); cs.DefaultZone = Substitute(rs, config.GetString("fallback_region", cs.DefaultZone)); m_log.DebugFormat("[IRC-Channel-{0}] DefaultZone : <{1}>", cs.idn, cs.DefaultZone); cs.ConnectDelay = Convert.ToInt32(Substitute(rs, config.GetString("connect_delay", Convert.ToString(cs.ConnectDelay)))); m_log.DebugFormat("[IRC-Channel-{0}] ConnectDelay : <{1}>", cs.idn, cs.ConnectDelay); cs.PingDelay = Convert.ToInt32(Substitute(rs, config.GetString("ping_delay", Convert.ToString(cs.PingDelay)))); m_log.DebugFormat("[IRC-Channel-{0}] PingDelay : <{1}>", cs.idn, cs.PingDelay); cs.AccessPassword = Substitute(rs, config.GetString("access_password", cs.AccessPassword)); m_log.DebugFormat("[IRC-Channel-{0}] AccessPassword : <{1}>", cs.idn, cs.AccessPassword); string[] excludes = config.GetString("exclude_list", "").Trim().Split(new Char[] { ',' }); cs.ExcludeList = new List<string>(excludes.Length); foreach (string name in excludes) { cs.ExcludeList.Add(name.Trim().ToLower()); } // Fail if fundamental information is still missing if (cs.Server == null || cs.IrcChannel == null || cs.BaseNickname == null || cs.User == null) throw new Exception(String.Format("[IRC-Channel-{0}] Invalid configuration for region {1}", cs.idn, rs.Region)); m_log.InfoFormat("[IRC-Channel-{0}] Configuration for Region {1} is valid", cs.idn, rs.Region); m_log.InfoFormat("[IRC-Channel-{0}] Server = {1}", cs.idn, cs.Server); m_log.InfoFormat("[IRC-Channel-{0}] Channel = {1}", cs.idn, cs.IrcChannel); m_log.InfoFormat("[IRC-Channel-{0}] Port = {1}", cs.idn, cs.Port); m_log.InfoFormat("[IRC-Channel-{0}] Nickname = {1}", cs.idn, cs.BaseNickname); m_log.InfoFormat("[IRC-Channel-{0}] User = {1}", cs.idn, cs.User); // Set the channel state for this region if (cs.RelayChat) { cs.ValidInWorldChannels.Add(0); cs.ValidInWorldChannels.Add(DEBUG_CHANNEL); } if (cs.RelayPrivateChannels) cs.ValidInWorldChannels.Add(cs.RelayChannelOut); rs.cs = Integrate(rs, cs); } // An initialized channel state instance is passed in. If an identical // channel state instance already exists, then the existing instance // is used to replace the supplied value. // If the instance matches with respect to IRC, then the underlying // IRCConnector is assigned to the supplied channel state and the // updated value is returned. // If there is no match, then the supplied instance is completed by // creating and assigning an instance of an IRC connector. private static ChannelState Integrate(RegionState rs, ChannelState p_cs) { ChannelState cs = p_cs; // Check to see if we have an existing server/channel setup that can be used // In the absence of variable substitution this will always resolve to the // same ChannelState instance, and the table will only contains a single // entry, so the performance considerations for the existing behavior are // zero. Only the IRC connector is shared, the ChannelState still contains // values that, while independent of the IRC connetion, do still distinguish // this region's behavior. lock (IRCBridgeModule.m_channels) { foreach (ChannelState xcs in IRCBridgeModule.m_channels) { if (cs.IsAPerfectMatchFor(xcs)) { m_log.DebugFormat("[IRC-Channel-{0}] Channel state matched", cs.idn); cs = xcs; break; } if (cs.IsAConnectionMatchFor(xcs)) { m_log.DebugFormat("[IRC-Channel-{0}] Channel matched", cs.idn); cs.irc = xcs.irc; break; } } } // No entry was found, so this is going to be a new entry. if (cs.irc == null) { m_log.DebugFormat("[IRC-Channel-{0}] New channel required", cs.idn); if ((cs.irc = new IRCConnector(cs)) != null) { IRCBridgeModule.m_channels.Add(cs); m_log.InfoFormat("[IRC-Channel-{0}] New channel initialized for {1}, nick: {2}, commands {3}, private channels {4}", cs.idn, rs.Region, cs.DefaultZone, cs.CommandsEnabled ? "enabled" : "not enabled", cs.RelayPrivateChannels ? "relayed" : "not relayed"); } else { string txt = String.Format("[IRC-Channel-{0}] Region {1} failed to connect to channel {2} on server {3}:{4}", cs.idn, rs.Region, cs.IrcChannel, cs.Server, cs.Port); m_log.Error(txt); throw new Exception(txt); } } else { m_log.InfoFormat("[IRC-Channel-{0}] Region {1} reusing existing connection to channel {2} on server {3}:{4}", cs.idn, rs.Region, cs.IrcChannel, cs.Server, cs.Port); } m_log.InfoFormat("[IRC-Channel-{0}] Region {1} associated with channel {2} on server {3}:{4}", cs.idn, rs.Region, cs.IrcChannel, cs.Server, cs.Port); // We're finally ready to commit ourselves return cs; } // These routines allow differentiating changes to // the underlying channel state. If necessary, a // new channel state will be created. internal ChannelState UpdateServer(RegionState rs, string server) { RemoveRegion(rs); ChannelState cs = new ChannelState(this); cs.Server = server; cs = Integrate(rs, cs); cs.AddRegion(rs); return cs; } internal ChannelState UpdatePort(RegionState rs, string port) { RemoveRegion(rs); ChannelState cs = new ChannelState(this); cs.Port = Convert.ToUInt32(port); cs = Integrate(rs, cs); cs.AddRegion(rs); return cs; } internal ChannelState UpdateChannel(RegionState rs, string channel) { RemoveRegion(rs); ChannelState cs = new ChannelState(this); cs.IrcChannel = channel; cs = Integrate(rs, cs); cs.AddRegion(rs); return cs; } internal ChannelState UpdateNickname(RegionState rs, string nickname) { RemoveRegion(rs); ChannelState cs = new ChannelState(this); cs.BaseNickname = nickname; cs = Integrate(rs, cs); cs.AddRegion(rs); return cs; } internal ChannelState UpdateClientReporting(RegionState rs, string cr) { RemoveRegion(rs); ChannelState cs = new ChannelState(this); cs.ClientReporting = Convert.ToBoolean(cr); cs = Integrate(rs, cs); cs.AddRegion(rs); return cs; } internal ChannelState UpdateRelayIn(RegionState rs, string channel) { RemoveRegion(rs); ChannelState cs = new ChannelState(this); cs.RelayChannel = Convert.ToInt32(channel); cs = Integrate(rs, cs); cs.AddRegion(rs); return cs; } internal ChannelState UpdateRelayOut(RegionState rs, string channel) { RemoveRegion(rs); ChannelState cs = new ChannelState(this); cs.RelayChannelOut = Convert.ToInt32(channel); cs = Integrate(rs, cs); cs.AddRegion(rs); return cs; } // Determine whether or not this is a 'new' channel. Only those // attributes that uniquely distinguish an IRC connection should // be included here (and only those attributes should really be // in the ChannelState structure) private bool IsAConnectionMatchFor(ChannelState cs) { return ( Server == cs.Server && IrcChannel == cs.IrcChannel && Port == cs.Port && BaseNickname == cs.BaseNickname && User == cs.User ); } // This level of obsessive matching allows us to produce // a minimal overhead int he case of a server which does // need to differentiate IRC at a region level. private bool IsAPerfectMatchFor(ChannelState cs) { return (IsAConnectionMatchFor(cs) && RelayChannelOut == cs.RelayChannelOut && PrivateMessageFormat == cs.PrivateMessageFormat && NoticeMessageFormat == cs.NoticeMessageFormat && RandomizeNickname == cs.RandomizeNickname && AccessPassword == cs.AccessPassword && CommandsEnabled == cs.CommandsEnabled && CommandChannel == cs.CommandChannel && DefaultZone == cs.DefaultZone && RelayPrivateChannels == cs.RelayPrivateChannels && RelayChannel == cs.RelayChannel && RelayChat == cs.RelayChat && ClientReporting == cs.ClientReporting ); } // This function implements the variable substitution mechanism // for the configuration values. Each string read from the // configuration file is scanned for '[...]' enclosures. Each // one that is found is replaced by either a runtime variable // (%xxx) or an existing configuration key. When no further // substitution is possible, the remaining string is returned // to the caller. This allows for arbitrarily nested // enclosures. private static string Substitute(RegionState rs, string instr) { string result = instr; if (result == null || result.Length == 0) return result; // Repeatedly scan the string until all possible // substitutions have been performed. // m_log.DebugFormat("[IRC-Channel] Parse[1]: {0}", result); while (arg.IsMatch(result)) { string vvar = arg.Match(result).ToString(); string var = vvar.Substring(1,vvar.Length-2).Trim(); switch (var.ToLower()) { case "%region" : result = result.Replace(vvar, rs.Region); break; case "%host" : result = result.Replace(vvar, rs.Host); break; case "%master1" : result = result.Replace(vvar, rs.MA1); break; case "%master2" : result = result.Replace(vvar, rs.MA2); break; case "%locx" : result = result.Replace(vvar, rs.LocX); break; case "%locy" : result = result.Replace(vvar, rs.LocY); break; case "%k" : result = result.Replace(vvar, rs.IDK); break; default : result = result.Replace(vvar, rs.config.GetString(var,var)); break; } // m_log.DebugFormat("[IRC-Channel] Parse[2]: {0}", result); } // m_log.DebugFormat("[IRC-Channel] Parse[3]: {0}", result); return result; } public void Close() { m_log.InfoFormat("[IRC-Channel-{0}] Closing channel <{1}> to server <{2}:{3}>", idn, IrcChannel, Server, Port); m_log.InfoFormat("[IRC-Channel-{0}] There are {1} active clients", idn, clientregions.Count); irc.Close(); } public void Open() { m_log.InfoFormat("[IRC-Channel-{0}] Opening channel <{1}> to server <{2}:{3}>", idn, IrcChannel, Server, Port); irc.Open(); } // These are called by each region that attaches to this channel. The call affects // only the relationship of the region with the channel. Not the channel to IRC // relationship (unless it is closed and we want it open). public void Open(RegionState rs) { AddRegion(rs); Open(); } // Close is called to ensure that the IRC session is terminated if this is the // only client. public void Close(RegionState rs) { RemoveRegion(rs); lock (IRCBridgeModule.m_channels) { if (clientregions.Count == 0) { Close(); IRCBridgeModule.m_channels.Remove(this); m_log.InfoFormat("[IRC-Channel-{0}] Region {1} is last user of channel <{2}> to server <{3}:{4}>", idn, rs.Region, IrcChannel, Server, Port); m_log.InfoFormat("[IRC-Channel-{0}] Removed", idn); } } } // Add a client region to this channel if it is not already known public void AddRegion(RegionState rs) { m_log.InfoFormat("[IRC-Channel-{0}] Adding region {1} to channel <{2}> to server <{3}:{4}>", idn, rs.Region, IrcChannel, Server, Port); if (!clientregions.Contains(rs)) { clientregions.Add(rs); lock (irc) irc.depends++; } } // Remove a client region from the channel. If this is the last // region, then clean up the channel. The connector will clean itself // up if it finds itself about to be GC'd. public void RemoveRegion(RegionState rs) { m_log.InfoFormat("[IRC-Channel-{0}] Removing region {1} from channel <{2} to server <{3}:{4}>", idn, rs.Region, IrcChannel, Server, Port); if (clientregions.Contains(rs)) { clientregions.Remove(rs); lock (irc) irc.depends--; } } // This function is lifted from the IRCConnector because it // contains information that is not differentiating from an // IRC point-of-view. public static void OSChat(IRCConnector p_irc, OSChatMessage c, bool cmsg) { // m_log.DebugFormat("[IRC-OSCHAT] from {0}:{1}", p_irc.Server, p_irc.IrcChannel); try { // Scan through the set of unique channel configuration for those // that belong to this connector. And then forward the message to // all regions known to those channels. // Note that this code is responsible for completing some of the // settings for the inbound OSChatMessage lock (IRCBridgeModule.m_channels) { foreach (ChannelState cs in IRCBridgeModule.m_channels) { if (p_irc == cs.irc) { // This non-IRC differentiator moved to here if (cmsg && !cs.ClientReporting) continue; // This non-IRC differentiator moved to here c.Channel = (cs.RelayPrivateChannels ? cs.RelayChannel : 0); foreach (RegionState region in cs.clientregions) { region.OSChat(cs.irc, c); } } } } } catch (Exception ex) { m_log.ErrorFormat("[IRC-OSCHAT]: BroadcastSim Exception: {0}", ex.Message); m_log.Debug(ex); } } } }