/*
* 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;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Reflection;
using System.IO;
using System.Web;
using System.Xml;
using log4net;
using Mono.Addins;
using Nini.Config;
using OpenMetaverse;
using OpenMetaverse.Messages.Linden;
using OpenMetaverse.StructuredData;
using OpenSim.Framework;
using OpenSim.Framework.Capabilities;
using OpenSim.Framework.Servers;
using OpenSim.Framework.Servers.HttpServer;
using OpenSim.Region.Framework.Interfaces;
using OpenSim.Region.Framework.Scenes;
using OpenSim.Services.Interfaces;
using Caps = OpenSim.Framework.Capabilities.Caps;
using OSDArray = OpenMetaverse.StructuredData.OSDArray;
using OSDMap = OpenMetaverse.StructuredData.OSDMap;
namespace OpenSim.Region.CoreModules.World.Media.Moap
{
[Extension(Path = "/OpenSim/RegionModules", NodeName = "RegionModule", Id = "MoapModule")]
public class MoapModule : INonSharedRegionModule, IMoapModule
{
private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
public string Name { get { return "MoapModule"; } }
public Type ReplaceableInterface { get { return null; } }
///
/// Is this module enabled?
///
protected bool m_isEnabled = true;
///
/// The scene to which this module is attached
///
protected Scene m_scene;
///
/// Track the ObjectMedia capabilities given to users keyed by path
///
protected Dictionary m_omCapUsers = new Dictionary();
///
/// Track the ObjectMedia capabilities given to users keyed by agent. Lock m_omCapUsers to manipulate.
///
protected Dictionary m_omCapUrls = new Dictionary();
///
/// Track the ObjectMediaUpdate capabilities given to users keyed by path
///
protected Dictionary m_omuCapUsers = new Dictionary();
///
/// Track the ObjectMediaUpdate capabilities given to users keyed by agent. Lock m_omuCapUsers to manipulate
///
protected Dictionary m_omuCapUrls = new Dictionary();
public void Initialise(IConfigSource configSource)
{
IConfig config = configSource.Configs["MediaOnAPrim"];
if (config != null && !config.GetBoolean("Enabled", false))
m_isEnabled = false;
// else
// m_log.Debug("[MOAP]: Initialised module.")l
}
public void AddRegion(Scene scene)
{
if (!m_isEnabled)
return;
m_scene = scene;
m_scene.RegisterModuleInterface(this);
}
public void RemoveRegion(Scene scene) {}
public void RegionLoaded(Scene scene)
{
if (!m_isEnabled)
return;
m_scene.EventManager.OnRegisterCaps += OnRegisterCaps;
m_scene.EventManager.OnDeregisterCaps += OnDeregisterCaps;
m_scene.EventManager.OnSceneObjectPartCopy += OnSceneObjectPartCopy;
}
public void Close()
{
if (!m_isEnabled)
return;
m_scene.EventManager.OnRegisterCaps -= OnRegisterCaps;
m_scene.EventManager.OnDeregisterCaps -= OnDeregisterCaps;
m_scene.EventManager.OnSceneObjectPartCopy -= OnSceneObjectPartCopy;
}
public void OnRegisterCaps(UUID agentID, Caps caps)
{
// m_log.DebugFormat(
// "[MOAP]: Registering ObjectMedia and ObjectMediaNavigate capabilities for agent {0}", agentID);
string omCapUrl = "/CAPS/" + UUID.Random();
lock (m_omCapUsers)
{
m_omCapUsers[omCapUrl] = agentID;
m_omCapUrls[agentID] = omCapUrl;
// Even though we're registering for POST we're going to get GETS and UPDATES too
caps.RegisterHandler(
"ObjectMedia",
new RestStreamHandler(
"POST", omCapUrl, HandleObjectMediaMessage, "ObjectMedia", agentID.ToString()));
}
string omuCapUrl = "/CAPS/" + UUID.Random();
lock (m_omuCapUsers)
{
m_omuCapUsers[omuCapUrl] = agentID;
m_omuCapUrls[agentID] = omuCapUrl;
// Even though we're registering for POST we're going to get GETS and UPDATES too
caps.RegisterHandler(
"ObjectMediaNavigate",
new RestStreamHandler(
"POST", omuCapUrl, HandleObjectMediaNavigateMessage, "ObjectMediaNavigate", agentID.ToString()));
}
}
public void OnDeregisterCaps(UUID agentID, Caps caps)
{
lock (m_omCapUsers)
{
string path = m_omCapUrls[agentID];
m_omCapUrls.Remove(agentID);
m_omCapUsers.Remove(path);
}
lock (m_omuCapUsers)
{
string path = m_omuCapUrls[agentID];
m_omuCapUrls.Remove(agentID);
m_omuCapUsers.Remove(path);
}
}
protected void OnSceneObjectPartCopy(SceneObjectPart copy, SceneObjectPart original, bool userExposed)
{
if (original.Shape.Media != null)
{
PrimitiveBaseShape.MediaList dupeMedia = new PrimitiveBaseShape.MediaList();
lock (original.Shape.Media)
{
foreach (MediaEntry me in original.Shape.Media)
{
if (me != null)
dupeMedia.Add(MediaEntry.FromOSD(me.GetOSD()));
else
dupeMedia.Add(null);
}
}
copy.Shape.Media = dupeMedia;
}
}
public MediaEntry GetMediaEntry(SceneObjectPart part, int face)
{
MediaEntry me = null;
CheckFaceParam(part, face);
List media = part.Shape.Media;
if (null == media)
{
me = null;
}
else
{
lock (media)
me = media[face];
// TODO: Really need a proper copy constructor down in libopenmetaverse
if (me != null)
me = MediaEntry.FromOSD(me.GetOSD());
}
// m_log.DebugFormat("[MOAP]: GetMediaEntry for {0} face {1} found {2}", part.Name, face, me);
return me;
}
///
/// Set the media entry on the face of the given part.
///
/// /param>
///
/// If null, then the media entry is cleared.
public void SetMediaEntry(SceneObjectPart part, int face, MediaEntry me)
{
// m_log.DebugFormat("[MOAP]: SetMediaEntry for {0}, face {1}", part.Name, face);
CheckFaceParam(part, face);
if (null == part.Shape.Media)
{
if (me == null)
return;
else
part.Shape.Media = new PrimitiveBaseShape.MediaList(new MediaEntry[part.GetNumberOfSides()]);
}
lock (part.Shape.Media)
part.Shape.Media[face] = me;
UpdateMediaUrl(part, UUID.Zero);
SetPartMediaFlags(part, face, me != null);
part.ScheduleFullUpdate();
part.TriggerScriptChangedEvent(Changed.MEDIA);
}
///
/// Clear the media entry from the face of the given part.
///
///
///
public void ClearMediaEntry(SceneObjectPart part, int face)
{
SetMediaEntry(part, face, null);
}
///
/// Set the media flags on the texture face of the given part.
///
///
/// The fact that we need a separate function to do what should be a simple one line operation is BUTT UGLY.
///
///
///
///
protected void SetPartMediaFlags(SceneObjectPart part, int face, bool flag)
{
Primitive.TextureEntry te = part.Shape.Textures;
Primitive.TextureEntryFace teFace = te.CreateFace((uint)face);
teFace.MediaFlags = flag;
part.Shape.Textures = te;
}
///
/// Sets or gets per face media textures.
///
///
///
///
///
///
///
protected string HandleObjectMediaMessage(
string request, string path, string param, IOSHttpRequest httpRequest, IOSHttpResponse httpResponse)
{
// m_log.DebugFormat("[MOAP]: Got ObjectMedia path [{0}], raw request [{1}]", path, request);
OSDMap osd = (OSDMap)OSDParser.DeserializeLLSDXml(request);
ObjectMediaMessage omm = new ObjectMediaMessage();
omm.Deserialize(osd);
if (omm.Request is ObjectMediaRequest)
return HandleObjectMediaRequest(omm.Request as ObjectMediaRequest);
else if (omm.Request is ObjectMediaUpdate)
return HandleObjectMediaUpdate(path, omm.Request as ObjectMediaUpdate);
throw new Exception(
string.Format(
"[MOAP]: ObjectMediaMessage has unrecognized ObjectMediaBlock of {0}",
omm.Request.GetType()));
}
///
/// Handle a fetch request for media textures
///
///
///
protected string HandleObjectMediaRequest(ObjectMediaRequest omr)
{
UUID primId = omr.PrimID;
SceneObjectPart part = m_scene.GetSceneObjectPart(primId);
if (null == part)
{
m_log.WarnFormat(
"[MOAP]: Received a GET ObjectMediaRequest for prim {0} but this doesn't exist in region {1}",
primId, m_scene.RegionInfo.RegionName);
return string.Empty;
}
if (null == part.Shape.Media)
return string.Empty;
ObjectMediaResponse resp = new ObjectMediaResponse();
resp.PrimID = primId;
lock (part.Shape.Media)
resp.FaceMedia = part.Shape.Media.ToArray();
resp.Version = part.MediaUrl;
string rawResp = OSDParser.SerializeLLSDXmlString(resp.Serialize());
// m_log.DebugFormat("[MOAP]: Got HandleObjectMediaRequestGet raw response is [{0}]", rawResp);
return rawResp;
}
///
/// Handle an update of media textures.
///
/// Path on which this request was made
/// /param>
///
protected string HandleObjectMediaUpdate(string path, ObjectMediaUpdate omu)
{
UUID primId = omu.PrimID;
SceneObjectPart part = m_scene.GetSceneObjectPart(primId);
if (null == part)
{
m_log.WarnFormat(
"[MOAP]: Received an UPDATE ObjectMediaRequest for prim {0} but this doesn't exist in region {1}",
primId, m_scene.RegionInfo.RegionName);
return string.Empty;
}
// m_log.DebugFormat("[MOAP]: Received {0} media entries for prim {1}", omu.FaceMedia.Length, primId);
//
// for (int i = 0; i < omu.FaceMedia.Length; i++)
// {
// MediaEntry me = omu.FaceMedia[i];
// string v = (null == me ? "null": OSDParser.SerializeLLSDXmlString(me.GetOSD()));
// m_log.DebugFormat("[MOAP]: Face {0} [{1}]", i, v);
// }
if (omu.FaceMedia.Length > part.GetNumberOfSides())
{
m_log.WarnFormat(
"[MOAP]: Received {0} media entries from client for prim {1} {2} but this prim has only {3} faces. Dropping request.",
omu.FaceMedia.Length, part.Name, part.UUID, part.GetNumberOfSides());
return string.Empty;
}
UUID agentId = default(UUID);
lock (m_omCapUsers)
agentId = m_omCapUsers[path];
List media = part.Shape.Media;
if (null == media)
{
// m_log.DebugFormat("[MOAP]: Setting all new media list for {0}", part.Name);
part.Shape.Media = new PrimitiveBaseShape.MediaList(omu.FaceMedia);
for (int i = 0; i < omu.FaceMedia.Length; i++)
{
if (omu.FaceMedia[i] != null)
{
// FIXME: Race condition here since some other texture entry manipulator may overwrite/get
// overwritten. Unfortunately, PrimitiveBaseShape does not allow us to change texture entry
// directly.
SetPartMediaFlags(part, i, true);
// m_log.DebugFormat(
// "[MOAP]: Media flags for face {0} is {1}",
// i, part.Shape.Textures.FaceTextures[i].MediaFlags);
}
}
}
else
{
// m_log.DebugFormat("[MOAP]: Setting existing media list for {0}", part.Name);
// We need to go through the media textures one at a time to make sure that we have permission
// to change them
// FIXME: Race condition here since some other texture entry manipulator may overwrite/get
// overwritten. Unfortunately, PrimitiveBaseShape does not allow us to change texture entry
// directly.
Primitive.TextureEntry te = part.Shape.Textures;
lock (media)
{
for (int i = 0; i < media.Count; i++)
{
if (m_scene.Permissions.CanControlPrimMedia(agentId, part.UUID, i))
{
media[i] = omu.FaceMedia[i];
// When a face is cleared this is done by setting the MediaFlags in the TextureEntry via a normal
// texture update, so we don't need to worry about clearing MediaFlags here.
if (null == media[i])
continue;
SetPartMediaFlags(part, i, true);
// m_log.DebugFormat(
// "[MOAP]: Media flags for face {0} is {1}",
// i, face.MediaFlags);
// m_log.DebugFormat("[MOAP]: Set media entry for face {0} on {1}", i, part.Name);
}
}
}
part.Shape.Textures = te;
// for (int i2 = 0; i2 < part.Shape.Textures.FaceTextures.Length; i2++)
// m_log.DebugFormat("[MOAP]: FaceTexture[{0}] is {1}", i2, part.Shape.Textures.FaceTextures[i2]);
}
UpdateMediaUrl(part, agentId);
// Arguably, we could avoid sending a full update to the avatar that just changed the texture.
part.ScheduleFullUpdate();
part.TriggerScriptChangedEvent(Changed.MEDIA);
return string.Empty;
}
///
/// Received from the viewer if a user has changed the url of a media texture.
///
///
///
///
/// /param>
/// /param>
///
protected string HandleObjectMediaNavigateMessage(
string request, string path, string param, IOSHttpRequest httpRequest, IOSHttpResponse httpResponse)
{
// m_log.DebugFormat("[MOAP]: Got ObjectMediaNavigate request [{0}]", request);
OSDMap osd = (OSDMap)OSDParser.DeserializeLLSDXml(request);
ObjectMediaNavigateMessage omn = new ObjectMediaNavigateMessage();
omn.Deserialize(osd);
UUID primId = omn.PrimID;
SceneObjectPart part = m_scene.GetSceneObjectPart(primId);
if (null == part)
{
m_log.WarnFormat(
"[MOAP]: Received an ObjectMediaNavigateMessage for prim {0} but this doesn't exist in region {1}",
primId, m_scene.RegionInfo.RegionName);
return string.Empty;
}
UUID agentId = default(UUID);
lock (m_omuCapUsers)
agentId = m_omuCapUsers[path];
if (!m_scene.Permissions.CanInteractWithPrimMedia(agentId, part.UUID, omn.Face))
return string.Empty;
// m_log.DebugFormat(
// "[MOAP]: Received request to update media entry for face {0} on prim {1} {2} to {3}",
// omn.Face, part.Name, part.UUID, omn.URL);
// If media has never been set for this prim, then just return.
if (null == part.Shape.Media)
return string.Empty;
MediaEntry me = null;
lock (part.Shape.Media)
me = part.Shape.Media[omn.Face];
// Do the same if media has not been set up for a specific face
if (null == me)
return string.Empty;
if (me.EnableWhiteList)
{
if (!CheckUrlAgainstWhitelist(omn.URL, me.WhiteList))
{
// m_log.DebugFormat(
// "[MOAP]: Blocking change of face {0} on prim {1} {2} to {3} since it's not on the enabled whitelist",
// omn.Face, part.Name, part.UUID, omn.URL);
return string.Empty;
}
}
me.CurrentURL = omn.URL;
UpdateMediaUrl(part, agentId);
part.ScheduleFullUpdate();
part.TriggerScriptChangedEvent(Changed.MEDIA);
return OSDParser.SerializeLLSDXmlString(new OSD());
}
///
/// Check that the face number is valid for the given prim.
///
///
///
protected void CheckFaceParam(SceneObjectPart part, int face)
{
if (face < 0)
throw new ArgumentException("Face cannot be less than zero");
int maxFaces = part.GetNumberOfSides() - 1;
if (face > maxFaces)
throw new ArgumentException(
string.Format("Face argument was {0} but max is {1}", face, maxFaces));
}
///
/// Update the media url of the given part
///
///
///
/// The id to attach to this update. Normally, this is the user that changed the
/// texture
///
protected void UpdateMediaUrl(SceneObjectPart part, UUID updateId)
{
if (null == part.MediaUrl)
{
// TODO: We can't set the last changer until we start tracking which cap we give to which agent id
part.MediaUrl = "x-mv:0000000000/" + updateId;
}
else
{
string rawVersion = part.MediaUrl.Substring(5, 10);
int version = int.Parse(rawVersion);
part.MediaUrl = string.Format("x-mv:{0:D10}/{1}", ++version, updateId);
}
// m_log.DebugFormat("[MOAP]: Storing media url [{0}] in prim {1} {2}", part.MediaUrl, part.Name, part.UUID);
}
///
/// Check the given url against the given whitelist.
///
///
///
/// true if the url matches an entry on the whitelist, false otherwise
protected bool CheckUrlAgainstWhitelist(string rawUrl, string[] whitelist)
{
Uri url = new Uri(rawUrl);
foreach (string origWlUrl in whitelist)
{
string wlUrl = origWlUrl;
// Deal with a line-ending wildcard
if (wlUrl.EndsWith("*"))
wlUrl = wlUrl.Remove(wlUrl.Length - 1);
// m_log.DebugFormat("[MOAP]: Checking whitelist URL pattern {0}", origWlUrl);
// Handle a line starting wildcard slightly differently since this can only match the domain, not the path
if (wlUrl.StartsWith("*"))
{
wlUrl = wlUrl.Substring(1);
if (url.Host.Contains(wlUrl))
{
// m_log.DebugFormat("[MOAP]: Whitelist URL {0} matches {1}", origWlUrl, rawUrl);
return true;
}
}
else
{
string urlToMatch = url.Authority + url.AbsolutePath;
if (urlToMatch.StartsWith(wlUrl))
{
// m_log.DebugFormat("[MOAP]: Whitelist URL {0} matches {1}", origWlUrl, rawUrl);
return true;
}
}
}
return false;
}
}
}