/* * 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 OpenSim 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.IO; using System.Net; using System.Web; using System.Text; using System.Xml; using System.Collections; using System.Collections.Generic; using System.Reflection; using OpenMetaverse; using log4net; using Nini.Config; using Nwc.XmlRpc; using OpenSim.Framework; using OpenSim.Framework.Communications.Cache; using OpenSim.Framework.Communications.Capabilities; using OpenSim.Framework.Servers; using OpenSim.Region.Framework.Interfaces; using OpenSim.Region.Framework.Scenes; using Caps = OpenSim.Framework.Communications.Capabilities.Caps; using System.Text.RegularExpressions; namespace OpenSim.Region.OptionalModules.Avatar.Voice.FreeSwitchVoice { public class FreeSwitchVoiceModule : IRegionModule { // Infrastructure private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); // Capability string prefixes private static readonly string m_parcelVoiceInfoRequestPath = "0007/"; private static readonly string m_provisionVoiceAccountRequestPath = "0008/"; private static readonly string m_chatSessionRequestPath = "0009/"; // Control info private static bool m_WOF = true; private static bool m_pluginEnabled = false; // FreeSwitch server is going to contact us and ask us all // sorts of things. private static string m_freeSwitchServerUser; private static string m_freeSwitchServerPass; // SLVoice client will do a GET on this prefix private static string m_freeSwitchAPIPrefix; // We need to return some information to SLVoice // figured those out via curl // http://vd1.vivox.com/api2/viv_get_prelogin.php // // need to figure out whether we do need to return ALL of // these... private static string m_freeSwitchRealm; private static string m_freeSwitchSIPProxy; private static bool m_freeSwitchAttemptUseSTUN; private static string m_freeSwitchSTUNServer; private static string m_freeSwitchEchoServer; private static int m_freeSwitchEchoPort; private static string m_freeSwitchDefaultWellKnownIP; private static int m_freeSwitchDefaultTimeout; // private static int m_freeSwitchSubscribeRetry; private static string m_freeSwitchUrlResetPassword; // private static IPEndPoint m_FreeSwitchServiceIP; private int m_freeSwitchServicePort; private string m_openSimWellKnownHTTPAddress; private FreeSwitchDirectory m_FreeSwitchDirectory; private FreeSwitchDialplan m_FreeSwitchDialplan; private IConfig m_config; public void Initialise(Scene scene, IConfigSource config) { m_config = config.Configs["FreeSwitchVoice"]; if (null == m_config) { m_log.Info("[FreeSwitchVoice] no config found, plugin disabled"); return; } if (!m_config.GetBoolean("enabled", false)) { m_log.Info("[FreeSwitchVoice] plugin disabled by configuration"); return; } // This is only done the FIRST time this method is invoked. if (m_WOF) { m_pluginEnabled = true; m_WOF = false; try { m_freeSwitchServerUser = m_config.GetString("freeswitch_server_user", String.Empty); m_freeSwitchServerPass = m_config.GetString("freeswitch_server_pass", String.Empty); m_freeSwitchAPIPrefix = m_config.GetString("freeswitch_api_prefix", String.Empty); // XXX: get IP address of HTTP server. (This can be this OpenSim server or another, or could be a dedicated grid service or may live on the freeswitch server) string serviceIP = m_config.GetString("freeswitch_service_server", String.Empty); int servicePort = m_config.GetInt("freeswitch_service_port", 80); IPAddress serviceIPAddress = IPAddress.Parse(serviceIP); // m_FreeSwitchServiceIP = new IPEndPoint(serviceIPAddress, servicePort); m_freeSwitchServicePort = servicePort; m_freeSwitchRealm = m_config.GetString("freeswitch_realm", String.Empty); m_freeSwitchSIPProxy = m_config.GetString("freeswitch_sip_proxy", m_freeSwitchRealm); m_freeSwitchAttemptUseSTUN = m_config.GetBoolean("freeswitch_attempt_stun", true); m_freeSwitchSTUNServer = m_config.GetString("freeswitch_stun_server", m_freeSwitchRealm); m_freeSwitchEchoServer = m_config.GetString("freeswitch_echo_server", m_freeSwitchRealm); m_freeSwitchEchoPort = m_config.GetInt("freeswitch_echo_port", 50505); m_freeSwitchDefaultWellKnownIP = m_config.GetString("freeswitch_well_known_ip", m_freeSwitchRealm); m_openSimWellKnownHTTPAddress = m_config.GetString("opensim_well_known_http_address", serviceIPAddress.ToString()); m_freeSwitchDefaultTimeout = m_config.GetInt("freeswitch_default_timeout", 5000); // m_freeSwitchSubscribeRetry = m_config.GetInt("freeswitch_subscribe_retry", 120); m_freeSwitchUrlResetPassword = m_config.GetString("freeswitch_password_reset_url", String.Empty); if (String.IsNullOrEmpty(m_freeSwitchServerUser) || String.IsNullOrEmpty(m_freeSwitchServerPass) || String.IsNullOrEmpty(m_freeSwitchRealm) || String.IsNullOrEmpty(m_freeSwitchAPIPrefix)) { m_log.Error("[FreeSwitchVoice] plugin mis-configured"); m_log.Info("[FreeSwitchVoice] plugin disabled: incomplete configuration"); return; } // set up http request handlers for // - prelogin: viv_get_prelogin.php // - signin: viv_signin.php scene.CommsManager.HttpServer.AddHTTPHandler(String.Format("{0}/viv_get_prelogin.php", m_freeSwitchAPIPrefix), FreeSwitchSLVoiceGetPreloginHTTPHandler); // RestStreamHandler h = new // RestStreamHandler("GET", // String.Format("{0}/viv_get_prelogin.php", m_freeSwitchAPIPrefix), FreeSwitchSLVoiceGetPreloginHTTPHandler); // scene.CommsManager.HttpServer.AddStreamHandler(h); scene.CommsManager.HttpServer.AddHTTPHandler(String.Format("{0}/viv_signin.php", m_freeSwitchAPIPrefix), FreeSwitchSLVoiceSigninHTTPHandler); // set up http request handlers to provide // on-demand FreeSwitch configuration to // FreeSwitch's mod_curl_xml scene.CommsManager.HttpServer.AddHTTPHandler(String.Format("{0}/freeswitch-config", m_freeSwitchAPIPrefix), FreeSwitchConfigHTTPHandler); m_log.InfoFormat("[FreeSwitchVoice] using FreeSwitch server {0}", m_freeSwitchRealm); m_FreeSwitchDirectory = new FreeSwitchDirectory(); m_FreeSwitchDialplan = new FreeSwitchDialplan(); m_pluginEnabled = true; m_WOF = false; m_log.Info("[FreeSwitchVoice] plugin enabled"); } catch (Exception e) { m_log.ErrorFormat("[FreeSwitchVoice] plugin initialization failed: {0}", e.Message); m_log.DebugFormat("[FreeSwitchVoice] plugin initialization failed: {0}", e.ToString()); return; } } if (m_pluginEnabled) { // we need to capture scene in an anonymous method // here as we need it later in the callbacks scene.EventManager.OnRegisterCaps += delegate(UUID agentID, Caps caps) { OnRegisterCaps(scene, agentID, caps); }; } } public void PostInitialise() { } public void Close() { } public string Name { get { return "FreeSwitchVoiceModule"; } } public bool IsSharedModule { get { return true; } } // // OnRegisterCaps is invoked via the scene.EventManager // everytime OpenSim hands out capabilities to a client // (login, region crossing). We contribute two capabilities to // the set of capabilities handed back to the client: // ProvisionVoiceAccountRequest and ParcelVoiceInfoRequest. // // ProvisionVoiceAccountRequest allows the client to obtain // the voice account credentials for the avatar it is // controlling (e.g., user name, password, etc). // // ParcelVoiceInfoRequest is invoked whenever the client // changes from one region or parcel to another. // // Note that OnRegisterCaps is called here via a closure // delegate containing the scene of the respective region (see // Initialise()). // public void OnRegisterCaps(Scene scene, UUID agentID, Caps caps) { m_log.DebugFormat("[FreeSwitchVoice] OnRegisterCaps: agentID {0} caps {1}", agentID, caps); string capsBase = "/CAPS/" + caps.CapsObjectPath; caps.RegisterHandler("ProvisionVoiceAccountRequest", new RestStreamHandler("POST", capsBase + m_provisionVoiceAccountRequestPath, delegate(string request, string path, string param, OSHttpRequest httpRequest, OSHttpResponse httpResponse) { return ProvisionVoiceAccountRequest(scene, request, path, param, agentID, caps); })); caps.RegisterHandler("ParcelVoiceInfoRequest", new RestStreamHandler("POST", capsBase + m_parcelVoiceInfoRequestPath, delegate(string request, string path, string param, OSHttpRequest httpRequest, OSHttpResponse httpResponse) { return ParcelVoiceInfoRequest(scene, request, path, param, agentID, caps); })); caps.RegisterHandler("ChatSessionRequest", new RestStreamHandler("POST", capsBase + m_chatSessionRequestPath, delegate(string request, string path, string param, OSHttpRequest httpRequest, OSHttpResponse httpResponse) { return ChatSessionRequest(scene, request, path, param, agentID, caps); })); } /// /// Callback for a client request for Voice Account Details /// /// current scene object of the client /// /// /// /// /// /// public string ProvisionVoiceAccountRequest(Scene scene, string request, string path, string param, UUID agentID, Caps caps) { ScenePresence avatar = scene.GetScenePresence(agentID); string avatarName = avatar.Name; try { m_log.DebugFormat("[FreeSwitchVoice][PROVISIONVOICE]: request: {0}, path: {1}, param: {2}", request, path, param); //XmlElement resp; string agentname = "x" + Convert.ToBase64String(agentID.GetBytes()); string password = "1234";//temp hack//new UUID(Guid.NewGuid()).ToString().Replace('-','Z').Substring(0,16); // XXX: we need to cache the voice credentials, as // FreeSwitch is later going to come and ask us for // those agentname = agentname.Replace('+', '-').Replace('/', '_'); // LLSDVoiceAccountResponse voiceAccountResponse = // new LLSDVoiceAccountResponse(agentname, password, m_freeSwitchRealm, "http://etsvc02.hursley.ibm.com/api"); LLSDVoiceAccountResponse voiceAccountResponse = new LLSDVoiceAccountResponse(agentname, password, m_freeSwitchRealm, String.Format("http://{0}:{1}{2}/", m_openSimWellKnownHTTPAddress, m_freeSwitchServicePort, m_freeSwitchAPIPrefix)); string r = LLSDHelpers.SerialiseLLSDReply(voiceAccountResponse); m_log.DebugFormat("[FreeSwitchVoice][PROVISIONVOICE]: avatar \"{0}\": {1}", avatarName, r); return r; } catch (Exception e) { m_log.ErrorFormat("[FreeSwitchVoice][PROVISIONVOICE]: avatar \"{0}\": {1}, retry later", avatarName, e.Message); m_log.DebugFormat("[FreeSwitchVoice][PROVISIONVOICE]: avatar \"{0}\": {1} failed", avatarName, e.ToString()); return "undef"; } } /// /// Callback for a client request for ParcelVoiceInfo /// /// current scene object of the client /// /// /// /// /// /// public string ParcelVoiceInfoRequest(Scene scene, string request, string path, string param, UUID agentID, Caps caps) { ScenePresence avatar = scene.GetScenePresence(agentID); string avatarName = avatar.Name; // - check whether we have a region channel in our cache // - if not: // create it and cache it // - send it to the client // - send channel_uri: as "sip:regionID@m_sipDomain" try { LLSDParcelVoiceInfoResponse parcelVoiceInfo; string channelUri; if (null == scene.LandChannel) throw new Exception(String.Format("region \"{0}\": avatar \"{1}\": land data not yet available", scene.RegionInfo.RegionName, avatarName)); // get channel_uri: check first whether estate // settings allow voice, then whether parcel allows // voice, if all do retrieve or obtain the parcel // voice channel LandData land = scene.GetLandData(avatar.AbsolutePosition.X, avatar.AbsolutePosition.Y); m_log.DebugFormat("[FreeSwitchVoice][PARCELVOICE]: region \"{0}\": Parcel \"{1}\" ({2}): avatar \"{3}\": request: {4}, path: {5}, param: {6}", scene.RegionInfo.RegionName, land.Name, land.LocalID, avatarName, request, path, param); // TODO: EstateSettings don't seem to get propagated... // if (!scene.RegionInfo.EstateSettings.AllowVoice) // { // m_log.DebugFormat("[FreeSwitchVoice][PARCELVOICE]: region \"{0}\": voice not enabled in estate settings", // scene.RegionInfo.RegionName); // channel_uri = String.Empty; // } // else if ((land.Flags & (uint)Parcel.ParcelFlags.AllowVoiceChat) == 0) { m_log.DebugFormat("[FreeSwitchVoice][PARCELVOICE]: region \"{0}\": Parcel \"{1}\" ({2}): avatar \"{3}\": voice not enabled for parcel", scene.RegionInfo.RegionName, land.Name, land.LocalID, avatarName); channelUri = String.Empty; } else { channelUri = ChannelUri(scene, land); } // fill in our response to the client Hashtable creds = new Hashtable(); creds["channel_uri"] = channelUri; parcelVoiceInfo = new LLSDParcelVoiceInfoResponse(scene.RegionInfo.RegionName, land.LocalID, creds); string r = LLSDHelpers.SerialiseLLSDReply(parcelVoiceInfo); m_log.DebugFormat("[FreeSwitchVoice][PARCELVOICE]: region \"{0}\": Parcel \"{1}\" ({2}): avatar \"{3}\": {4}", scene.RegionInfo.RegionName, land.Name, land.LocalID, avatarName, r); return r; } catch (Exception e) { m_log.ErrorFormat("[FreeSwitchVoice][PARCELVOICE]: region \"{0}\": avatar \"{1}\": {2}, retry later", scene.RegionInfo.RegionName, avatarName, e.Message); m_log.DebugFormat("[FreeSwitchVoice][PARCELVOICE]: region \"{0}\": avatar \"{1}\": {2} failed", scene.RegionInfo.RegionName, avatarName, e.ToString()); return "undef"; } } /// /// Callback for a client request for ChatSessionRequest /// /// current scene object of the client /// /// /// /// /// /// public string ChatSessionRequest(Scene scene, string request, string path, string param, UUID agentID, Caps caps) { ScenePresence avatar = scene.GetScenePresence(agentID); string avatarName = avatar.Name; m_log.DebugFormat("[FreeSwitchVoice][CHATSESSION]: avatar \"{0}\": request: {1}, path: {2}, param: {3}", avatarName, request, path, param); return "true"; } public Hashtable FreeSwitchSLVoiceGetPreloginHTTPHandler(Hashtable request) { m_log.Debug("[FreeSwitchVoice] FreeSwitchSLVoiceGetPreloginHTTPHandler called"); Hashtable response = new Hashtable(); response["content_type"] = "text/xml"; response["keepalive"] = false; response["str_response_string"] = String.Format( "\r\n" + "\r\n"+ "{0}\r\n" + "{1}\r\n"+ "{2}\r\n"+ "{3}\r\n"+ "{4}\r\n"+ "{5}\r\n"+ "{6}\r\n"+ "{7}\r\n"+ "{8}\r\n"+ "\r\n"+ "false\r\n"+ "", m_freeSwitchRealm, m_freeSwitchSIPProxy, m_freeSwitchAttemptUseSTUN, m_freeSwitchSTUNServer, m_freeSwitchEchoServer, m_freeSwitchEchoPort, m_freeSwitchDefaultWellKnownIP, m_freeSwitchDefaultTimeout, m_freeSwitchUrlResetPassword, ""); response["int_response_code"] = 200; m_log.DebugFormat("[FreeSwitchVoice] FreeSwitchSLVoiceGetPreloginHTTPHandler return {0}",response["str_response_string"]); return response; } public Hashtable FreeSwitchSLVoiceSigninHTTPHandler(Hashtable request) { m_log.Debug("[FreeSwitchVoice] FreeSwitchSLVoiceSigninHTTPHandler called"); Hashtable response = new Hashtable(); response["str_response_string"] = @" OK 200 auth successful "; response["int_response_code"] = 200; return response; } public Hashtable FreeSwitchConfigHTTPHandler(Hashtable request) { m_log.DebugFormat("[FreeSwitchVoice] FreeSwitchConfigHTTPHandler called with {0}", (string)request["body"]); Hashtable response = new Hashtable(); // all the params come as NVPs in the request body Hashtable requestBody = parseRequestBody((string) request["body"]); // is this a dialplan or directory request string section = (string) requestBody["section"]; if (section == "directory") response = m_FreeSwitchDirectory.HandleDirectoryRequest(requestBody); else if (section == "dialplan") response = m_FreeSwitchDialplan.HandleDialplanRequest(requestBody); else m_log.WarnFormat("[FreeSwitchVoice]: section was {0}", section); // XXX: re-generate dialplan: // - conf == region UUID // - conf number = region port // -> TODO Initialise(): keep track of regions via events // re-generate accounts for all avatars // -> TODO Initialise(): keep track of avatars via events Regex normalizeEndLines = new Regex(@"\r\n", RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.Multiline); m_log.DebugFormat("[FreeSwitchVoice] FreeSwitchConfigHTTPHandler return {0}",normalizeEndLines.Replace(((string)response["str_response_string"]), "")); return response; } public Hashtable parseRequestBody(string body) { Hashtable bodyParams = new Hashtable(); // split string string [] nvps = body.Split(new Char [] {'&'}); foreach (string s in nvps) { if (s.Trim() != "") { string [] nvp = s.Split(new Char [] {'='}); bodyParams.Add(HttpUtility.UrlDecode(nvp[0]), HttpUtility.UrlDecode(nvp[1])); } } return bodyParams; } private string ChannelUri(Scene scene, LandData land) { string channelUri = null; string landUUID; string landName; // Create parcel voice channel. If no parcel exists, then the voice channel ID is the same // as the directory ID. Otherwise, it reflects the parcel's ID. if (land.LocalID != 1 && (land.Flags & (uint)Parcel.ParcelFlags.UseEstateVoiceChan) == 0) { landName = String.Format("{0}:{1}", scene.RegionInfo.RegionName, land.Name); landUUID = land.GlobalID.ToString(); m_log.DebugFormat("[FreeSwitchVoice]: Region:Parcel \"{0}\": parcel id {1}: using channel name {2}", landName, land.LocalID, landUUID); } else { landName = String.Format("{0}:{1}", scene.RegionInfo.RegionName, scene.RegionInfo.RegionName); landUUID = scene.RegionInfo.RegionID.ToString(); m_log.DebugFormat("[FreeSwitchVoice]: Region:Parcel \"{0}\": parcel id {1}: using channel name {2}", landName, land.LocalID, landUUID); } System.Text.ASCIIEncoding encoding = new System.Text.ASCIIEncoding(); channelUri = String.Format("sip:confctl-{0}@{1}", "x" + Convert.ToBase64String(encoding.GetBytes(landUUID)), m_freeSwitchRealm); //channelUri="sip:confctl-3001@9.20.151.43"; //channelUri="sip:opensimconf-3001@9.20.151.43"; return channelUri; } } }