/*
* 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;
namespace OpenSim.Region.OptionalModules.Avatar.Voice.FreeSwitchVoice
{
public class FreeSwitchVoiceModule : IRegionModule
{
// Infrastructure
private static readonly ILog m_log =
LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
private static readonly bool DUMP = true;
// 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
m_log.DebugFormat("[FreeSwitchVoice] FreeSwitchConfigHTTPHandler return {0}",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;
}
}
}