aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/OpenSim/Region/OptionalModules/Avatar/Voice
diff options
context:
space:
mode:
authorUbitUmarov2015-09-01 11:43:07 +0100
committerUbitUmarov2015-09-01 11:43:07 +0100
commitfb78b182520fc9bb0f971afd0322029c70278ea6 (patch)
treeb4e30d383938fdeef8c92d1d1c2f44bb61d329bd /OpenSim/Region/OptionalModules/Avatar/Voice
parentlixo (diff)
parentMantis #7713: fixed bug introduced by 1st MOSES patch. (diff)
downloadopensim-SC-fb78b182520fc9bb0f971afd0322029c70278ea6.zip
opensim-SC-fb78b182520fc9bb0f971afd0322029c70278ea6.tar.gz
opensim-SC-fb78b182520fc9bb0f971afd0322029c70278ea6.tar.bz2
opensim-SC-fb78b182520fc9bb0f971afd0322029c70278ea6.tar.xz
Merge remote-tracking branch 'os/master'
Diffstat (limited to 'OpenSim/Region/OptionalModules/Avatar/Voice')
-rw-r--r--OpenSim/Region/OptionalModules/Avatar/Voice/FreeSwitchVoice/FreeSwitchVoiceModule.cs899
-rw-r--r--OpenSim/Region/OptionalModules/Avatar/Voice/VivoxVoice/VivoxVoiceModule.cs1328
2 files changed, 2227 insertions, 0 deletions
diff --git a/OpenSim/Region/OptionalModules/Avatar/Voice/FreeSwitchVoice/FreeSwitchVoiceModule.cs b/OpenSim/Region/OptionalModules/Avatar/Voice/FreeSwitchVoice/FreeSwitchVoiceModule.cs
new file mode 100644
index 0000000..45af212
--- /dev/null
+++ b/OpenSim/Region/OptionalModules/Avatar/Voice/FreeSwitchVoice/FreeSwitchVoiceModule.cs
@@ -0,0 +1,899 @@
1/*
2 * Copyright (c) Contributors, http://opensimulator.org/
3 * See CONTRIBUTORS.TXT for a full list of copyright holders.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
7 * * Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * * Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 * * Neither the name of the OpenSimulator Project nor the
13 * names of its contributors may be used to endorse or promote products
14 * derived from this software without specific prior written permission.
15 *
16 * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
17 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
20 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
23 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 */
27
28using System;
29using System.IO;
30using System.Net;
31using System.Net.Security;
32using System.Web;
33using System.Security.Cryptography.X509Certificates;
34using System.Text;
35using System.Xml;
36using System.Collections;
37using System.Collections.Generic;
38using System.Reflection;
39using OpenMetaverse;
40using OpenMetaverse.StructuredData;
41using log4net;
42using Nini.Config;
43using Nwc.XmlRpc;
44using OpenSim.Framework;
45using Mono.Addins;
46
47using OpenSim.Framework.Capabilities;
48using OpenSim.Framework.Servers;
49using OpenSim.Framework.Servers.HttpServer;
50using OpenSim.Region.Framework.Interfaces;
51using OpenSim.Region.Framework.Scenes;
52using Caps = OpenSim.Framework.Capabilities.Caps;
53using System.Text.RegularExpressions;
54using OpenSim.Server.Base;
55using OpenSim.Services.Interfaces;
56using OSDMap = OpenMetaverse.StructuredData.OSDMap;
57
58namespace OpenSim.Region.OptionalModules.Avatar.Voice.FreeSwitchVoice
59{
60 [Extension(Path = "/OpenSim/RegionModules", NodeName = "RegionModule", Id = "FreeSwitchVoiceModule")]
61 public class FreeSwitchVoiceModule : ISharedRegionModule, IVoiceModule
62 {
63 private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
64
65 // Capability string prefixes
66 private static readonly string m_parcelVoiceInfoRequestPath = "0207/";
67 private static readonly string m_provisionVoiceAccountRequestPath = "0208/";
68 //private static readonly string m_chatSessionRequestPath = "0209/";
69
70 // Control info
71 private static bool m_Enabled = false;
72
73 // FreeSwitch server is going to contact us and ask us all
74 // sorts of things.
75
76 // SLVoice client will do a GET on this prefix
77 private static string m_freeSwitchAPIPrefix;
78
79 // We need to return some information to SLVoice
80 // figured those out via curl
81 // http://vd1.vivox.com/api2/viv_get_prelogin.php
82 //
83 // need to figure out whether we do need to return ALL of
84 // these...
85 private static string m_freeSwitchRealm;
86 private static string m_freeSwitchSIPProxy;
87 private static bool m_freeSwitchAttemptUseSTUN;
88 private static string m_freeSwitchEchoServer;
89 private static int m_freeSwitchEchoPort;
90 private static string m_freeSwitchDefaultWellKnownIP;
91 private static int m_freeSwitchDefaultTimeout;
92 private static string m_freeSwitchUrlResetPassword;
93 private uint m_freeSwitchServicePort;
94 private string m_openSimWellKnownHTTPAddress;
95// private string m_freeSwitchContext;
96
97 private readonly Dictionary<string, string> m_UUIDName = new Dictionary<string, string>();
98 private Dictionary<string, string> m_ParcelAddress = new Dictionary<string, string>();
99
100 private IConfig m_Config;
101
102 private IFreeswitchService m_FreeswitchService;
103
104 public void Initialise(IConfigSource config)
105 {
106 m_Config = config.Configs["FreeSwitchVoice"];
107
108 if (m_Config == null)
109 return;
110
111 if (!m_Config.GetBoolean("Enabled", false))
112 return;
113
114 try
115 {
116 string serviceDll = m_Config.GetString("LocalServiceModule",
117 String.Empty);
118
119 if (serviceDll == String.Empty)
120 {
121 m_log.Error("[FreeSwitchVoice]: No LocalServiceModule named in section FreeSwitchVoice. Not starting.");
122 return;
123 }
124
125 Object[] args = new Object[] { config };
126 m_FreeswitchService = ServerUtils.LoadPlugin<IFreeswitchService>(serviceDll, args);
127
128 string jsonConfig = m_FreeswitchService.GetJsonConfig();
129 //m_log.Debug("[FreeSwitchVoice]: Configuration string: " + jsonConfig);
130 OSDMap map = (OSDMap)OSDParser.DeserializeJson(jsonConfig);
131
132 m_freeSwitchAPIPrefix = map["APIPrefix"].AsString();
133 m_freeSwitchRealm = map["Realm"].AsString();
134 m_freeSwitchSIPProxy = map["SIPProxy"].AsString();
135 m_freeSwitchAttemptUseSTUN = map["AttemptUseSTUN"].AsBoolean();
136 m_freeSwitchEchoServer = map["EchoServer"].AsString();
137 m_freeSwitchEchoPort = map["EchoPort"].AsInteger();
138 m_freeSwitchDefaultWellKnownIP = map["DefaultWellKnownIP"].AsString();
139 m_freeSwitchDefaultTimeout = map["DefaultTimeout"].AsInteger();
140 m_freeSwitchUrlResetPassword = String.Empty;
141// m_freeSwitchContext = map["Context"].AsString();
142
143 if (String.IsNullOrEmpty(m_freeSwitchRealm) ||
144 String.IsNullOrEmpty(m_freeSwitchAPIPrefix))
145 {
146 m_log.Error("[FreeSwitchVoice]: Freeswitch service mis-configured. Not starting.");
147 return;
148 }
149
150 // set up http request handlers for
151 // - prelogin: viv_get_prelogin.php
152 // - signin: viv_signin.php
153 // - buddies: viv_buddy.php
154 // - ???: viv_watcher.php
155 // - signout: viv_signout.php
156 MainServer.Instance.AddHTTPHandler(String.Format("{0}/viv_get_prelogin.php", m_freeSwitchAPIPrefix),
157 FreeSwitchSLVoiceGetPreloginHTTPHandler);
158
159 MainServer.Instance.AddHTTPHandler(String.Format("{0}/freeswitch-config", m_freeSwitchAPIPrefix), FreeSwitchConfigHTTPHandler);
160
161 // RestStreamHandler h = new
162 // RestStreamHandler("GET",
163 // String.Format("{0}/viv_get_prelogin.php", m_freeSwitchAPIPrefix), FreeSwitchSLVoiceGetPreloginHTTPHandler);
164 // MainServer.Instance.AddStreamHandler(h);
165
166 MainServer.Instance.AddHTTPHandler(String.Format("{0}/viv_signin.php", m_freeSwitchAPIPrefix),
167 FreeSwitchSLVoiceSigninHTTPHandler);
168
169 MainServer.Instance.AddHTTPHandler(String.Format("{0}/viv_buddy.php", m_freeSwitchAPIPrefix),
170 FreeSwitchSLVoiceBuddyHTTPHandler);
171
172 MainServer.Instance.AddHTTPHandler(String.Format("{0}/viv_watcher.php", m_freeSwitchAPIPrefix),
173 FreeSwitchSLVoiceWatcherHTTPHandler);
174
175 m_log.InfoFormat("[FreeSwitchVoice]: using FreeSwitch server {0}", m_freeSwitchRealm);
176
177 m_Enabled = true;
178
179 m_log.Info("[FreeSwitchVoice]: plugin enabled");
180 }
181 catch (Exception e)
182 {
183 m_log.ErrorFormat("[FreeSwitchVoice]: plugin initialization failed: {0} {1}", e.Message, e.StackTrace);
184 return;
185 }
186
187 // This here is a region module trying to make a global setting.
188 // Not really a good idea but it's Windows only, so I can't test.
189 try
190 {
191 ServicePointManager.ServerCertificateValidationCallback += CustomCertificateValidation;
192 }
193 catch (NotImplementedException)
194 {
195 try
196 {
197#pragma warning disable 0612, 0618
198 // Mono does not implement the ServicePointManager.ServerCertificateValidationCallback yet! Don't remove this!
199 ServicePointManager.CertificatePolicy = new MonoCert();
200#pragma warning restore 0612, 0618
201 }
202 catch (Exception)
203 {
204 // COmmented multiline spam log message
205 //m_log.Error("[FreeSwitchVoice]: Certificate validation handler change not supported. You may get ssl certificate validation errors teleporting from your region to some SSL regions.");
206 }
207 }
208 }
209
210 public void PostInitialise()
211 {
212 }
213
214 public void AddRegion(Scene scene)
215 {
216 // We generate these like this: The region's external host name
217 // as defined in Regions.ini is a good address to use. It's a
218 // dotted quad (or should be!) and it can reach this host from
219 // a client. The port is grabbed from the region's HTTP server.
220 m_openSimWellKnownHTTPAddress = scene.RegionInfo.ExternalHostName;
221 m_freeSwitchServicePort = MainServer.Instance.Port;
222
223 if (m_Enabled)
224 {
225 // we need to capture scene in an anonymous method
226 // here as we need it later in the callbacks
227 scene.EventManager.OnRegisterCaps += delegate(UUID agentID, Caps caps)
228 {
229 OnRegisterCaps(scene, agentID, caps);
230 };
231 }
232 }
233
234 public void RemoveRegion(Scene scene)
235 {
236 }
237
238 public void RegionLoaded(Scene scene)
239 {
240 if (m_Enabled)
241 {
242 m_log.Info("[FreeSwitchVoice]: registering IVoiceModule with the scene");
243
244 // register the voice interface for this module, so the script engine can call us
245 scene.RegisterModuleInterface<IVoiceModule>(this);
246 }
247 }
248
249 public void Close()
250 {
251 }
252
253 public string Name
254 {
255 get { return "FreeSwitchVoiceModule"; }
256 }
257
258 public Type ReplaceableInterface
259 {
260 get { return null; }
261 }
262
263 // <summary>
264 // implementation of IVoiceModule, called by osSetParcelSIPAddress script function
265 // </summary>
266 public void setLandSIPAddress(string SIPAddress,UUID GlobalID)
267 {
268 m_log.DebugFormat("[FreeSwitchVoice]: setLandSIPAddress parcel id {0}: setting sip address {1}",
269 GlobalID, SIPAddress);
270
271 lock (m_ParcelAddress)
272 {
273 if (m_ParcelAddress.ContainsKey(GlobalID.ToString()))
274 {
275 m_ParcelAddress[GlobalID.ToString()] = SIPAddress;
276 }
277 else
278 {
279 m_ParcelAddress.Add(GlobalID.ToString(), SIPAddress);
280 }
281 }
282 }
283
284 // <summary>
285 // OnRegisterCaps is invoked via the scene.EventManager
286 // everytime OpenSim hands out capabilities to a client
287 // (login, region crossing). We contribute two capabilities to
288 // the set of capabilities handed back to the client:
289 // ProvisionVoiceAccountRequest and ParcelVoiceInfoRequest.
290 //
291 // ProvisionVoiceAccountRequest allows the client to obtain
292 // the voice account credentials for the avatar it is
293 // controlling (e.g., user name, password, etc).
294 //
295 // ParcelVoiceInfoRequest is invoked whenever the client
296 // changes from one region or parcel to another.
297 //
298 // Note that OnRegisterCaps is called here via a closure
299 // delegate containing the scene of the respective region (see
300 // Initialise()).
301 // </summary>
302 public void OnRegisterCaps(Scene scene, UUID agentID, Caps caps)
303 {
304 m_log.DebugFormat(
305 "[FreeSwitchVoice]: OnRegisterCaps() called with agentID {0} caps {1} in scene {2}",
306 agentID, caps, scene.RegionInfo.RegionName);
307
308 string capsBase = "/CAPS/" + caps.CapsObjectPath;
309 caps.RegisterHandler(
310 "ProvisionVoiceAccountRequest",
311 new RestStreamHandler(
312 "POST",
313 capsBase + m_provisionVoiceAccountRequestPath,
314 (request, path, param, httpRequest, httpResponse)
315 => ProvisionVoiceAccountRequest(scene, request, path, param, agentID, caps),
316 "ProvisionVoiceAccountRequest",
317 agentID.ToString()));
318
319 caps.RegisterHandler(
320 "ParcelVoiceInfoRequest",
321 new RestStreamHandler(
322 "POST",
323 capsBase + m_parcelVoiceInfoRequestPath,
324 (request, path, param, httpRequest, httpResponse)
325 => ParcelVoiceInfoRequest(scene, request, path, param, agentID, caps),
326 "ParcelVoiceInfoRequest",
327 agentID.ToString()));
328
329 //caps.RegisterHandler(
330 // "ChatSessionRequest",
331 // new RestStreamHandler(
332 // "POST",
333 // capsBase + m_chatSessionRequestPath,
334 // (request, path, param, httpRequest, httpResponse)
335 // => ChatSessionRequest(scene, request, path, param, agentID, caps),
336 // "ChatSessionRequest",
337 // agentID.ToString()));
338 }
339
340 /// <summary>
341 /// Callback for a client request for Voice Account Details
342 /// </summary>
343 /// <param name="scene">current scene object of the client</param>
344 /// <param name="request"></param>
345 /// <param name="path"></param>
346 /// <param name="param"></param>
347 /// <param name="agentID"></param>
348 /// <param name="caps"></param>
349 /// <returns></returns>
350 public string ProvisionVoiceAccountRequest(Scene scene, string request, string path, string param,
351 UUID agentID, Caps caps)
352 {
353 m_log.DebugFormat(
354 "[FreeSwitchVoice][PROVISIONVOICE]: ProvisionVoiceAccountRequest() request: {0}, path: {1}, param: {2}", request, path, param);
355
356 ScenePresence avatar = scene.GetScenePresence(agentID);
357 if (avatar == null)
358 {
359 System.Threading.Thread.Sleep(2000);
360 avatar = scene.GetScenePresence(agentID);
361
362 if (avatar == null)
363 return "<llsd>undef</llsd>";
364 }
365 string avatarName = avatar.Name;
366
367 try
368 {
369 //XmlElement resp;
370 string agentname = "x" + Convert.ToBase64String(agentID.GetBytes());
371 string password = "1234";//temp hack//new UUID(Guid.NewGuid()).ToString().Replace('-','Z').Substring(0,16);
372
373 // XXX: we need to cache the voice credentials, as
374 // FreeSwitch is later going to come and ask us for
375 // those
376 agentname = agentname.Replace('+', '-').Replace('/', '_');
377
378 lock (m_UUIDName)
379 {
380 if (m_UUIDName.ContainsKey(agentname))
381 {
382 m_UUIDName[agentname] = avatarName;
383 }
384 else
385 {
386 m_UUIDName.Add(agentname, avatarName);
387 }
388 }
389
390 // LLSDVoiceAccountResponse voiceAccountResponse =
391 // new LLSDVoiceAccountResponse(agentname, password, m_freeSwitchRealm, "http://etsvc02.hursley.ibm.com/api");
392 LLSDVoiceAccountResponse voiceAccountResponse =
393 new LLSDVoiceAccountResponse(agentname, password, m_freeSwitchRealm,
394 String.Format("http://{0}:{1}{2}/", m_openSimWellKnownHTTPAddress,
395 m_freeSwitchServicePort, m_freeSwitchAPIPrefix));
396
397 string r = LLSDHelpers.SerialiseLLSDReply(voiceAccountResponse);
398
399// m_log.DebugFormat("[FreeSwitchVoice][PROVISIONVOICE]: avatar \"{0}\": {1}", avatarName, r);
400
401 return r;
402 }
403 catch (Exception e)
404 {
405 m_log.ErrorFormat("[FreeSwitchVoice][PROVISIONVOICE]: avatar \"{0}\": {1}, retry later", avatarName, e.Message);
406 m_log.DebugFormat("[FreeSwitchVoice][PROVISIONVOICE]: avatar \"{0}\": {1} failed", avatarName, e.ToString());
407
408 return "<llsd>undef</llsd>";
409 }
410 }
411
412 /// <summary>
413 /// Callback for a client request for ParcelVoiceInfo
414 /// </summary>
415 /// <param name="scene">current scene object of the client</param>
416 /// <param name="request"></param>
417 /// <param name="path"></param>
418 /// <param name="param"></param>
419 /// <param name="agentID"></param>
420 /// <param name="caps"></param>
421 /// <returns></returns>
422 public string ParcelVoiceInfoRequest(Scene scene, string request, string path, string param,
423 UUID agentID, Caps caps)
424 {
425 m_log.DebugFormat(
426 "[FreeSwitchVoice][PARCELVOICE]: ParcelVoiceInfoRequest() on {0} for {1}",
427 scene.RegionInfo.RegionName, agentID);
428
429 ScenePresence avatar = scene.GetScenePresence(agentID);
430 string avatarName = avatar.Name;
431
432 // - check whether we have a region channel in our cache
433 // - if not:
434 // create it and cache it
435 // - send it to the client
436 // - send channel_uri: as "sip:regionID@m_sipDomain"
437 try
438 {
439 LLSDParcelVoiceInfoResponse parcelVoiceInfo;
440 string channelUri;
441
442 if (null == scene.LandChannel)
443 throw new Exception(String.Format("region \"{0}\": avatar \"{1}\": land data not yet available",
444 scene.RegionInfo.RegionName, avatarName));
445
446 // get channel_uri: check first whether estate
447 // settings allow voice, then whether parcel allows
448 // voice, if all do retrieve or obtain the parcel
449 // voice channel
450 LandData land = scene.GetLandData(avatar.AbsolutePosition);
451
452 //m_log.DebugFormat("[FreeSwitchVoice][PARCELVOICE]: region \"{0}\": Parcel \"{1}\" ({2}): avatar \"{3}\": request: {4}, path: {5}, param: {6}",
453 // scene.RegionInfo.RegionName, land.Name, land.LocalID, avatarName, request, path, param);
454
455 // TODO: EstateSettings don't seem to get propagated...
456 // if (!scene.RegionInfo.EstateSettings.AllowVoice)
457 // {
458 // m_log.DebugFormat("[FreeSwitchVoice][PARCELVOICE]: region \"{0}\": voice not enabled in estate settings",
459 // scene.RegionInfo.RegionName);
460 // channel_uri = String.Empty;
461 // }
462 // else
463
464 if ((land.Flags & (uint)ParcelFlags.AllowVoiceChat) == 0)
465 {
466// m_log.DebugFormat("[FreeSwitchVoice][PARCELVOICE]: region \"{0}\": Parcel \"{1}\" ({2}): avatar \"{3}\": voice not enabled for parcel",
467// scene.RegionInfo.RegionName, land.Name, land.LocalID, avatarName);
468 channelUri = String.Empty;
469 }
470 else
471 {
472 channelUri = ChannelUri(scene, land);
473 }
474
475 // fill in our response to the client
476 Hashtable creds = new Hashtable();
477 creds["channel_uri"] = channelUri;
478
479 parcelVoiceInfo = new LLSDParcelVoiceInfoResponse(scene.RegionInfo.RegionName, land.LocalID, creds);
480 string r = LLSDHelpers.SerialiseLLSDReply(parcelVoiceInfo);
481
482// m_log.DebugFormat("[FreeSwitchVoice][PARCELVOICE]: region \"{0}\": Parcel \"{1}\" ({2}): avatar \"{3}\": {4}",
483// scene.RegionInfo.RegionName, land.Name, land.LocalID, avatarName, r);
484 return r;
485 }
486 catch (Exception e)
487 {
488 m_log.ErrorFormat("[FreeSwitchVoice][PARCELVOICE]: region \"{0}\": avatar \"{1}\": {2}, retry later",
489 scene.RegionInfo.RegionName, avatarName, e.Message);
490 m_log.DebugFormat("[FreeSwitchVoice][PARCELVOICE]: region \"{0}\": avatar \"{1}\": {2} failed",
491 scene.RegionInfo.RegionName, avatarName, e.ToString());
492
493 return "<llsd>undef</llsd>";
494 }
495 }
496
497 /// <summary>
498 /// Callback for a client request for ChatSessionRequest
499 /// </summary>
500 /// <param name="scene">current scene object of the client</param>
501 /// <param name="request"></param>
502 /// <param name="path"></param>
503 /// <param name="param"></param>
504 /// <param name="agentID"></param>
505 /// <param name="caps"></param>
506 /// <returns></returns>
507 public string ChatSessionRequest(Scene scene, string request, string path, string param,
508 UUID agentID, Caps caps)
509 {
510 ScenePresence avatar = scene.GetScenePresence(agentID);
511 string avatarName = avatar.Name;
512
513 m_log.DebugFormat("[FreeSwitchVoice][CHATSESSION]: avatar \"{0}\": request: {1}, path: {2}, param: {3}",
514 avatarName, request, path, param);
515
516 return "<llsd>true</llsd>";
517 }
518
519 public Hashtable ForwardProxyRequest(Hashtable request)
520 {
521 m_log.Debug("[PROXYING]: -------------------------------proxying request");
522 Hashtable response = new Hashtable();
523 response["content_type"] = "text/xml";
524 response["str_response_string"] = "";
525 response["int_response_code"] = 200;
526
527 string forwardaddress = "https://www.bhr.vivox.com/api2/";
528 string body = (string)request["body"];
529 string method = (string) request["http-method"];
530 string contenttype = (string) request["content-type"];
531 string uri = (string) request["uri"];
532 uri = uri.Replace("/api/", "");
533 forwardaddress += uri;
534
535
536 string fwdresponsestr = "";
537 int fwdresponsecode = 200;
538 string fwdresponsecontenttype = "text/xml";
539
540 HttpWebRequest forwardreq = (HttpWebRequest)WebRequest.Create(forwardaddress);
541 forwardreq.Method = method;
542 forwardreq.ContentType = contenttype;
543 forwardreq.KeepAlive = false;
544
545 if (method == "POST")
546 {
547 byte[] contentreq = Util.UTF8.GetBytes(body);
548 forwardreq.ContentLength = contentreq.Length;
549 Stream reqStream = forwardreq.GetRequestStream();
550 reqStream.Write(contentreq, 0, contentreq.Length);
551 reqStream.Close();
552 }
553
554 using (HttpWebResponse fwdrsp = (HttpWebResponse)forwardreq.GetResponse())
555 {
556 Encoding encoding = Util.UTF8;
557
558 using (Stream s = fwdrsp.GetResponseStream())
559 {
560 using (StreamReader fwdresponsestream = new StreamReader(s))
561 {
562 fwdresponsestr = fwdresponsestream.ReadToEnd();
563 fwdresponsecontenttype = fwdrsp.ContentType;
564 fwdresponsecode = (int)fwdrsp.StatusCode;
565 }
566 }
567 }
568
569 response["content_type"] = fwdresponsecontenttype;
570 response["str_response_string"] = fwdresponsestr;
571 response["int_response_code"] = fwdresponsecode;
572
573 return response;
574 }
575
576 public Hashtable FreeSwitchSLVoiceGetPreloginHTTPHandler(Hashtable request)
577 {
578 m_log.Debug("[FreeSwitchVoice]: FreeSwitchSLVoiceGetPreloginHTTPHandler called");
579
580 Hashtable response = new Hashtable();
581 response["content_type"] = "text/xml";
582 response["keepalive"] = false;
583
584 response["str_response_string"] = String.Format(
585 "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n" +
586 "<VCConfiguration>\r\n"+
587 "<DefaultRealm>{0}</DefaultRealm>\r\n" +
588 "<DefaultSIPProxy>{1}</DefaultSIPProxy>\r\n"+
589 "<DefaultAttemptUseSTUN>{2}</DefaultAttemptUseSTUN>\r\n"+
590 "<DefaultEchoServer>{3}</DefaultEchoServer>\r\n"+
591 "<DefaultEchoPort>{4}</DefaultEchoPort>\r\n"+
592 "<DefaultWellKnownIP>{5}</DefaultWellKnownIP>\r\n"+
593 "<DefaultTimeout>{6}</DefaultTimeout>\r\n"+
594 "<UrlResetPassword>{7}</UrlResetPassword>\r\n"+
595 "<UrlPrivacyNotice>{8}</UrlPrivacyNotice>\r\n"+
596 "<UrlEulaNotice/>\r\n"+
597 "<App.NoBottomLogo>false</App.NoBottomLogo>\r\n"+
598 "</VCConfiguration>",
599 m_freeSwitchRealm, m_freeSwitchSIPProxy, m_freeSwitchAttemptUseSTUN,
600 m_freeSwitchEchoServer, m_freeSwitchEchoPort,
601 m_freeSwitchDefaultWellKnownIP, m_freeSwitchDefaultTimeout,
602 m_freeSwitchUrlResetPassword, "");
603
604 response["int_response_code"] = 200;
605
606 //m_log.DebugFormat("[FreeSwitchVoice] FreeSwitchSLVoiceGetPreloginHTTPHandler return {0}",response["str_response_string"]);
607 return response;
608 }
609
610 public Hashtable FreeSwitchSLVoiceBuddyHTTPHandler(Hashtable request)
611 {
612 m_log.Debug("[FreeSwitchVoice]: FreeSwitchSLVoiceBuddyHTTPHandler called");
613
614 Hashtable response = new Hashtable();
615 response["int_response_code"] = 200;
616 response["str_response_string"] = string.Empty;
617 response["content-type"] = "text/xml";
618
619 Hashtable requestBody = ParseRequestBody((string)request["body"]);
620
621 if (!requestBody.ContainsKey("auth_token"))
622 return response;
623
624 string auth_token = (string)requestBody["auth_token"];
625 //string[] auth_tokenvals = auth_token.Split(':');
626 //string username = auth_tokenvals[0];
627 int strcount = 0;
628
629 string[] ids = new string[strcount];
630
631 int iter = -1;
632 lock (m_UUIDName)
633 {
634 strcount = m_UUIDName.Count;
635 ids = new string[strcount];
636 foreach (string s in m_UUIDName.Keys)
637 {
638 iter++;
639 ids[iter] = s;
640 }
641 }
642 StringBuilder resp = new StringBuilder();
643 resp.Append("<?xml version=\"1.0\" encoding=\"iso-8859-1\" ?><response xmlns=\"http://www.vivox.com\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation= \"/xsd/buddy_list.xsd\">");
644
645 resp.Append(string.Format(@"<level0>
646 <status>OK</status>
647 <cookie_name>lib_session</cookie_name>
648 <cookie>{0}</cookie>
649 <auth_token>{0}</auth_token>
650 <body>
651 <buddies>",auth_token));
652 /*
653 <cookie_name>lib_session</cookie_name>
654 <cookie>{0}:{1}:9303959503950::</cookie>
655 <auth_token>{0}:{1}:9303959503950::</auth_token>
656 */
657 for (int i=0;i<ids.Length;i++)
658 {
659 DateTime currenttime = DateTime.Now;
660 string dt = currenttime.ToString("yyyy-MM-dd HH:mm:ss.0zz");
661 resp.Append(
662 string.Format(@"<level3>
663 <bdy_id>{1}</bdy_id>
664 <bdy_data></bdy_data>
665 <bdy_uri>sip:{0}@{2}</bdy_uri>
666 <bdy_nickname>{0}</bdy_nickname>
667 <bdy_username>{0}</bdy_username>
668 <bdy_domain>{2}</bdy_domain>
669 <bdy_status>A</bdy_status>
670 <modified_ts>{3}</modified_ts>
671 <b2g_group_id></b2g_group_id>
672 </level3>", ids[i], i ,m_freeSwitchRealm, dt));
673 }
674
675 resp.Append("</buddies><groups></groups></body></level0></response>");
676
677 response["str_response_string"] = resp.ToString();
678// Regex normalizeEndLines = new Regex(@"(\r\n|\n)", RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.Multiline);
679//
680// m_log.DebugFormat(
681// "[FREESWITCH]: FreeSwitchSLVoiceBuddyHTTPHandler() response {0}",
682// normalizeEndLines.Replace((string)response["str_response_string"],""));
683
684 return response;
685 }
686
687 public Hashtable FreeSwitchSLVoiceWatcherHTTPHandler(Hashtable request)
688 {
689 m_log.Debug("[FreeSwitchVoice]: FreeSwitchSLVoiceWatcherHTTPHandler called");
690
691 Hashtable response = new Hashtable();
692 response["int_response_code"] = 200;
693 response["content-type"] = "text/xml";
694
695 Hashtable requestBody = ParseRequestBody((string)request["body"]);
696
697 string auth_token = (string)requestBody["auth_token"];
698 //string[] auth_tokenvals = auth_token.Split(':');
699 //string username = auth_tokenvals[0];
700
701 StringBuilder resp = new StringBuilder();
702 resp.Append("<?xml version=\"1.0\" encoding=\"iso-8859-1\" ?><response xmlns=\"http://www.vivox.com\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation= \"/xsd/buddy_list.xsd\">");
703
704 // FIXME: This is enough of a response to stop viewer 2 complaining about a login failure and get voice to work. If we don't
705 // give an OK response, then viewer 2 engages in an continuous viv_signin.php, viv_buddy.php, viv_watcher.php loop
706 // Viewer 1 appeared happy to ignore the lack of reply and still works with this reply.
707 //
708 // However, really we need to fill in whatever watcher data should be here (whatever that is).
709 resp.Append(string.Format(@"<level0>
710 <status>OK</status>
711 <cookie_name>lib_session</cookie_name>
712 <cookie>{0}</cookie>
713 <auth_token>{0}</auth_token>
714 <body/></level0></response>", auth_token));
715
716 response["str_response_string"] = resp.ToString();
717
718// Regex normalizeEndLines = new Regex(@"(\r\n|\n)", RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.Multiline);
719//
720// m_log.DebugFormat(
721// "[FREESWITCH]: FreeSwitchSLVoiceWatcherHTTPHandler() response {0}",
722// normalizeEndLines.Replace((string)response["str_response_string"],""));
723
724 return response;
725 }
726
727 public Hashtable FreeSwitchSLVoiceSigninHTTPHandler(Hashtable request)
728 {
729 m_log.Debug("[FreeSwitchVoice]: FreeSwitchSLVoiceSigninHTTPHandler called");
730// string requestbody = (string)request["body"];
731// string uri = (string)request["uri"];
732// string contenttype = (string)request["content-type"];
733
734 Hashtable requestBody = ParseRequestBody((string)request["body"]);
735
736 //string pwd = (string) requestBody["pwd"];
737 string userid = (string) requestBody["userid"];
738
739 string avatarName = string.Empty;
740 int pos = -1;
741 lock (m_UUIDName)
742 {
743 if (m_UUIDName.ContainsKey(userid))
744 {
745 avatarName = m_UUIDName[userid];
746 foreach (string s in m_UUIDName.Keys)
747 {
748 pos++;
749 if (s == userid)
750 break;
751 }
752 }
753 }
754
755 //m_log.DebugFormat("[FreeSwitchVoice]: AUTH, URI: {0}, Content-Type:{1}, Body{2}", uri, contenttype,
756 // requestbody);
757 Hashtable response = new Hashtable();
758 response["str_response_string"] = string.Format(@"<response xsi:schemaLocation=""/xsd/signin.xsd"">
759 <level0>
760 <status>OK</status>
761 <body>
762 <code>200</code>
763 <cookie_name>lib_session</cookie_name>
764 <cookie>{0}:{1}:9303959503950::</cookie>
765 <auth_token>{0}:{1}:9303959503950::</auth_token>
766 <primary>1</primary>
767 <account_id>{1}</account_id>
768 <displayname>{2}</displayname>
769 <msg>auth successful</msg>
770 </body>
771 </level0>
772 </response>", userid, pos, avatarName);
773
774 response["int_response_code"] = 200;
775
776// m_log.DebugFormat("[FreeSwitchVoice]: Sending FreeSwitchSLVoiceSigninHTTPHandler response");
777
778 return response;
779 }
780
781 public Hashtable ParseRequestBody(string body)
782 {
783 Hashtable bodyParams = new Hashtable();
784 // split string
785 string [] nvps = body.Split(new Char [] {'&'});
786
787 foreach (string s in nvps)
788 {
789 if (s.Trim() != "")
790 {
791 string [] nvp = s.Split(new Char [] {'='});
792 bodyParams.Add(HttpUtility.UrlDecode(nvp[0]), HttpUtility.UrlDecode(nvp[1]));
793 }
794 }
795
796 return bodyParams;
797 }
798
799 private string ChannelUri(Scene scene, LandData land)
800 {
801 string channelUri = null;
802
803 string landUUID;
804 string landName;
805
806 // Create parcel voice channel. If no parcel exists, then the voice channel ID is the same
807 // as the directory ID. Otherwise, it reflects the parcel's ID.
808
809 lock (m_ParcelAddress)
810 {
811 if (m_ParcelAddress.ContainsKey(land.GlobalID.ToString()))
812 {
813 m_log.DebugFormat("[FreeSwitchVoice]: parcel id {0}: using sip address {1}",
814 land.GlobalID, m_ParcelAddress[land.GlobalID.ToString()]);
815 return m_ParcelAddress[land.GlobalID.ToString()];
816 }
817 }
818
819 if (land.LocalID != 1 && (land.Flags & (uint)ParcelFlags.UseEstateVoiceChan) == 0)
820 {
821 landName = String.Format("{0}:{1}", scene.RegionInfo.RegionName, land.Name);
822 landUUID = land.GlobalID.ToString();
823 m_log.DebugFormat("[FreeSwitchVoice]: Region:Parcel \"{0}\": parcel id {1}: using channel name {2}",
824 landName, land.LocalID, landUUID);
825 }
826 else
827 {
828 landName = String.Format("{0}:{1}", scene.RegionInfo.RegionName, scene.RegionInfo.RegionName);
829 landUUID = scene.RegionInfo.RegionID.ToString();
830 m_log.DebugFormat("[FreeSwitchVoice]: Region:Parcel \"{0}\": parcel id {1}: using channel name {2}",
831 landName, land.LocalID, landUUID);
832 }
833
834 // slvoice handles the sip address differently if it begins with confctl, hiding it from the user in the friends list. however it also disables
835 // 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.
836 channelUri = String.Format("sip:conf-{0}@{1}", "x" + Convert.ToBase64String(Encoding.ASCII.GetBytes(landUUID)), m_freeSwitchRealm);
837
838 lock (m_ParcelAddress)
839 {
840 if (!m_ParcelAddress.ContainsKey(land.GlobalID.ToString()))
841 {
842 m_ParcelAddress.Add(land.GlobalID.ToString(),channelUri);
843 }
844 }
845
846 return channelUri;
847 }
848
849 private static bool CustomCertificateValidation(object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors error)
850 {
851 return true;
852 }
853
854 public Hashtable FreeSwitchConfigHTTPHandler(Hashtable request)
855 {
856 Hashtable response = new Hashtable();
857 response["str_response_string"] = string.Empty;
858 response["content_type"] = "text/plain";
859 response["keepalive"] = false;
860 response["int_response_code"] = 500;
861
862 Hashtable requestBody = ParseRequestBody((string)request["body"]);
863
864 string section = (string) requestBody["section"];
865
866 if (section == "directory")
867 {
868 string eventCallingFunction = (string)requestBody["Event-Calling-Function"];
869 m_log.DebugFormat(
870 "[FreeSwitchVoice]: Received request for config section directory, event calling function '{0}'",
871 eventCallingFunction);
872
873 response = m_FreeswitchService.HandleDirectoryRequest(requestBody);
874 }
875 else if (section == "dialplan")
876 {
877 m_log.DebugFormat("[FreeSwitchVoice]: Received request for config section dialplan");
878
879 response = m_FreeswitchService.HandleDialplanRequest(requestBody);
880 }
881 else
882 m_log.WarnFormat("[FreeSwitchVoice]: Unknown section {0} was requested from config.", section);
883
884 return response;
885 }
886 }
887
888 public class MonoCert : ICertificatePolicy
889 {
890 #region ICertificatePolicy Members
891
892 public bool CheckValidationResult(ServicePoint srvPoint, X509Certificate certificate, WebRequest request, int certificateProblem)
893 {
894 return true;
895 }
896
897 #endregion
898 }
899}
diff --git a/OpenSim/Region/OptionalModules/Avatar/Voice/VivoxVoice/VivoxVoiceModule.cs b/OpenSim/Region/OptionalModules/Avatar/Voice/VivoxVoice/VivoxVoiceModule.cs
new file mode 100644
index 0000000..dd44564
--- /dev/null
+++ b/OpenSim/Region/OptionalModules/Avatar/Voice/VivoxVoice/VivoxVoiceModule.cs
@@ -0,0 +1,1328 @@
1/*
2 * Copyright (c) Contributors, http://opensimulator.org/
3 * See CONTRIBUTORS.TXT for a full list of copyright holders.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
7 * * Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * * Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 * * Neither the name of the OpenSim Project nor the
13 * names of its contributors may be used to endorse or promote products
14 * derived from this software without specific prior written permission.
15 *
16 * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
17 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
20 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
23 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 */
27
28using System;
29using System.IO;
30using System.Net;
31using System.Text;
32using System.Xml;
33using System.Collections;
34using System.Collections.Generic;
35using System.Reflection;
36using System.Threading;
37using OpenMetaverse;
38using log4net;
39using Mono.Addins;
40using Nini.Config;
41using Nwc.XmlRpc;
42using OpenSim.Framework;
43
44using OpenSim.Framework.Capabilities;
45using OpenSim.Framework.Servers;
46using OpenSim.Framework.Servers.HttpServer;
47using OpenSim.Region.Framework.Interfaces;
48using OpenSim.Region.Framework.Scenes;
49using Caps = OpenSim.Framework.Capabilities.Caps;
50
51namespace OpenSim.Region.OptionalModules.Avatar.Voice.VivoxVoice
52{
53 [Extension(Path = "/OpenSim/RegionModules", NodeName = "RegionModule", Id = "VivoxVoiceModule")]
54 public class VivoxVoiceModule : ISharedRegionModule
55 {
56
57 // channel distance model values
58 public const int CHAN_DIST_NONE = 0; // no attenuation
59 public const int CHAN_DIST_INVERSE = 1; // inverse distance attenuation
60 public const int CHAN_DIST_LINEAR = 2; // linear attenuation
61 public const int CHAN_DIST_EXPONENT = 3; // exponential attenuation
62 public const int CHAN_DIST_DEFAULT = CHAN_DIST_LINEAR;
63
64 // channel type values
65 public static readonly string CHAN_TYPE_POSITIONAL = "positional";
66 public static readonly string CHAN_TYPE_CHANNEL = "channel";
67 public static readonly string CHAN_TYPE_DEFAULT = CHAN_TYPE_POSITIONAL;
68
69 // channel mode values
70 public static readonly string CHAN_MODE_OPEN = "open";
71 public static readonly string CHAN_MODE_LECTURE = "lecture";
72 public static readonly string CHAN_MODE_PRESENTATION = "presentation";
73 public static readonly string CHAN_MODE_AUDITORIUM = "auditorium";
74 public static readonly string CHAN_MODE_DEFAULT = CHAN_MODE_OPEN;
75
76 // unconstrained default values
77 public const double CHAN_ROLL_OFF_DEFAULT = 2.0; // rate of attenuation
78 public const double CHAN_ROLL_OFF_MIN = 1.0;
79 public const double CHAN_ROLL_OFF_MAX = 4.0;
80 public const int CHAN_MAX_RANGE_DEFAULT = 80; // distance at which channel is silent
81 public const int CHAN_MAX_RANGE_MIN = 0;
82 public const int CHAN_MAX_RANGE_MAX = 160;
83 public const int CHAN_CLAMPING_DISTANCE_DEFAULT = 10; // distance before attenuation applies
84 public const int CHAN_CLAMPING_DISTANCE_MIN = 0;
85 public const int CHAN_CLAMPING_DISTANCE_MAX = 160;
86
87 // Infrastructure
88 private static readonly ILog m_log =
89 LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
90 private static readonly Object vlock = new Object();
91
92 // Capability strings
93 private static readonly string m_parcelVoiceInfoRequestPath = "0107/";
94 private static readonly string m_provisionVoiceAccountRequestPath = "0108/";
95 //private static readonly string m_chatSessionRequestPath = "0109/";
96
97 // Control info, e.g. vivox server, admin user, admin password
98 private static bool m_pluginEnabled = false;
99 private static bool m_adminConnected = false;
100
101 private static string m_vivoxServer;
102 private static string m_vivoxSipUri;
103 private static string m_vivoxVoiceAccountApi;
104 private static string m_vivoxAdminUser;
105 private static string m_vivoxAdminPassword;
106 private static string m_authToken = String.Empty;
107
108 private static int m_vivoxChannelDistanceModel;
109 private static double m_vivoxChannelRollOff;
110 private static int m_vivoxChannelMaximumRange;
111 private static string m_vivoxChannelMode;
112 private static string m_vivoxChannelType;
113 private static int m_vivoxChannelClampingDistance;
114
115 private static Dictionary<string,string> m_parents = new Dictionary<string,string>();
116 private static bool m_dumpXml;
117
118 private IConfig m_config;
119
120 private object m_Lock;
121
122 public void Initialise(IConfigSource config)
123 {
124
125 m_config = config.Configs["VivoxVoice"];
126
127 if (null == m_config)
128 return;
129
130 if (!m_config.GetBoolean("enabled", false))
131 return;
132
133 m_Lock = new object();
134
135 try
136 {
137 // retrieve configuration variables
138 m_vivoxServer = m_config.GetString("vivox_server", String.Empty);
139 m_vivoxSipUri = m_config.GetString("vivox_sip_uri", String.Empty);
140 m_vivoxAdminUser = m_config.GetString("vivox_admin_user", String.Empty);
141 m_vivoxAdminPassword = m_config.GetString("vivox_admin_password", String.Empty);
142
143 m_vivoxChannelDistanceModel = m_config.GetInt("vivox_channel_distance_model", CHAN_DIST_DEFAULT);
144 m_vivoxChannelRollOff = m_config.GetDouble("vivox_channel_roll_off", CHAN_ROLL_OFF_DEFAULT);
145 m_vivoxChannelMaximumRange = m_config.GetInt("vivox_channel_max_range", CHAN_MAX_RANGE_DEFAULT);
146 m_vivoxChannelMode = m_config.GetString("vivox_channel_mode", CHAN_MODE_DEFAULT).ToLower();
147 m_vivoxChannelType = m_config.GetString("vivox_channel_type", CHAN_TYPE_DEFAULT).ToLower();
148 m_vivoxChannelClampingDistance = m_config.GetInt("vivox_channel_clamping_distance",
149 CHAN_CLAMPING_DISTANCE_DEFAULT);
150 m_dumpXml = m_config.GetBoolean("dump_xml", false);
151
152 // Validate against constraints and default if necessary
153 if (m_vivoxChannelRollOff < CHAN_ROLL_OFF_MIN || m_vivoxChannelRollOff > CHAN_ROLL_OFF_MAX)
154 {
155 m_log.WarnFormat("[VivoxVoice] Invalid value for roll off ({0}), reset to {1}.",
156 m_vivoxChannelRollOff, CHAN_ROLL_OFF_DEFAULT);
157 m_vivoxChannelRollOff = CHAN_ROLL_OFF_DEFAULT;
158 }
159
160 if (m_vivoxChannelMaximumRange < CHAN_MAX_RANGE_MIN || m_vivoxChannelMaximumRange > CHAN_MAX_RANGE_MAX)
161 {
162 m_log.WarnFormat("[VivoxVoice] Invalid value for maximum range ({0}), reset to {1}.",
163 m_vivoxChannelMaximumRange, CHAN_MAX_RANGE_DEFAULT);
164 m_vivoxChannelMaximumRange = CHAN_MAX_RANGE_DEFAULT;
165 }
166
167 if (m_vivoxChannelClampingDistance < CHAN_CLAMPING_DISTANCE_MIN ||
168 m_vivoxChannelClampingDistance > CHAN_CLAMPING_DISTANCE_MAX)
169 {
170 m_log.WarnFormat("[VivoxVoice] Invalid value for clamping distance ({0}), reset to {1}.",
171 m_vivoxChannelClampingDistance, CHAN_CLAMPING_DISTANCE_DEFAULT);
172 m_vivoxChannelClampingDistance = CHAN_CLAMPING_DISTANCE_DEFAULT;
173 }
174
175 switch (m_vivoxChannelMode)
176 {
177 case "open" : break;
178 case "lecture" : break;
179 case "presentation" : break;
180 case "auditorium" : break;
181 default :
182 m_log.WarnFormat("[VivoxVoice] Invalid value for channel mode ({0}), reset to {1}.",
183 m_vivoxChannelMode, CHAN_MODE_DEFAULT);
184 m_vivoxChannelMode = CHAN_MODE_DEFAULT;
185 break;
186 }
187
188 switch (m_vivoxChannelType)
189 {
190 case "positional" : break;
191 case "channel" : break;
192 default :
193 m_log.WarnFormat("[VivoxVoice] Invalid value for channel type ({0}), reset to {1}.",
194 m_vivoxChannelType, CHAN_TYPE_DEFAULT);
195 m_vivoxChannelType = CHAN_TYPE_DEFAULT;
196 break;
197 }
198
199 m_vivoxVoiceAccountApi = String.Format("http://{0}/api2", m_vivoxServer);
200
201 // Admin interface required values
202 if (String.IsNullOrEmpty(m_vivoxServer) ||
203 String.IsNullOrEmpty(m_vivoxSipUri) ||
204 String.IsNullOrEmpty(m_vivoxAdminUser) ||
205 String.IsNullOrEmpty(m_vivoxAdminPassword))
206 {
207 m_log.Error("[VivoxVoice] plugin mis-configured");
208 m_log.Info("[VivoxVoice] plugin disabled: incomplete configuration");
209 return;
210 }
211
212 m_log.InfoFormat("[VivoxVoice] using vivox server {0}", m_vivoxServer);
213
214 // Get admin rights and cleanup any residual channel definition
215
216 DoAdminLogin();
217
218 m_pluginEnabled = true;
219
220 m_log.Info("[VivoxVoice] plugin enabled");
221 }
222 catch (Exception e)
223 {
224 m_log.ErrorFormat("[VivoxVoice] plugin initialization failed: {0}", e.Message);
225 m_log.DebugFormat("[VivoxVoice] plugin initialization failed: {0}", e.ToString());
226 return;
227 }
228 }
229
230 public void AddRegion(Scene scene)
231 {
232 if (m_pluginEnabled)
233 {
234 lock (vlock)
235 {
236 string channelId;
237
238 string sceneUUID = scene.RegionInfo.RegionID.ToString();
239 string sceneName = scene.RegionInfo.RegionName;
240
241 // Make sure that all local channels are deleted.
242 // So we have to search for the children, and then do an
243 // iteration over the set of chidren identified.
244 // This assumes that there is just one directory per
245 // region.
246
247 if (VivoxTryGetDirectory(sceneUUID + "D", out channelId))
248 {
249 m_log.DebugFormat("[VivoxVoice]: region {0}: uuid {1}: located directory id {2}",
250 sceneName, sceneUUID, channelId);
251
252 XmlElement children = VivoxListChildren(channelId);
253 string count;
254
255 if (XmlFind(children, "response.level0.channel-search.count", out count))
256 {
257 int cnum = Convert.ToInt32(count);
258 for (int i = 0; i < cnum; i++)
259 {
260 string id;
261 if (XmlFind(children, "response.level0.channel-search.channels.channels.level4.id", i, out id))
262 {
263 if (!IsOK(VivoxDeleteChannel(channelId, id)))
264 m_log.WarnFormat("[VivoxVoice] Channel delete failed {0}:{1}:{2}", i, channelId, id);
265 }
266 }
267 }
268 }
269 else
270 {
271 if (!VivoxTryCreateDirectory(sceneUUID + "D", sceneName, out channelId))
272 {
273 m_log.WarnFormat("[VivoxVoice] Create failed <{0}:{1}:{2}>",
274 "*", sceneUUID, sceneName);
275 channelId = String.Empty;
276 }
277 }
278
279 // Create a dictionary entry unconditionally. This eliminates the
280 // need to check for a parent in the core code. The end result is
281 // the same, if the parent table entry is an empty string, then
282 // region channels will be created as first-level channels.
283 lock (m_parents)
284 {
285 if (m_parents.ContainsKey(sceneUUID))
286 {
287 RemoveRegion(scene);
288 m_parents.Add(sceneUUID, channelId);
289 }
290 else
291 {
292 m_parents.Add(sceneUUID, channelId);
293 }
294 }
295 }
296
297 // we need to capture scene in an anonymous method
298 // here as we need it later in the callbacks
299 scene.EventManager.OnRegisterCaps += delegate(UUID agentID, Caps caps)
300 {
301 OnRegisterCaps(scene, agentID, caps);
302 };
303 }
304 }
305
306 public void RegionLoaded(Scene scene)
307 {
308 // Do nothing.
309 }
310
311 public void RemoveRegion(Scene scene)
312 {
313 if (m_pluginEnabled)
314 {
315 lock (vlock)
316 {
317 string channelId;
318
319 string sceneUUID = scene.RegionInfo.RegionID.ToString();
320 string sceneName = scene.RegionInfo.RegionName;
321
322 // Make sure that all local channels are deleted.
323 // So we have to search for the children, and then do an
324 // iteration over the set of chidren identified.
325 // This assumes that there is just one directory per
326 // region.
327 if (VivoxTryGetDirectory(sceneUUID + "D", out channelId))
328 {
329 m_log.DebugFormat("[VivoxVoice]: region {0}: uuid {1}: located directory id {2}",
330 sceneName, sceneUUID, channelId);
331
332 XmlElement children = VivoxListChildren(channelId);
333 string count;
334
335 if (XmlFind(children, "response.level0.channel-search.count", out count))
336 {
337 int cnum = Convert.ToInt32(count);
338 for (int i = 0; i < cnum; i++)
339 {
340 string id;
341 if (XmlFind(children, "response.level0.channel-search.channels.channels.level4.id", i, out id))
342 {
343 if (!IsOK(VivoxDeleteChannel(channelId, id)))
344 m_log.WarnFormat("[VivoxVoice] Channel delete failed {0}:{1}:{2}", i, channelId, id);
345 }
346 }
347 }
348 }
349
350 if (!IsOK(VivoxDeleteChannel(null, channelId)))
351 m_log.WarnFormat("[VivoxVoice] Parent channel delete failed {0}:{1}:{2}", sceneName, sceneUUID, channelId);
352
353 // Remove the channel umbrella entry
354
355 lock (m_parents)
356 {
357 if (m_parents.ContainsKey(sceneUUID))
358 {
359 m_parents.Remove(sceneUUID);
360 }
361 }
362 }
363 }
364 }
365
366 public void PostInitialise()
367 {
368 // Do nothing.
369 }
370
371 public void Close()
372 {
373 if (m_pluginEnabled)
374 VivoxLogout();
375 }
376
377 public Type ReplaceableInterface
378 {
379 get { return null; }
380 }
381
382 public string Name
383 {
384 get { return "VivoxVoiceModule"; }
385 }
386
387 public bool IsSharedModule
388 {
389 get { return true; }
390 }
391
392 // <summary>
393 // OnRegisterCaps is invoked via the scene.EventManager
394 // everytime OpenSim hands out capabilities to a client
395 // (login, region crossing). We contribute two capabilities to
396 // the set of capabilities handed back to the client:
397 // ProvisionVoiceAccountRequest and ParcelVoiceInfoRequest.
398 //
399 // ProvisionVoiceAccountRequest allows the client to obtain
400 // the voice account credentials for the avatar it is
401 // controlling (e.g., user name, password, etc).
402 //
403 // ParcelVoiceInfoRequest is invoked whenever the client
404 // changes from one region or parcel to another.
405 //
406 // Note that OnRegisterCaps is called here via a closure
407 // delegate containing the scene of the respective region (see
408 // Initialise()).
409 // </summary>
410 public void OnRegisterCaps(Scene scene, UUID agentID, Caps caps)
411 {
412 m_log.DebugFormat("[VivoxVoice] OnRegisterCaps: agentID {0} caps {1}", agentID, caps);
413
414 string capsBase = "/CAPS/" + caps.CapsObjectPath;
415
416 caps.RegisterHandler(
417 "ProvisionVoiceAccountRequest",
418 new RestStreamHandler(
419 "POST",
420 capsBase + m_provisionVoiceAccountRequestPath,
421 (request, path, param, httpRequest, httpResponse)
422 => ProvisionVoiceAccountRequest(scene, request, path, param, agentID, caps),
423 "ProvisionVoiceAccountRequest",
424 agentID.ToString()));
425
426 caps.RegisterHandler(
427 "ParcelVoiceInfoRequest",
428 new RestStreamHandler(
429 "POST",
430 capsBase + m_parcelVoiceInfoRequestPath,
431 (request, path, param, httpRequest, httpResponse)
432 => ParcelVoiceInfoRequest(scene, request, path, param, agentID, caps),
433 "ParcelVoiceInfoRequest",
434 agentID.ToString()));
435
436 //caps.RegisterHandler(
437 // "ChatSessionRequest",
438 // new RestStreamHandler(
439 // "POST",
440 // capsBase + m_chatSessionRequestPath,
441 // (request, path, param, httpRequest, httpResponse)
442 // => ChatSessionRequest(scene, request, path, param, agentID, caps),
443 // "ChatSessionRequest",
444 // agentID.ToString()));
445 }
446
447 /// <summary>
448 /// Callback for a client request for Voice Account Details
449 /// </summary>
450 /// <param name="scene">current scene object of the client</param>
451 /// <param name="request"></param>
452 /// <param name="path"></param>
453 /// <param name="param"></param>
454 /// <param name="agentID"></param>
455 /// <param name="caps"></param>
456 /// <returns></returns>
457 public string ProvisionVoiceAccountRequest(Scene scene, string request, string path, string param,
458 UUID agentID, Caps caps)
459 {
460 try
461 {
462 ScenePresence avatar = null;
463 string avatarName = null;
464
465 if (scene == null)
466 throw new Exception("[VivoxVoice][PROVISIONVOICE]: Invalid scene");
467
468 avatar = scene.GetScenePresence(agentID);
469 while (avatar == null)
470 {
471 Thread.Sleep(100);
472 avatar = scene.GetScenePresence(agentID);
473 }
474
475 avatarName = avatar.Name;
476
477 m_log.DebugFormat("[VivoxVoice][PROVISIONVOICE]: scene = {0}, agentID = {1}", scene, agentID);
478 m_log.DebugFormat("[VivoxVoice][PROVISIONVOICE]: request: {0}, path: {1}, param: {2}",
479 request, path, param);
480
481 XmlElement resp;
482 bool retry = false;
483 string agentname = "x" + Convert.ToBase64String(agentID.GetBytes());
484 string password = new UUID(Guid.NewGuid()).ToString().Replace('-','Z').Substring(0,16);
485 string code = String.Empty;
486
487 agentname = agentname.Replace('+', '-').Replace('/', '_');
488
489 do
490 {
491 resp = VivoxGetAccountInfo(agentname);
492
493 if (XmlFind(resp, "response.level0.status", out code))
494 {
495 if (code != "OK")
496 {
497 if (XmlFind(resp, "response.level0.body.code", out code))
498 {
499 // If the request was recognized, then this should be set to something
500 switch (code)
501 {
502 case "201" : // Account expired
503 m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Get account information failed : expired credentials",
504 avatarName);
505 m_adminConnected = false;
506 retry = DoAdminLogin();
507 break;
508
509 case "202" : // Missing credentials
510 m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Get account information failed : missing credentials",
511 avatarName);
512 break;
513
514 case "212" : // Not authorized
515 m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Get account information failed : not authorized",
516 avatarName);
517 break;
518
519 case "300" : // Required parameter missing
520 m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Get account information failed : parameter missing",
521 avatarName);
522 break;
523
524 case "403" : // Account does not exist
525 resp = VivoxCreateAccount(agentname,password);
526 // Note: This REALLY MUST BE status. Create Account does not return code.
527 if (XmlFind(resp, "response.level0.status", out code))
528 {
529 switch (code)
530 {
531 case "201" : // Account expired
532 m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Create account information failed : expired credentials",
533 avatarName);
534 m_adminConnected = false;
535 retry = DoAdminLogin();
536 break;
537
538 case "202" : // Missing credentials
539 m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Create account information failed : missing credentials",
540 avatarName);
541 break;
542
543 case "212" : // Not authorized
544 m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Create account information failed : not authorized",
545 avatarName);
546 break;
547
548 case "300" : // Required parameter missing
549 m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Create account information failed : parameter missing",
550 avatarName);
551 break;
552
553 case "400" : // Create failed
554 m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Create account information failed : create failed",
555 avatarName);
556 break;
557 }
558 }
559 break;
560
561 case "404" : // Failed to retrieve account
562 m_log.ErrorFormat("[VivoxVoice]: avatar \"{0}\": Get account information failed : retrieve failed");
563 // [AMW] Sleep and retry for a fixed period? Or just abandon?
564 break;
565 }
566 }
567 }
568 }
569 }
570 while (retry);
571
572 if (code != "OK")
573 {
574 m_log.DebugFormat("[VivoxVoice][PROVISIONVOICE]: Get Account Request failed for \"{0}\"", avatarName);
575 throw new Exception("Unable to execute request");
576 }
577
578 // Unconditionally change the password on each request
579 VivoxPassword(agentname, password);
580
581 LLSDVoiceAccountResponse voiceAccountResponse =
582 new LLSDVoiceAccountResponse(agentname, password, m_vivoxSipUri, m_vivoxVoiceAccountApi);
583
584 string r = LLSDHelpers.SerialiseLLSDReply(voiceAccountResponse);
585
586 m_log.DebugFormat("[VivoxVoice][PROVISIONVOICE]: avatar \"{0}\": {1}", avatarName, r);
587
588 return r;
589 }
590 catch (Exception e)
591 {
592 m_log.ErrorFormat("[VivoxVoice][PROVISIONVOICE]: : {0}, retry later", e.Message);
593 m_log.DebugFormat("[VivoxVoice][PROVISIONVOICE]: : {0} failed", e.ToString());
594 return "<llsd><undef /></llsd>";
595 }
596 }
597
598 /// <summary>
599 /// Callback for a client request for ParcelVoiceInfo
600 /// </summary>
601 /// <param name="scene">current scene object of the client</param>
602 /// <param name="request"></param>
603 /// <param name="path"></param>
604 /// <param name="param"></param>
605 /// <param name="agentID"></param>
606 /// <param name="caps"></param>
607 /// <returns></returns>
608 public string ParcelVoiceInfoRequest(Scene scene, string request, string path, string param,
609 UUID agentID, Caps caps)
610 {
611 ScenePresence avatar = scene.GetScenePresence(agentID);
612 string avatarName = avatar.Name;
613
614 // - check whether we have a region channel in our cache
615 // - if not:
616 // create it and cache it
617 // - send it to the client
618 // - send channel_uri: as "sip:regionID@m_sipDomain"
619 try
620 {
621 LLSDParcelVoiceInfoResponse parcelVoiceInfo;
622 string channel_uri;
623
624 if (null == scene.LandChannel)
625 throw new Exception(String.Format("region \"{0}\": avatar \"{1}\": land data not yet available",
626 scene.RegionInfo.RegionName, avatarName));
627
628 // get channel_uri: check first whether estate
629 // settings allow voice, then whether parcel allows
630 // voice, if all do retrieve or obtain the parcel
631 // voice channel
632 LandData land = scene.GetLandData(avatar.AbsolutePosition);
633
634 m_log.DebugFormat("[VivoxVoice][PARCELVOICE]: region \"{0}\": Parcel \"{1}\" ({2}): avatar \"{3}\": request: {4}, path: {5}, param: {6}",
635 scene.RegionInfo.RegionName, land.Name, land.LocalID, avatarName, request, path, param);
636 // m_log.DebugFormat("[VivoxVoice][PARCELVOICE]: avatar \"{0}\": location: {1} {2} {3}",
637 // avatarName, avatar.AbsolutePosition.X, avatar.AbsolutePosition.Y, avatar.AbsolutePosition.Z);
638
639 // TODO: EstateSettings don't seem to get propagated...
640 if (!scene.RegionInfo.EstateSettings.AllowVoice)
641 {
642 m_log.DebugFormat("[VivoxVoice][PARCELVOICE]: region \"{0}\": voice not enabled in estate settings",
643 scene.RegionInfo.RegionName);
644 channel_uri = String.Empty;
645 }
646
647 if ((land.Flags & (uint)ParcelFlags.AllowVoiceChat) == 0)
648 {
649 m_log.DebugFormat("[VivoxVoice][PARCELVOICE]: region \"{0}\": Parcel \"{1}\" ({2}): avatar \"{3}\": voice not enabled for parcel",
650 scene.RegionInfo.RegionName, land.Name, land.LocalID, avatarName);
651 channel_uri = String.Empty;
652 }
653 else
654 {
655 channel_uri = RegionGetOrCreateChannel(scene, land);
656 }
657
658 // fill in our response to the client
659 Hashtable creds = new Hashtable();
660 creds["channel_uri"] = channel_uri;
661
662 parcelVoiceInfo = new LLSDParcelVoiceInfoResponse(scene.RegionInfo.RegionName, land.LocalID, creds);
663 string r = LLSDHelpers.SerialiseLLSDReply(parcelVoiceInfo);
664
665 m_log.DebugFormat("[VivoxVoice][PARCELVOICE]: region \"{0}\": Parcel \"{1}\" ({2}): avatar \"{3}\": {4}",
666 scene.RegionInfo.RegionName, land.Name, land.LocalID, avatarName, r);
667 return r;
668 }
669 catch (Exception e)
670 {
671 m_log.ErrorFormat("[VivoxVoice][PARCELVOICE]: region \"{0}\": avatar \"{1}\": {2}, retry later",
672 scene.RegionInfo.RegionName, avatarName, e.Message);
673 m_log.DebugFormat("[VivoxVoice][PARCELVOICE]: region \"{0}\": avatar \"{1}\": {2} failed",
674 scene.RegionInfo.RegionName, avatarName, e.ToString());
675
676 return "<llsd><undef /></llsd>";
677 }
678 }
679
680 /// <summary>
681 /// Callback for a client request for a private chat channel
682 /// </summary>
683 /// <param name="scene">current scene object of the client</param>
684 /// <param name="request"></param>
685 /// <param name="path"></param>
686 /// <param name="param"></param>
687 /// <param name="agentID"></param>
688 /// <param name="caps"></param>
689 /// <returns></returns>
690 public string ChatSessionRequest(Scene scene, string request, string path, string param,
691 UUID agentID, Caps caps)
692 {
693 ScenePresence avatar = scene.GetScenePresence(agentID);
694 string avatarName = avatar.Name;
695
696 m_log.DebugFormat("[VivoxVoice][CHATSESSION]: avatar \"{0}\": request: {1}, path: {2}, param: {3}",
697 avatarName, request, path, param);
698 return "<llsd>true</llsd>";
699 }
700
701 private string RegionGetOrCreateChannel(Scene scene, LandData land)
702 {
703 string channelUri = null;
704 string channelId = null;
705
706 string landUUID;
707 string landName;
708 string parentId;
709
710 lock (m_parents)
711 parentId = m_parents[scene.RegionInfo.RegionID.ToString()];
712
713 // Create parcel voice channel. If no parcel exists, then the voice channel ID is the same
714 // as the directory ID. Otherwise, it reflects the parcel's ID.
715 if (land.LocalID != 1 && (land.Flags & (uint)ParcelFlags.UseEstateVoiceChan) == 0)
716 {
717 landName = String.Format("{0}:{1}", scene.RegionInfo.RegionName, land.Name);
718 landUUID = land.GlobalID.ToString();
719 m_log.DebugFormat("[VivoxVoice]: Region:Parcel \"{0}\": parcel id {1}: using channel name {2}",
720 landName, land.LocalID, landUUID);
721 }
722 else
723 {
724 landName = String.Format("{0}:{1}", scene.RegionInfo.RegionName, scene.RegionInfo.RegionName);
725 landUUID = scene.RegionInfo.RegionID.ToString();
726 m_log.DebugFormat("[VivoxVoice]: Region:Parcel \"{0}\": parcel id {1}: using channel name {2}",
727 landName, land.LocalID, landUUID);
728 }
729
730 lock (vlock)
731 {
732 // Added by Adam to help debug channel not availible errors.
733 if (VivoxTryGetChannel(parentId, landUUID, out channelId, out channelUri))
734 m_log.DebugFormat("[VivoxVoice] Found existing channel at " + channelUri);
735 else if (VivoxTryCreateChannel(parentId, landUUID, landName, out channelUri))
736 m_log.DebugFormat("[VivoxVoice] Created new channel at " + channelUri);
737 else
738 throw new Exception("vivox channel uri not available");
739
740 m_log.DebugFormat("[VivoxVoice]: Region:Parcel \"{0}\": parent channel id {1}: retrieved parcel channel_uri {2} ",
741 landName, parentId, channelUri);
742 }
743
744 return channelUri;
745 }
746
747 private static readonly string m_vivoxLoginPath = "http://{0}/api2/viv_signin.php?userid={1}&pwd={2}";
748
749 /// <summary>
750 /// Perform administrative login for Vivox.
751 /// Returns a hash table containing values returned from the request.
752 /// </summary>
753 private XmlElement VivoxLogin(string name, string password)
754 {
755 string requrl = String.Format(m_vivoxLoginPath, m_vivoxServer, name, password);
756 return VivoxCall(requrl, false);
757 }
758
759 private static readonly string m_vivoxLogoutPath = "http://{0}/api2/viv_signout.php?auth_token={1}";
760
761 /// <summary>
762 /// Perform administrative logout for Vivox.
763 /// </summary>
764 private XmlElement VivoxLogout()
765 {
766 string requrl = String.Format(m_vivoxLogoutPath, m_vivoxServer, m_authToken);
767 return VivoxCall(requrl, false);
768 }
769
770 private static readonly string m_vivoxGetAccountPath = "http://{0}/api2/viv_get_acct.php?auth_token={1}&user_name={2}";
771
772 /// <summary>
773 /// Retrieve account information for the specified user.
774 /// Returns a hash table containing values returned from the request.
775 /// </summary>
776 private XmlElement VivoxGetAccountInfo(string user)
777 {
778 string requrl = String.Format(m_vivoxGetAccountPath, m_vivoxServer, m_authToken, user);
779 return VivoxCall(requrl, true);
780 }
781
782 private static readonly string m_vivoxNewAccountPath = "http://{0}/api2/viv_adm_acct_new.php?username={1}&pwd={2}&auth_token={3}";
783
784 /// <summary>
785 /// Creates a new account.
786 /// For now we supply the minimum set of values, which
787 /// is user name and password. We *can* supply a lot more
788 /// demographic data.
789 /// </summary>
790 private XmlElement VivoxCreateAccount(string user, string password)
791 {
792 string requrl = String.Format(m_vivoxNewAccountPath, m_vivoxServer, user, password, m_authToken);
793 return VivoxCall(requrl, true);
794 }
795
796 private static readonly string m_vivoxPasswordPath = "http://{0}/api2/viv_adm_password.php?user_name={1}&new_pwd={2}&auth_token={3}";
797
798 /// <summary>
799 /// Change the user's password.
800 /// </summary>
801 private XmlElement VivoxPassword(string user, string password)
802 {
803 string requrl = String.Format(m_vivoxPasswordPath, m_vivoxServer, user, password, m_authToken);
804 return VivoxCall(requrl, true);
805 }
806
807 private static readonly string m_vivoxChannelPath = "http://{0}/api2/viv_chan_mod.php?mode={1}&chan_name={2}&auth_token={3}";
808
809 /// <summary>
810 /// Create a channel.
811 /// Once again, there a multitude of options possible. In the simplest case
812 /// we specify only the name and get a non-persistent cannel in return. Non
813 /// persistent means that the channel gets deleted if no-one uses it for
814 /// 5 hours. To accomodate future requirements, it may be a good idea to
815 /// initially create channels under the umbrella of a parent ID based upon
816 /// the region name. That way we have a context for side channels, if those
817 /// are required in a later phase.
818 ///
819 /// In this case the call handles parent and description as optional values.
820 /// </summary>
821 private bool VivoxTryCreateChannel(string parent, string channelId, string description, out string channelUri)
822 {
823 string requrl = String.Format(m_vivoxChannelPath, m_vivoxServer, "create", channelId, m_authToken);
824
825 if (!string.IsNullOrEmpty(parent))
826 {
827 requrl = String.Format("{0}&chan_parent={1}", requrl, parent);
828 }
829 if (!string.IsNullOrEmpty(description))
830 {
831 requrl = String.Format("{0}&chan_desc={1}", requrl, description);
832 }
833
834 requrl = String.Format("{0}&chan_type={1}", requrl, m_vivoxChannelType);
835 requrl = String.Format("{0}&chan_mode={1}", requrl, m_vivoxChannelMode);
836 requrl = String.Format("{0}&chan_roll_off={1}", requrl, m_vivoxChannelRollOff);
837 requrl = String.Format("{0}&chan_dist_model={1}", requrl, m_vivoxChannelDistanceModel);
838 requrl = String.Format("{0}&chan_max_range={1}", requrl, m_vivoxChannelMaximumRange);
839 requrl = String.Format("{0}&chan_clamping_distance={1}", requrl, m_vivoxChannelClampingDistance);
840
841 XmlElement resp = VivoxCall(requrl, true);
842 if (XmlFind(resp, "response.level0.body.chan_uri", out channelUri))
843 return true;
844
845 channelUri = String.Empty;
846 return false;
847 }
848
849 /// <summary>
850 /// Create a directory.
851 /// Create a channel with an unconditional type of "dir" (indicating directory).
852 /// This is used to create an arbitrary name tree for partitioning of the
853 /// channel name space.
854 /// The parent and description are optional values.
855 /// </summary>
856 private bool VivoxTryCreateDirectory(string dirId, string description, out string channelId)
857 {
858 string requrl = String.Format(m_vivoxChannelPath, m_vivoxServer, "create", dirId, m_authToken);
859
860 // if (parent != null && parent != String.Empty)
861 // {
862 // requrl = String.Format("{0}&chan_parent={1}", requrl, parent);
863 // }
864
865 if (!string.IsNullOrEmpty(description))
866 {
867 requrl = String.Format("{0}&chan_desc={1}", requrl, description);
868 }
869 requrl = String.Format("{0}&chan_type={1}", requrl, "dir");
870
871 XmlElement resp = VivoxCall(requrl, true);
872 if (IsOK(resp) && XmlFind(resp, "response.level0.body.chan_id", out channelId))
873 return true;
874
875 channelId = String.Empty;
876 return false;
877 }
878
879 private static readonly string m_vivoxChannelSearchPath = "http://{0}/api2/viv_chan_search.php?cond_channame={1}&auth_token={2}";
880
881 /// <summary>
882 /// Retrieve a channel.
883 /// Once again, there a multitude of options possible. In the simplest case
884 /// we specify only the name and get a non-persistent cannel in return. Non
885 /// persistent means that the channel gets deleted if no-one uses it for
886 /// 5 hours. To accomodate future requirements, it may be a good idea to
887 /// initially create channels under the umbrella of a parent ID based upon
888 /// the region name. That way we have a context for side channels, if those
889 /// are required in a later phase.
890 /// In this case the call handles parent and description as optional values.
891 /// </summary>
892 private bool VivoxTryGetChannel(string channelParent, string channelName,
893 out string channelId, out string channelUri)
894 {
895 string count;
896
897 string requrl = String.Format(m_vivoxChannelSearchPath, m_vivoxServer, channelName, m_authToken);
898 XmlElement resp = VivoxCall(requrl, true);
899
900 if (XmlFind(resp, "response.level0.channel-search.count", out count))
901 {
902 int channels = Convert.ToInt32(count);
903
904 // Bug in Vivox Server r2978 where count returns 0
905 // Found by Adam
906 if (channels == 0)
907 {
908 for (int j=0;j<100;j++)
909 {
910 string tmpId;
911 if (!XmlFind(resp, "response.level0.channel-search.channels.channels.level4.id", j, out tmpId))
912 break;
913
914 channels = j + 1;
915 }
916 }
917
918 for (int i = 0; i < channels; i++)
919 {
920 string name;
921 string id;
922 string type;
923 string uri;
924 string parent;
925
926 // skip if not a channel
927 if (!XmlFind(resp, "response.level0.channel-search.channels.channels.level4.type", i, out type) ||
928 (type != "channel" && type != "positional_M"))
929 {
930 m_log.Debug("[VivoxVoice] Skipping Channel " + i + " as it's not a channel.");
931 continue;
932 }
933
934 // skip if not the name we are looking for
935 if (!XmlFind(resp, "response.level0.channel-search.channels.channels.level4.name", i, out name) ||
936 name != channelName)
937 {
938 m_log.Debug("[VivoxVoice] Skipping Channel " + i + " as it has no name.");
939 continue;
940 }
941
942 // skip if parent does not match
943 if (channelParent != null && !XmlFind(resp, "response.level0.channel-search.channels.channels.level4.parent", i, out parent))
944 {
945 m_log.Debug("[VivoxVoice] Skipping Channel " + i + "/" + name + " as it's parent doesnt match");
946 continue;
947 }
948
949 // skip if no channel id available
950 if (!XmlFind(resp, "response.level0.channel-search.channels.channels.level4.id", i, out id))
951 {
952 m_log.Debug("[VivoxVoice] Skipping Channel " + i + "/" + name + " as it has no channel ID");
953 continue;
954 }
955
956 // skip if no channel uri available
957 if (!XmlFind(resp, "response.level0.channel-search.channels.channels.level4.uri", i, out uri))
958 {
959 m_log.Debug("[VivoxVoice] Skipping Channel " + i + "/" + name + " as it has no channel URI");
960 continue;
961 }
962
963 channelId = id;
964 channelUri = uri;
965
966 return true;
967 }
968 }
969 else
970 {
971 m_log.Debug("[VivoxVoice] No count element?");
972 }
973
974 channelId = String.Empty;
975 channelUri = String.Empty;
976
977 // Useful incase something goes wrong.
978 //m_log.Debug("[VivoxVoice] Could not find channel in XMLRESP: " + resp.InnerXml);
979
980 return false;
981 }
982
983 private bool VivoxTryGetDirectory(string directoryName, out string directoryId)
984 {
985 string count;
986
987 string requrl = String.Format(m_vivoxChannelSearchPath, m_vivoxServer, directoryName, m_authToken);
988 XmlElement resp = VivoxCall(requrl, true);
989
990 if (XmlFind(resp, "response.level0.channel-search.count", out count))
991 {
992 int channels = Convert.ToInt32(count);
993 for (int i = 0; i < channels; i++)
994 {
995 string name;
996 string id;
997 string type;
998
999 // skip if not a directory
1000 if (!XmlFind(resp, "response.level0.channel-search.channels.channels.level4.type", i, out type) ||
1001 type != "dir")
1002 continue;
1003
1004 // skip if not the name we are looking for
1005 if (!XmlFind(resp, "response.level0.channel-search.channels.channels.level4.name", i, out name) ||
1006 name != directoryName)
1007 continue;
1008
1009 // skip if no channel id available
1010 if (!XmlFind(resp, "response.level0.channel-search.channels.channels.level4.id", i, out id))
1011 continue;
1012
1013 directoryId = id;
1014 return true;
1015 }
1016 }
1017
1018 directoryId = String.Empty;
1019 return false;
1020 }
1021
1022 // private static readonly string m_vivoxChannelById = "http://{0}/api2/viv_chan_mod.php?mode={1}&chan_id={2}&auth_token={3}";
1023
1024 // private XmlElement VivoxGetChannelById(string parent, string channelid)
1025 // {
1026 // string requrl = String.Format(m_vivoxChannelById, m_vivoxServer, "get", channelid, m_authToken);
1027
1028 // if (parent != null && parent != String.Empty)
1029 // return VivoxGetChild(parent, channelid);
1030 // else
1031 // return VivoxCall(requrl, true);
1032 // }
1033
1034 private static readonly string m_vivoxChannelDel = "http://{0}/api2/viv_chan_mod.php?mode={1}&chan_id={2}&auth_token={3}";
1035
1036 /// <summary>
1037 /// Delete a channel.
1038 /// Once again, there a multitude of options possible. In the simplest case
1039 /// we specify only the name and get a non-persistent cannel in return. Non
1040 /// persistent means that the channel gets deleted if no-one uses it for
1041 /// 5 hours. To accomodate future requirements, it may be a good idea to
1042 /// initially create channels under the umbrella of a parent ID based upon
1043 /// the region name. That way we have a context for side channels, if those
1044 /// are required in a later phase.
1045 /// In this case the call handles parent and description as optional values.
1046 /// </summary>
1047 private XmlElement VivoxDeleteChannel(string parent, string channelid)
1048 {
1049 string requrl = String.Format(m_vivoxChannelDel, m_vivoxServer, "delete", channelid, m_authToken);
1050 if (!string.IsNullOrEmpty(parent))
1051 {
1052 requrl = String.Format("{0}&chan_parent={1}", requrl, parent);
1053 }
1054 return VivoxCall(requrl, true);
1055 }
1056
1057 private static readonly string m_vivoxChannelSearch = "http://{0}/api2/viv_chan_search.php?&cond_chanparent={1}&auth_token={2}";
1058
1059 /// <summary>
1060 /// Return information on channels in the given directory
1061 /// </summary>
1062 private XmlElement VivoxListChildren(string channelid)
1063 {
1064 string requrl = String.Format(m_vivoxChannelSearch, m_vivoxServer, channelid, m_authToken);
1065 return VivoxCall(requrl, true);
1066 }
1067
1068 // private XmlElement VivoxGetChild(string parent, string child)
1069 // {
1070
1071 // XmlElement children = VivoxListChildren(parent);
1072 // string count;
1073
1074 // if (XmlFind(children, "response.level0.channel-search.count", out count))
1075 // {
1076 // int cnum = Convert.ToInt32(count);
1077 // for (int i = 0; i < cnum; i++)
1078 // {
1079 // string name;
1080 // string id;
1081 // if (XmlFind(children, "response.level0.channel-search.channels.channels.level4.name", i, out name))
1082 // {
1083 // if (name == child)
1084 // {
1085 // if (XmlFind(children, "response.level0.channel-search.channels.channels.level4.id", i, out id))
1086 // {
1087 // return VivoxGetChannelById(null, id);
1088 // }
1089 // }
1090 // }
1091 // }
1092 // }
1093
1094 // // One we *know* does not exist.
1095 // return VivoxGetChannel(null, Guid.NewGuid().ToString());
1096
1097 // }
1098
1099 /// <summary>
1100 /// This method handles the WEB side of making a request over the
1101 /// Vivox interface. The returned values are tansferred to a has
1102 /// table which is returned as the result.
1103 /// The outcome of the call can be determined by examining the
1104 /// status value in the hash table.
1105 /// </summary>
1106 private XmlElement VivoxCall(string requrl, bool admin)
1107 {
1108
1109 XmlDocument doc = null;
1110
1111 // If this is an admin call, and admin is not connected,
1112 // and the admin id cannot be connected, then fail.
1113 if (admin && !m_adminConnected && !DoAdminLogin())
1114 return null;
1115
1116 doc = new XmlDocument();
1117
1118 // Let's serialize all calls to Vivox. Most of these are driven by
1119 // the clients (CAPs), when the user arrives at the region. We don't
1120 // want to issue many simultaneous http requests to Vivox, because mono
1121 // doesn't like that
1122 lock (m_Lock)
1123 {
1124 try
1125 {
1126 // Otherwise prepare the request
1127 m_log.DebugFormat("[VivoxVoice] Sending request <{0}>", requrl);
1128
1129 HttpWebRequest req = (HttpWebRequest)WebRequest.Create(requrl);
1130
1131 // We are sending just parameters, no content
1132 req.ContentLength = 0;
1133
1134 // Send request and retrieve the response
1135 using (HttpWebResponse rsp = (HttpWebResponse)req.GetResponse())
1136 using (Stream s = rsp.GetResponseStream())
1137 using (XmlTextReader rdr = new XmlTextReader(s))
1138 doc.Load(rdr);
1139 }
1140 catch (Exception e)
1141 {
1142 m_log.ErrorFormat("[VivoxVoice] Error in admin call : {0}", e.Message);
1143 }
1144 }
1145
1146 // If we're debugging server responses, dump the whole
1147 // load now
1148 if (m_dumpXml) XmlScanl(doc.DocumentElement,0);
1149
1150 return doc.DocumentElement;
1151 }
1152
1153 /// <summary>
1154 /// Just say if it worked.
1155 /// </summary>
1156 private bool IsOK(XmlElement resp)
1157 {
1158 string status;
1159 XmlFind(resp, "response.level0.status", out status);
1160 return (status == "OK");
1161 }
1162
1163 /// <summary>
1164 /// Login has been factored in this way because it gets called
1165 /// from several places in the module, and we want it to work
1166 /// the same way each time.
1167 /// </summary>
1168 private bool DoAdminLogin()
1169 {
1170 m_log.Debug("[VivoxVoice] Establishing admin connection");
1171
1172 lock (vlock)
1173 {
1174 if (!m_adminConnected)
1175 {
1176 string status = "Unknown";
1177 XmlElement resp = null;
1178
1179 resp = VivoxLogin(m_vivoxAdminUser, m_vivoxAdminPassword);
1180
1181 if (XmlFind(resp, "response.level0.body.status", out status))
1182 {
1183 if (status == "Ok")
1184 {
1185 m_log.Info("[VivoxVoice] Admin connection established");
1186 if (XmlFind(resp, "response.level0.body.auth_token", out m_authToken))
1187 {
1188 if (m_dumpXml) m_log.DebugFormat("[VivoxVoice] Auth Token <{0}>",
1189 m_authToken);
1190 m_adminConnected = true;
1191 }
1192 }
1193 else
1194 {
1195 m_log.WarnFormat("[VivoxVoice] Admin connection failed, status = {0}",
1196 status);
1197 }
1198 }
1199 }
1200 }
1201
1202 return m_adminConnected;
1203 }
1204
1205 /// <summary>
1206 /// The XmlScan routine is provided to aid in the
1207 /// reverse engineering of incompletely
1208 /// documented packets returned by the Vivox
1209 /// voice server. It is only called if the
1210 /// m_dumpXml switch is set.
1211 /// </summary>
1212 private void XmlScanl(XmlElement e, int index)
1213 {
1214 if (e.HasChildNodes)
1215 {
1216 m_log.DebugFormat("<{0}>".PadLeft(index+5), e.Name);
1217 XmlNodeList children = e.ChildNodes;
1218 foreach (XmlNode node in children)
1219 switch (node.NodeType)
1220 {
1221 case XmlNodeType.Element :
1222 XmlScanl((XmlElement)node, index+1);
1223 break;
1224 case XmlNodeType.Text :
1225 m_log.DebugFormat("\"{0}\"".PadLeft(index+5), node.Value);
1226 break;
1227 default :
1228 break;
1229 }
1230 m_log.DebugFormat("</{0}>".PadLeft(index+6), e.Name);
1231 }
1232 else
1233 {
1234 m_log.DebugFormat("<{0}/>".PadLeft(index+6), e.Name);
1235 }
1236 }
1237
1238 private static readonly char[] C_POINT = {'.'};
1239
1240 /// <summary>
1241 /// The Find method is passed an element whose
1242 /// inner text is scanned in an attempt to match
1243 /// the name hierarchy passed in the 'tag' parameter.
1244 /// If the whole hierarchy is resolved, the InnerText
1245 /// value at that point is returned. Note that this
1246 /// may itself be a subhierarchy of the entire
1247 /// document. The function returns a boolean indicator
1248 /// of the search's success. The search is performed
1249 /// by the recursive Search method.
1250 /// </summary>
1251 private bool XmlFind(XmlElement root, string tag, int nth, out string result)
1252 {
1253 if (root == null || tag == null || tag == String.Empty)
1254 {
1255 result = String.Empty;
1256 return false;
1257 }
1258 return XmlSearch(root,tag.Split(C_POINT),0, ref nth, out result);
1259 }
1260
1261 private bool XmlFind(XmlElement root, string tag, out string result)
1262 {
1263 int nth = 0;
1264 if (root == null || tag == null || tag == String.Empty)
1265 {
1266 result = String.Empty;
1267 return false;
1268 }
1269 return XmlSearch(root,tag.Split(C_POINT),0, ref nth, out result);
1270 }
1271
1272 /// <summary>
1273 /// XmlSearch is initially called by XmlFind, and then
1274 /// recursively called by itself until the document
1275 /// supplied to XmlFind is either exhausted or the name hierarchy
1276 /// is matched.
1277 ///
1278 /// If the hierarchy is matched, the value is returned in
1279 /// result, and true returned as the function's
1280 /// value. Otherwise the result is set to the empty string and
1281 /// false is returned.
1282 /// </summary>
1283 private bool XmlSearch(XmlElement e, string[] tags, int index, ref int nth, out string result)
1284 {
1285 if (index == tags.Length || e.Name != tags[index])
1286 {
1287 result = String.Empty;
1288 return false;
1289 }
1290
1291 if (tags.Length-index == 1)
1292 {
1293 if (nth == 0)
1294 {
1295 result = e.InnerText;
1296 return true;
1297 }
1298 else
1299 {
1300 nth--;
1301 result = String.Empty;
1302 return false;
1303 }
1304 }
1305
1306 if (e.HasChildNodes)
1307 {
1308 XmlNodeList children = e.ChildNodes;
1309 foreach (XmlNode node in children)
1310 {
1311 switch (node.NodeType)
1312 {
1313 case XmlNodeType.Element :
1314 if (XmlSearch((XmlElement)node, tags, index+1, ref nth, out result))
1315 return true;
1316 break;
1317
1318 default :
1319 break;
1320 }
1321 }
1322 }
1323
1324 result = String.Empty;
1325 return false;
1326 }
1327 }
1328}