/* * 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 log4net; using Mono.Addins; using Nini.Config; using OpenMetaverse; using OpenMetaverse.StructuredData; using OpenSim.Framework; using OpenSim.Framework.Servers.HttpServer; using OpenSim.Region.Framework.Interfaces; using OpenSim.Region.Framework.Scenes; using OpenSim.Server.Base; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Reflection; using System.Threading.Tasks; using Caps = OpenSim.Framework.Capabilities.Caps; using OSDMap = OpenMetaverse.StructuredData.OSDMap; [assembly: Addin("WebRtcVoiceRegionModule", "1.0")] [assembly: AddinDependency("OpenSim.Region.Framework", OpenSim.VersionInfo.VersionNumber)] namespace osWebRtcVoice { /// /// This module provides the WebRTC voice interface for viewer clients.. /// /// In particular, it provides the following capabilities: /// ProvisionVoiceAccountRequest, VoiceSignalingRequest and limited ChatSessionRequest /// 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 static byte[] llsdUndefAnswerBytes = Util.UTF8.GetBytes(""); private bool _MessageDetails = false; // Control info private static bool m_Enabled = false; private IConfig m_Config; private IWebRtcVoiceService m_spatialVoiceService; private IWebRtcVoiceService m_nonSpatialVoiceService; private UUID gridHash = UUID.Zero; // 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) { // Get the DLLs for the two voice services // TODO: spacial/nonspacial names are wrong // spacial here means service for region parcels, that can be spacial or not // non spacial means for other uses like IMs, that just happen to be non spacial // in fact this needs more consideration than just this 2 options string spatialDllName = m_Config.GetString("SpatialVoiceService", string.Empty); string nonSpatialDllName = m_Config.GetString("NonSpatialVoiceService", string.Empty); if (string.IsNullOrEmpty(spatialDllName) && string.IsNullOrEmpty(nonSpatialDllName)) { m_log.Error($"{LogHeader} No VoiceService specified in configuration"); m_Enabled = false; return; } // Default non-spatial to spatial if not specified if (string.IsNullOrEmpty(nonSpatialDllName)) { m_log.Debug($"{LogHeader} nonSpatialDllName not specified. Defaulting to spatialDllName"); nonSpatialDllName = spatialDllName; } // Load the two voice services m_log.Debug($"{LogHeader} Loading SpatialVoiceService from {spatialDllName}"); m_spatialVoiceService = ServerUtils.LoadPlugin(spatialDllName, [config]); if (m_spatialVoiceService is null) { m_log.Error($"{LogHeader} Could not load SpatialVoiceService from {spatialDllName}, module disabled"); m_Enabled = false; return; } m_log.Debug($"{LogHeader} Loading NonSpatialVoiceService from {nonSpatialDllName}"); if (spatialDllName != nonSpatialDllName) { m_nonSpatialVoiceService = ServerUtils.LoadPlugin(nonSpatialDllName, [ m_Config ]); if (m_nonSpatialVoiceService is null) { m_log.Error($"{LogHeader} Could not load NonSpatialVoiceService from {nonSpatialDllName}"); m_Enabled = false; } } else m_nonSpatialVoiceService = m_spatialVoiceService; if (m_Enabled) { _MessageDetails = m_Config.GetBoolean("MessageDetails", false); m_log.Info($"{LogHeader} WebRtcVoiceService enabled"); } } } } // ISharedRegionModule.PostInitialize public void PostInitialise() { } // ISharedRegionModule.AddRegion public void AddRegion(Scene scene) { // TODO: register module to get parcels changes etc } // ISharedRegionModule.RemoveRegion public void RemoveRegion(Scene scene) { } // ISharedRegionModule.RegionLoaded public void RegionLoaded(Scene scene) { if (m_Enabled) { scene.EventManager.OnRegisterCaps += delegate (UUID agentID, Caps caps) { OnRegisterCaps(scene, agentID, caps); }; scene.EventManager.OnRemovePresence += delegate (UUID agentID) { OnRemovePresence(scene, agentID); }; scene.EventManager.OnNewClient += OnNewClient; if(gridHash.IsZero()) { if(!string.IsNullOrEmpty(scene.SceneGridInfo.GridUrl)) gridHash = Util.ComputeShake128UUID(scene.SceneGridInfo.GridUrl + scene.SceneGridInfo.GridName); else if (!string.IsNullOrEmpty(scene.SceneGridInfo.HomeURL + scene.SceneGridInfo.GridName)) gridHash = Util.ComputeShake128UUID(scene.SceneGridInfo.HomeURLNoEndSlash); } ISimulatorFeaturesModule simFeatures = scene.RequestModuleInterface(); simFeatures?.AddFeature("VoiceServerType", OSD.FromString("webrtc")); } } // ISharedRegionModule.Close public void Close() { } // ISharedRegionModule.Name public string Name { get { return "RegionVoiceModule"; } } // ISharedRegionModule.ReplaceableInterface public Type ReplaceableInterface { get { return null; } } private void OnNewClient(IClientAPI client) { client.OnLogout += OnClientLogOut; } private void OnClientLogOut(IClientAPI client) { client.OnLogout -= OnClientLogOut; if(client.SceneAgent is not ScenePresence sp) return; List toremove = []; if (VoiceViewerSession.TryGetViewerSessionsByAgentAndRegion(sp.UUID, sp.Scene.ID, out IEnumerable> vSessions)) { foreach(KeyValuePair v in vSessions) { if((v.Value.Flags & IVoiceViewerSession.VFlags.IsParcel) != 0) toremove.Add(v.Value); } foreach(IVoiceViewerSession v in toremove) VoiceViewerSession.RemoveViewerSession(v.ViewerSessionID); } Util.FireAndForget( x => { try { OSDMap vreq = new() { { "logout" , true}, { "viewer_session" , UUID.Zero} }; m_spatialVoiceService?.ProvisionVoiceAccountRequest(vreq , sp.UUID, sp.Scene.ID); if(m_nonSpatialVoiceService != m_spatialVoiceService) m_nonSpatialVoiceService?.ProvisionVoiceAccountRequest(vreq , sp.UUID, sp.Scene.ID); } catch (Exception ex) { m_log.Debug($"{LogHeader} OnClientLogOut exception: {ex.Message}"); } }); } private static void OnRemovePresence(Scene pScene, UUID pAgentID) { List toremove = []; if (VoiceViewerSession.TryGetViewerSessionsByAgentAndRegion(pAgentID, pScene.RegionInfo.RegionID, out IEnumerable> vSessions)) { foreach(KeyValuePair v in vSessions) { if((v.Value.Flags & IVoiceViewerSession.VFlags.IsParcel) != 0) toremove.Add(v.Value); } if(toremove.Count > 0) { foreach(IVoiceViewerSession v in toremove) VoiceViewerSession.RemoveViewerSession(v.ViewerSessionID); Util.FireAndForget( x => { List toremoveas = toremove; foreach(IVoiceViewerSession v in toremoveas) try { OSDMap vreq = new() { { "logout" , true}, { "viewer_session" , v.ViewerSessionID} }; v.VoiceService.ProvisionVoiceAccountRequest(v, vreq , v.AgentId, v.RegionId); } catch (Exception ex) { m_log.Debug( $"{LogHeader} OnRemovePresence: failed for viewer_session {v.ViewerSessionID}: {ex.Message}"); } }); } } } private static void CleanupDuplicateSessions(UUID pAgentID, UUID pSceneID, string pKeepViewerSessionId) { if(VoiceViewerSession.TryGetViewerSessionsByAgentAndRegion(pAgentID, pSceneID, out IEnumerable> candidates)) { bool noskip = string.IsNullOrEmpty(pKeepViewerSessionId); List toremove = []; foreach (KeyValuePair candidate in candidates) { if (noskip && candidate.Key == pKeepViewerSessionId) continue; m_log.Warn( $"{LogHeader} CleanupDuplicateSessions: removing stale viewer_session {candidate.Key} for agent {pAgentID}, scene {pSceneID}"); toremove.Add(candidate.Value); } foreach(IVoiceViewerSession v in toremove) VoiceViewerSession.RemoveViewerSession(v.ViewerSessionID); if(toremove.Count > 0) { Util.FireAndForget( x => { foreach(IVoiceViewerSession v in toremove) { try { OSDMap vreq = new() { { "logout" , true}, { "viewer_session" , v.ViewerSessionID} }; v.VoiceService.ProvisionVoiceAccountRequest(v, vreq , v.AgentId, v.RegionId); } catch (Exception ex) { m_log.Debug( $"{LogHeader} CleanupDuplicateSessions: shutdown failed for viewer_session {v.ViewerSessionID}: {ex.Message}"); } } }); } } } // // 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 limited ChatSessionRequest // // ProvisionVoiceAccountRequest allows the client to obtain // voice communication information the the avater. // // VoiceSignalingRequest: Used for trickling ICE candidates. // // ChatSessionRequest // // 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.Debug( $"{LogHeader}: OnRegisterCaps called with agentID {agentID} in scene {scene.Name}"); 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("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.Debug($"{LogHeader}[ProvisionVoice]: Not a POST request. Agent={agentID}"); response.StatusCode = (int)HttpStatusCode.NotFound; return; } // Deserialize the request. Convert the LLSDXml to OSD for our use OSDMap map = BodyToMap(request, $"{LogHeader}[ProvisionVoice]"); if (map is null) { m_log.Error($"{LogHeader}[ProvisionVoice]: No request data found. Agent={agentID}"); 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)) { m_log.Warn($"{LogHeader}[ProvisionVoice]: voice_server_type is not 'webrtc'"); if (m_log.IsDebugEnabled) m_log.Warn($"{LogHeader}[ProvisionVoice]: Request detail: {map}"); response.RawBuffer = llsdUndefAnswerBytes; response.StatusCode = (int)HttpStatusCode.OK; return; } } if (_MessageDetails) m_log.Debug($"{LogHeader}[ProvisionVoice]: request: {map}"); IVoiceViewerSession vSession = null; if (map.TryGetString("viewer_session", out string viewerSessionId)) { if(map.TryGetBool("logout", out bool islog) && islog) { if(UUID.ZeroString.Equals(viewerSessionId, StringComparison.OrdinalIgnoreCase)) { if (VoiceViewerSession.TryGetViewerSessionsByAgentId(agentID, out IEnumerable> vSessions)) { m_log.Info( $"{LogHeader} ProvisionVoiceAccountRequest: doing logout for {vSessions.Count()} stall sessions"); OSDMap vreq = new() {{ "logout" , true} }; foreach(KeyValuePair kvp in vSessions) { IVoiceViewerSession v = kvp.Value; if(v is null) continue; vreq["viewer_session"] = v.ViewerSessionID; VoiceViewerSession.RemoveViewerSession(v.ViewerSessionID); v.VoiceService.ProvisionVoiceAccountRequest(v, vreq , agentID, scene.RegionInfo.RegionID); } } response.RawBuffer = OSDParser.SerializeLLSDXmlBytes(new OSDMap {{ "response", "closed" }}); response.StatusCode = (int)HttpStatusCode.OK; return ; } OSDMap logoutresp = null; if (VoiceViewerSession.TryGetViewerSession(viewerSessionId, out vSession)) { VoiceViewerSession.RemoveViewerSession(viewerSessionId); logoutresp = vSession.VoiceService.ProvisionVoiceAccountRequest(vSession, map, agentID, scene.RegionInfo.RegionID); } logoutresp ??= new OSDMap() { { "response", "error" }, { "message", "Logout session not found" } }; response.RawBuffer = OSDParser.SerializeLLSDXmlBytes(logoutresp); response.StatusCode = (int)HttpStatusCode.OK; return ; } // request has a viewer session. Use that to find the voice service if (VoiceViewerSession.TryGetViewerSession(viewerSessionId, out vSession)) { CleanupDuplicateSessions(agentID, scene.RegionInfo.RegionID, viewerSessionId); } } else { //no session id.. new channel? if (map.TryGetString("channel_type", out string channelType)) { CleanupDuplicateSessions(agentID, scene.RegionInfo.RegionID, null); if(!scene.TryGetScenePresence(agentID, out ScenePresence sp)) { m_log.Debug($"{LogHeader}[ProvisionVoice]:avatar not found"); response.RawBuffer = llsdUndefAnswerBytes; response.StatusCode = (int)HttpStatusCode.NotFound; return; } IVoiceViewerSession.VFlags flags = IVoiceViewerSession.VFlags.None; //do fully not trust viewers voice parcel requests if (channelType == "local") { if (!scene.RegionInfo.EstateSettings.AllowVoice) { m_log.Debug($"{LogHeader}[ProvisionVoice]:region \"{scene.Name}\": voice not enabled in estate settings"); response.RawBuffer = llsdUndefAnswerBytes; response.StatusCode = (int)HttpStatusCode.NotImplemented; return; } if (scene.LandChannel == null) { m_log.Error($"{LogHeader}[ProvisionVoice] region \"{scene.Name}\" land data not yet available"); response.RawBuffer = llsdUndefAnswerBytes; response.StatusCode = (int)HttpStatusCode.NotImplemented; return; } if(map.TryGetInt("parcel_local_id", out int parcelID)) { ILandObject parcel = scene.LandChannel.GetLandObject(parcelID); if (parcel == null) { response.RawBuffer = llsdUndefAnswerBytes; response.StatusCode = (int)HttpStatusCode.NotFound; return; } LandData land = parcel.LandData; if (land == null) { response.RawBuffer = llsdUndefAnswerBytes; response.StatusCode = (int)HttpStatusCode.NotFound; return; } if (!scene.RegionInfo.EstateSettings.TaxFree && (land.Flags & (uint)ParcelFlags.AllowVoiceChat) == 0) { m_log.Debug($"{LogHeader}[ProvisionVoice]:parcel voice not allowed"); response.RawBuffer = llsdUndefAnswerBytes; response.StatusCode = (int)HttpStatusCode.Forbidden; return; } if ((land.Flags & (uint)ParcelFlags.UseEstateVoiceChan) != 0) { // By removing the parcel_local_id, the voice service will treat this as an estate channel // request and return the appropriate voice credentials for the estate channel // instead of a parcel channel map.Remove("parcel_local_id"); // estate channel flags = IVoiceViewerSession.VFlags.IsEstate; } else { if(parcel.IsRestrictedFromLand(agentID) || parcel.IsBannedFromLand(agentID)) { // check Z distance? m_log.Debug($"{LogHeader}[ProvisionVoice]:agent not allowed on parcel"); response.RawBuffer = llsdUndefAnswerBytes; response.StatusCode = (int)HttpStatusCode.Forbidden; return; } flags = parcel.OwnerID.Equals(agentID) ? IVoiceViewerSession.VFlags.IsAdmin | IVoiceViewerSession.VFlags.IsParcel : IVoiceViewerSession.VFlags.IsParcel; } } else { flags = IVoiceViewerSession.VFlags.IsEstate; } // TODO: check if this userId is making a new session (case that user is reconnecting) vSession = m_spatialVoiceService.CreateViewerSession(map, agentID, scene.RegionInfo.RegionID); if(vSession != null) { if(sp.IsChildAgent) flags |= IVoiceViewerSession.VFlags.IsChildAgent; else if(scene.Permissions.IsEstateManager(agentID)) flags |= IVoiceViewerSession.VFlags.IsAdmin; vSession.Flags = flags; VoiceViewerSession.AddViewerSession(vSession); } } else { if(sp.IsChildAgent) { // check Z distance? m_log.Debug($"{LogHeader}[ProvisionVoice]:child agent request non local voice"); response.RawBuffer = llsdUndefAnswerBytes; response.StatusCode = (int)HttpStatusCode.Forbidden; return; } vSession = m_nonSpatialVoiceService.CreateViewerSession(map, agentID, scene.RegionInfo.RegionID); if(vSession != null) { vSession.Flags = IVoiceViewerSession.VFlags.IsAdmin; VoiceViewerSession.AddViewerSession(vSession); map["gridhash"] = gridHash; } } } } OSDMap resp = null; if (vSession is not null) { resp = vSession.VoiceService.ProvisionVoiceAccountRequest(vSession, map, agentID, scene.RegionInfo.RegionID); } if (resp is null) { response.StatusCode = (int)HttpStatusCode.BadRequest; if (_MessageDetails) m_log.Debug($"{LogHeader}[ProvisionVoice]: got null response"); return; } if (_MessageDetails) m_log.Debug($"{LogHeader}[ProvisionVoice]: response: {resp}"); response.RawBuffer = OSDParser.SerializeLLSDXmlToBytes(resp); response.StatusCode = (int)HttpStatusCode.OK; return; } public void VoiceSignalingRequest(IOSHttpRequest request, IOSHttpResponse response, UUID agentID, Scene scene) { if(request.HttpMethod != "POST") { m_log.Error($"{LogHeader}[VoiceSignaling]: Not a POST request. Agent={agentID}"); response.StatusCode = (int)HttpStatusCode.NotFound; return; } // Deserialize the request. Convert the LLSDXml to OSD for our use OSDMap map = BodyToMap(request, $"{LogHeader}[VoiceSignaling]"); if (map is null) { m_log.Error($"{LogHeader}[VoiceSignalingRequest]: No request data found. Agent={agentID}"); 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 = llsdUndefAnswerBytes; response.StatusCode = (int)HttpStatusCode.OK; return; } } OSDMap resp = null; if (map.TryGetString("viewer_session", out string viewerSessionId)) { // request has a viewer session. Use that to find the voice service if (VoiceViewerSession.TryGetViewerSession(viewerSessionId, out IVoiceViewerSession vSession)) { resp = vSession.VoiceService.VoiceSignalingRequest(vSession, map, agentID, scene.RegionInfo.RegionID); } else { m_log.Error($"{LogHeader} VoiceSignalingRequest: viewer session {viewerSessionId} not found"); } } else { m_log.Error($"{LogHeader} VoiceSignalingRequest: no viewer_session in request"); } if (_MessageDetails) m_log.Debug($"{LogHeader}[VoiceSignalingRequest]: Response: {resp ?? "null"}"); // TODO: check for errors // viewers ignore response response.RawBuffer = llsdUndefAnswerBytes; response.StatusCode = (int)HttpStatusCode.OK; 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.Debug($"{LogHeader}: ChatSessionRequest received for agent {agentID} in scene {scene.Name}"); 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, $"{LogHeader}[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; } string servertype = null; if(reqmap.TryGetOSDMap("alt_params", out OSDMap altparams)) { if(!altparams.TryGetString("voice_server_type", out servertype)) _ = altparams.TryGetString("preferred_voice_server_type", out servertype); } if(!string.IsNullOrEmpty(servertype)) { if(!servertype.Equals("webrtc", StringComparison.OrdinalIgnoreCase)) { response.RawBuffer = llsdUndefAnswerBytes; response.StatusCode = (int)HttpStatusCode.OK; 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.Error($"{LogHeader}: no event queue for scene {scene.Name}"); 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; case "call": m_log.Debug($"{LogHeader}: ChatSessionRequest call: {reqmap}"); response.StatusCode = (int)HttpStatusCode.BadRequest; break; default: response.StatusCode = (int)HttpStatusCode.BadRequest; break; } } /// /// 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) { try { if (request.InputStream.Length > 0) { using Stream inputStream = request.InputStream; OSD tmp = OSDParser.DeserializeLLSDXml(inputStream); if (_MessageDetails) m_log.Debug($"{pCaller} BodyToMap: Request: {tmp}"); if(tmp is OSDMap map) return map; } } catch { m_log.Debug($"{pCaller} BodyToMap: Fail to decode LLSDXml request"); } return null; } } }