/*
* 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.IO;
using System.Net;
using System.Text;
using System.Collections.Generic;
using System.Reflection;
using Mono.Addins;
using OpenSim.Framework;
using OpenSim.Framework.Servers.HttpServer;
using OpenSim.Region.Framework.Interfaces;
using OpenSim.Region.Framework.Scenes;
using Caps = OpenSim.Framework.Capabilities.Caps;
using OpenMetaverse;
using OpenMetaverse.StructuredData;
using OSDMap = OpenMetaverse.StructuredData.OSDMap;
using log4net;
using Nini.Config;
[assembly: Addin("WebRtcVoiceRegionModule", "1.0")]
[assembly: AddinDependency("OpenSim.Region.Framework", OpenSim.VersionInfo.VersionNumber)]
namespace WebRtcVoice
{
///
/// This module provides the WebRTC voice interface for viewer clients..
///
/// In particular, it provides the following capabilities:
/// ProvisionVoiceAccountRequest, VoiceSignalingRequest, and ParcelVoiceInfoRequest.
/// which are the user interface to the voice service.
///
/// Initially, when the user connects to the region, the region feature "VoiceServiceType" is
/// set to "webrtc" and the capabilities that support voice are enabled.
/// The capabilities then pass the user request information to the IWebRtcVoiceService interface
/// that has been registered for the reqion.
///
[Extension(Path = "/OpenSim/RegionModules", NodeName = "RegionModule", Id = "RegionVoiceModule")]
public class WebRtcVoiceRegionModule : ISharedRegionModule
{
private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
private static readonly string logHeader = "[REGION WEBRTC VOICE]";
private bool _MessageDetails = false;
// Control info
private static bool m_Enabled = false;
private readonly Dictionary m_UUIDName = new Dictionary();
private Dictionary m_ParcelAddress = new Dictionary();
private IConfig m_Config;
// ISharedRegionModule.Initialize
public void Initialise(IConfigSource config)
{
m_Config = config.Configs["WebRtcVoice"];
if (m_Config is not null)
{
m_Enabled = m_Config.GetBoolean("Enabled", false);
if (m_Enabled)
{
_MessageDetails = m_Config.GetBoolean("MessageDetails", false);
m_log.Info($"{logHeader}: enabled");
}
}
}
// ISharedRegionModule.PostInitialize
public void PostInitialise()
{
}
// ISharedRegionModule.AddRegion
public void AddRegion(Scene scene)
{
if (m_Enabled)
{
// Get the hook that means Capbibilities are being registered
scene.EventManager.OnRegisterCaps += (UUID agentID, Caps caps) =>
{
OnRegisterCaps(scene, agentID, caps);
};
}
}
// ISharedRegionModule.RemoveRegion
public void RemoveRegion(Scene scene)
{
var sfm = scene.RequestModuleInterface();
sfm.OnSimulatorFeaturesRequest -= OnSimulatorFeatureRequestHandler;
}
// ISharedRegionModule.RegionLoaded
public void RegionLoaded(Scene scene)
{
if (m_Enabled)
{
// Register for the region feature reporting so we can add 'webrtc'
var sfm = scene.RequestModuleInterface();
sfm.OnSimulatorFeaturesRequest += OnSimulatorFeatureRequestHandler;
m_log.DebugFormat("{0}: registering OnSimulatorFeatureRequestHandler", logHeader);
}
}
// ISharedRegionModule.Close
public void Close()
{
}
// ISharedRegionModule.Name
public string Name
{
get { return "RegionVoiceModule"; }
}
// ISharedRegionModule.ReplaceableInterface
public Type ReplaceableInterface
{
get { return null; }
}
// Called when the simulator features are being constructed.
// Add the flag that says we support WebRtc voice.
private void OnSimulatorFeatureRequestHandler(UUID agentID, ref OSDMap features)
{
m_log.DebugFormat("{0}: setting VoiceServerType=webrtc for agent {1}", logHeader, agentID);
features["VoiceServerType"] = "webrtc";
}
//
// OnRegisterCaps is invoked via the scene.EventManager
// everytime OpenSim hands out capabilities to a client
// (login, region crossing). We contribute three capabilities to
// the set of capabilities handed back to the client:
// ProvisionVoiceAccountRequest, VoiceSignalingRequest, and ParcelVoiceInfoRequest.
//
// ProvisionVoiceAccountRequest allows the client to obtain
// voice communication information the the avater.
//
// VoiceSignalingRequest: Used for trickling ICE candidates.
//
// 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(
"{0}: OnRegisterCaps() called with agentID {1} caps {2} in scene {3}",
logHeader, agentID, caps, scene.RegionInfo.RegionName);
caps.RegisterSimpleHandler("ProvisionVoiceAccountRequest",
new SimpleStreamHandler("/" + UUID.Random(), (IOSHttpRequest httpRequest, IOSHttpResponse httpResponse) =>
{
ProvisionVoiceAccountRequest(httpRequest, httpResponse, agentID, scene);
}));
caps.RegisterSimpleHandler("VoiceSignalingRequest",
new SimpleStreamHandler("/" + UUID.Random(), (IOSHttpRequest httpRequest, IOSHttpResponse httpResponse) =>
{
VoiceSignalingRequest(httpRequest, httpResponse, agentID, scene);
}));
caps.RegisterSimpleHandler("ParcelVoiceInfoRequest",
new SimpleStreamHandler("/" + UUID.Random(), (IOSHttpRequest httpRequest, IOSHttpResponse httpResponse) =>
{
ParcelVoiceInfoRequest(httpRequest, httpResponse, agentID, scene);
}));
caps.RegisterSimpleHandler("ChatSessionRequest",
new SimpleStreamHandler("/" + UUID.Random(), (IOSHttpRequest httpRequest, IOSHttpResponse httpResponse) =>
{
ChatSessionRequest(httpRequest, httpResponse, agentID, scene);
}));
}
///
/// Callback for a client request for Voice Account Details
///
/// current scene object of the client
///
///
///
///
///
///
public void ProvisionVoiceAccountRequest(IOSHttpRequest request, IOSHttpResponse response, UUID agentID, Scene scene)
{
if (request.HttpMethod != "POST")
{
m_log.DebugFormat("[{0}][ProvisionVoice]: Not a POST request. Agent={1}", logHeader, agentID.ToString());
response.StatusCode = (int)HttpStatusCode.NotFound;
return;
}
// Deserialize the request. Convert the LLSDXml to OSD for our use
OSDMap map = BodyToMap(request, "[ProvisionVoiceAccountRequest]");
if (map is null)
{
m_log.ErrorFormat("{0}[ProvisionVoice]: No request data found. Agent={1}", logHeader, agentID.ToString());
response.StatusCode = (int)HttpStatusCode.NoContent;
return;
}
// Get the voice service. If it doesn't exist, return an error.
IWebRtcVoiceService voiceService = scene.RequestModuleInterface();
if (voiceService is null)
{
m_log.ErrorFormat("{0}[ProvisionVoice]: avatar \"{1}\": no voice service", logHeader, agentID);
response.StatusCode = (int)HttpStatusCode.NotFound;
return;
}
// Make sure the request is for WebRtc voice
if (map.TryGetValue("voice_server_type", out OSD vstosd))
{
if (vstosd is OSDString vst && !((string)vst).Equals("webrtc", StringComparison.OrdinalIgnoreCase))
{
m_log.WarnFormat("{0}[ProvisionVoice]: voice_server_type is not 'webrtc'. Request: {1}", logHeader, map.ToString());
response.RawBuffer = Util.UTF8.GetBytes("");
return;
}
}
// The checks passed. Send the request to the voice service.
OSDMap resp = voiceService.ProvisionVoiceAccountRequest(map, agentID, scene.RegionInfo.RegionID).Result;
if (_MessageDetails) m_log.DebugFormat("{0}[ProvisionVoice]: response: {1}", logHeader, resp.ToString());
// TODO: check for errors and package the response
// Convert the OSD to LLSDXml for the response
string xmlResp = OSDParser.SerializeLLSDXmlString(resp);
response.StatusCode = (int)HttpStatusCode.OK;
response.RawBuffer = Util.UTF8.GetBytes(xmlResp);
return;
}
public void VoiceSignalingRequest(IOSHttpRequest request, IOSHttpResponse response, UUID agentID, Scene scene)
{
if (request.HttpMethod != "POST")
{
m_log.ErrorFormat("[{0}][VoiceSignaling]: Not a POST request. Agent={1}", logHeader, agentID.ToString());
response.StatusCode = (int)HttpStatusCode.NotFound;
return;
}
// Deserialize the request. Convert the LLSDXml to OSD for our use
OSDMap map = BodyToMap(request, "[VoiceSignalingRequest]");
if (map is null)
{
m_log.ErrorFormat("{0}[VoiceSignalingRequest]: No request data found. Agent={1}", logHeader, agentID.ToString());
response.StatusCode = (int)HttpStatusCode.NoContent;
return;
}
// Make sure the request is for WebRTC voice
if (map.TryGetValue("voice_server_type", out OSD vstosd))
{
if (vstosd is OSDString vst && !((string)vst).Equals("webrtc", StringComparison.OrdinalIgnoreCase))
{
response.RawBuffer = Util.UTF8.GetBytes("");
return;
}
}
IWebRtcVoiceService voiceService = scene.RequestModuleInterface();
if (voiceService is null)
{
m_log.ErrorFormat("{0}[VoiceSignalingRequest]: avatar \"{1}\": no voice service", logHeader, agentID);
response.StatusCode = (int)HttpStatusCode.NotFound;
return;
}
OSDMap resp = voiceService.VoiceSignalingRequest(map, agentID, scene.RegionInfo.RegionID).Result;
if (_MessageDetails) m_log.DebugFormat("{0}[VoiceSignalingRequest]: Response: {1}", logHeader, resp);
// TODO: check for errors and package the response
response.StatusCode = (int)HttpStatusCode.OK;
response.RawBuffer = Util.UTF8.GetBytes("");
return;
}
///
/// Callback for a client request for ChatSessionRequest.
/// The viewer sends this request when the user tries to start a P2P text or voice session
/// with another user. We need to generate a new session ID and return it to the client.
///
///
///
///
///
public void ChatSessionRequest(IOSHttpRequest request, IOSHttpResponse response, UUID agentID, Scene scene)
{
m_log.DebugFormat("{0}: ChatSessionRequest received for agent {1} in scene {2}", logHeader, agentID, scene.RegionInfo.RegionName);
if (request.HttpMethod != "POST")
{
response.StatusCode = (int)HttpStatusCode.NotFound;
return;
}
if (!scene.TryGetScenePresence(agentID, out ScenePresence sp) || sp.IsDeleted)
{
m_log.Warn($"{logHeader} ChatSessionRequest: scene presence not found or deleted for agent {agentID}");
response.StatusCode = (int)HttpStatusCode.NotFound;
return;
}
OSDMap reqmap = BodyToMap(request, "[ChatSessionRequest]");
if (reqmap is null)
{
m_log.Warn($"{logHeader} ChatSessionRequest: message body not parsable in request for agent {agentID}");
response.StatusCode = (int)HttpStatusCode.NoContent;
return;
}
m_log.Debug($"{logHeader} ChatSessionRequest");
if (!reqmap.TryGetString("method", out string method))
{
m_log.Warn($"{logHeader} ChatSessionRequest: missing required 'method' field in request for agent {agentID}");
response.StatusCode = (int)HttpStatusCode.NotFound;
return;
}
if (!reqmap.TryGetUUID("session-id", out UUID sessionID))
{
m_log.Warn($"{logHeader} ChatSessionRequest: missing required 'session-id' field in request for agent {agentID}");
response.StatusCode = (int)HttpStatusCode.NotFound;
return;
}
switch (method.ToLower())
{
// Several different method requests that we don't know how to handle.
// Just return OK for now.
case "decline p2p voice":
case "decline invitation":
case "start conference":
case "fetch history":
response.StatusCode = (int)HttpStatusCode.OK;
break;
// Asking to start a P2P voice session. We need to generate a new session ID and return
// it to the client in a ChatterBoxSessionStartReply event.
case "start p2p voice":
UUID newSessionID;
if (reqmap.TryGetUUID("params", out UUID otherID))
newSessionID = new(otherID.ulonga ^ agentID.ulonga, otherID.ulongb ^ agentID.ulongb);
else
newSessionID = UUID.Random();
IEventQueue queue = scene.RequestModuleInterface();
if (queue is null)
{
m_log.ErrorFormat("{0}: no event queue for scene {1}", logHeader, scene.RegionInfo.RegionName);
response.StatusCode = (int)HttpStatusCode.InternalServerError;
}
else
{
queue.ChatterBoxSessionStartReply(
newSessionID,
sp.Name,
2,
false,
true,
sessionID,
true,
string.Empty,
agentID);
response.StatusCode = (int)HttpStatusCode.OK;
}
break;
default:
response.StatusCode = (int)HttpStatusCode.BadRequest;
break;
}
}
// NOTE NOTE!! This is code from the FreeSwitch module. It is not clear if this is correct for WebRtc.
///
/// Callback for a client request for ParcelVoiceInfo
///
/// current scene object of the client
///
///
///
///
///
///
public void ParcelVoiceInfoRequest(IOSHttpRequest request, IOSHttpResponse response, UUID agentID, Scene scene)
{
if (request.HttpMethod != "POST")
{
response.StatusCode = (int)HttpStatusCode.NotFound;
return;
}
response.StatusCode = (int)HttpStatusCode.OK;
m_log.DebugFormat(
"{0}[PARCELVOICE]: ParcelVoiceInfoRequest() on {1} for {2}",
logHeader, scene.RegionInfo.RegionName, agentID);
ScenePresence avatar = scene.GetScenePresence(agentID);
if (avatar == null)
{
response.RawBuffer = Util.UTF8.GetBytes("undef");
return;
}
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
{
string channelUri;
if (null == scene.LandChannel)
{
m_log.ErrorFormat("region \"{0}\": avatar \"{1}\": land data not yet available",
scene.RegionInfo.RegionName, avatarName);
response.RawBuffer = Util.UTF8.GetBytes("undef");
return;
}
// 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);
// TODO: EstateSettings don't seem to get propagated...
if (!scene.RegionInfo.EstateSettings.AllowVoice)
{
m_log.DebugFormat("{0}[PARCELVOICE]: region \"{1}\": voice not enabled in estate settings",
logHeader, scene.RegionInfo.RegionName);
channelUri = String.Empty;
}
else
if (!scene.RegionInfo.EstateSettings.TaxFree && (land.Flags & (uint)ParcelFlags.AllowVoiceChat) == 0)
{
channelUri = String.Empty;
}
else
{
channelUri = ChannelUri(scene, land);
}
// fast foward encode
osUTF8 lsl = LLSDxmlEncode2.Start(512);
LLSDxmlEncode2.AddMap(lsl);
LLSDxmlEncode2.AddElem("parcel_local_id", land.LocalID, lsl);
LLSDxmlEncode2.AddElem("region_name", scene.Name, lsl);
LLSDxmlEncode2.AddMap("voice_credentials", lsl);
LLSDxmlEncode2.AddElem("channel_uri", channelUri, lsl);
//LLSDxmlEncode2.AddElem("channel_credentials", channel_credentials, lsl);
LLSDxmlEncode2.AddEndMap(lsl);
LLSDxmlEncode2.AddEndMap(lsl);
response.RawBuffer = LLSDxmlEncode2.EndToBytes(lsl);
}
catch (Exception e)
{
m_log.ErrorFormat("{0}[PARCELVOICE]: region \"{1}\": avatar \"{2}\": {3}, retry later",
logHeader, scene.RegionInfo.RegionName, avatarName, e.Message);
m_log.DebugFormat("{0}[PARCELVOICE]: region \"{1}\": avatar \"{2}\": {3} failed",
logHeader, scene.RegionInfo.RegionName, avatarName, e.ToString());
response.RawBuffer = Util.UTF8.GetBytes("undef");
}
}
// NOTE NOTE!! This is code from the FreeSwitch module. It is not clear if this is correct for WebRtc.
// Not sure what this Uri is for. Is this FreeSwitch specific?
// TODO: is this useful for WebRtc?
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.
lock (m_ParcelAddress)
{
if (m_ParcelAddress.ContainsKey(land.GlobalID.ToString()))
{
m_log.DebugFormat("{0}: parcel id {1}: using sip address {2}",
logHeader, land.GlobalID, m_ParcelAddress[land.GlobalID.ToString()]);
return m_ParcelAddress[land.GlobalID.ToString()];
}
}
if (land.LocalID != 1 && (land.Flags & (uint)ParcelFlags.UseEstateVoiceChan) == 0)
{
landName = String.Format("{0}:{1}", scene.RegionInfo.RegionName, land.Name);
landUUID = land.GlobalID.ToString();
m_log.DebugFormat("{0}: Region:Parcel \"{1}\": parcel id {2}: using channel name {3}",
logHeader, landName, land.LocalID, landUUID);
}
else
{
landName = String.Format("{0}:{1}", scene.RegionInfo.RegionName, scene.RegionInfo.RegionName);
landUUID = scene.RegionInfo.RegionID.ToString();
m_log.DebugFormat("{0}: Region:Parcel \"{1}\": parcel id {2}: using channel name {3}",
logHeader, landName, land.LocalID, landUUID);
}
// slvoice handles the sip address differently if it begins with confctl, hiding it from the user in
// the friends list. however it also disables the personal speech indicators as well unless some
// siren14-3d codec magic happens. we dont have siren143d so we'll settle for the personal speech indicator.
channelUri = String.Format("sip:conf-{0}@{1}",
"x" + Convert.ToBase64String(Encoding.ASCII.GetBytes(landUUID)),
/*m_freeSwitchRealm*/ "webRTC");
lock (m_ParcelAddress)
{
if (!m_ParcelAddress.ContainsKey(land.GlobalID.ToString()))
{
m_ParcelAddress.Add(land.GlobalID.ToString(), channelUri);
}
}
return channelUri;
}
///
/// Convert the LLSDXml body of the request to an OSDMap for easier handling.
/// Also logs the request if message details is enabled.
///
///
///
/// 'null' if the request body is empty or cannot be deserialized
private OSDMap BodyToMap(IOSHttpRequest request, string pCaller)
{
OSDMap? map = null;
using (Stream inputStream = request.InputStream)
{
if (inputStream.Length > 0)
{
OSD tmp = OSDParser.DeserializeLLSDXml(inputStream);
if (_MessageDetails) m_log.DebugFormat("{0} BodyToMap: Request: {1}", pCaller, tmp.ToString());
map = tmp as OSDMap;
}
}
return map;
}
}
}