PageRenderTime 56ms CodeModel.GetById 22ms RepoModel.GetById 1ms app.codeStats 0ms

/sipsorcery-core/SIPSorcery.AppServer.DialPlan/DialPlanEngine.cs

https://github.com/thecc4re/sipsorcery-mono
C# | 1020 lines | 778 code | 147 blank | 95 comment | 83 complexity | 400a14386c941e4a4968808b3ad04659 MD5 | raw file
Possible License(s): CC-BY-SA-3.0

Large files files are truncated, but you can click here to view the full file

  1. // ============================================================================
  2. // FileName: DialPlanEngine.cs
  3. //
  4. // Description:
  5. // Converts an Asterisk like SIP dial plan into a useable form for the Proxy server.
  6. //
  7. // Author(s):
  8. // Aaron Clauson
  9. //
  10. // History:
  11. // ?? 2006 Aaron Clauson Created.
  12. // 26 Aug 2007 Aaron Clauson Added the ability to set the From header and the send to socket in a dial plan command.
  13. // 08 Feb 2008 Aaron Clauson Added SIP Providers to dial plan to facilitate multi-legged forwards in the Switch command.
  14. // 16 Feb 2008 Aaron Clauson Added capability for Ruby scripted dial plans.
  15. // 28 Sep 2008 Aaron Clauson Renamed from SIPDialPlan to SIPDialPlanEngine.
  16. // 11 Jun 2010 Aaron Clauson Added per user execution count property.
  17. //
  18. // License:
  19. // This software is licensed under the BSD License http://www.opensource.org/licenses/bsd-license.php
  20. //
  21. // Copyright (c) 2010 Aaron Clauson (aaron@sipsorcery.com), SIP Sorcery Ltd, Hobart, Australia (www.sipsorcery.com)
  22. // All rights reserved.
  23. //
  24. // Redistribution and use in source and binary forms, with or without modification, are permitted provided that
  25. // the following conditions are met:
  26. //
  27. // Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
  28. // Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
  29. // disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of Blue Face Ltd.
  30. // nor the names of its contributors may be used to endorse or promote products derived from this software without specific
  31. // prior written permission.
  32. //
  33. // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
  34. // BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
  35. // IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
  36. // OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
  37. // OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
  38. // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  39. // POSSIBILITY OF SUCH DAMAGE.
  40. // ============================================================================
  41. using System;
  42. using System.Collections;
  43. using System.Collections.Generic;
  44. using System.IO;
  45. using System.Linq;
  46. using System.Net;
  47. using System.Net.Sockets;
  48. using System.Security;
  49. using System.Security.Principal;
  50. using System.Text;
  51. using System.Text.RegularExpressions;
  52. using System.Threading;
  53. using System.Transactions;
  54. using SIPSorcery.CRM;
  55. using SIPSorcery.Persistence;
  56. using SIPSorcery.SIP;
  57. using SIPSorcery.SIP.App;
  58. using SIPSorcery.Sys;
  59. using log4net;
  60. using Microsoft.Scripting;
  61. using Microsoft.Scripting.Interpreter;
  62. using Microsoft.Scripting.Hosting;
  63. using IronRuby;
  64. #if UNITTEST
  65. using NUnit.Framework;
  66. #endif
  67. namespace SIPSorcery.AppServer.DialPlan
  68. {
  69. /// <summary>
  70. /// Dial plan is in the form:
  71. ///
  72. /// exten = 100,1,Switch(anonymous.invalid,,612@freeworlddialup.com)
  73. /// exten =~ 101,1,Switch(anonymous.invalid,,303@sip.blueface.ie)
  74. ///
  75. /// </summary>
  76. public class DialPlanEngine
  77. {
  78. private const string MONITOR_THREAD_NAME = "dialplanengine-monitor";
  79. public const string SCRIPT_REQUESTOBJECT_NAME = "req"; // Access using $req from the Ruby script.
  80. public const string SCRIPT_HELPEROBJECT_NAME = "sys"; // Access using $sys from the Ruby script.
  81. public const string SCRIPT_CRMOBJECT_NAME = "crm"; // Access using $crm from the Ruby script.
  82. public const string SCRIPT_LOOKUPOBJECT_NAME = "lookup"; // Access using $lookup from the Ruby script.
  83. public const int ABSOLUTEMAX_SCRIPTPROCESSING_SECONDS = 300; // The absolute maximum amount of seconds a script thread will be allowed to execute for.
  84. public const int MAX_ALLOWED_SCRIPTSCOPES = 20; // The maximum allowed number of scopes one if which is required for each simultaneously executing script.
  85. private const string RUBY_COMMON_COPY_EXTEN = ".tmp";
  86. private const int RUBY_COMMON_RELOAD_INTERVAL = 2;
  87. private static ILog logger;
  88. //private ScriptEngine m_scriptEngine;
  89. private List<DialPlanExecutingScript> m_runningScripts = new List<DialPlanExecutingScript>();
  90. private int m_dialPlanScriptContextsCreated;
  91. public bool StopScriptMonitoring = false; // Set to true to stop the thread keeping an eye on long running dial plan scripts.
  92. public int ScriptCount
  93. {
  94. get { return m_runningScripts.Count; }
  95. }
  96. private SIPTransport m_sipTransport;
  97. private SIPEndPoint m_outboundProxySocket; // If this app forwards calls via an outbound proxy this value will be set.
  98. private SIPMonitorLogDelegate LogDelegate_External; // Delegate from proxy core to fire when log messages should be bubbled up to the core.
  99. private SIPAssetPersistor<SIPAccount> m_sipAccountPersistor;
  100. private SIPAssetPersistor<SIPDialogueAsset> m_sipDialoguePersistor;
  101. private SIPAssetGetListDelegate<SIPRegistrarBinding> GetSIPAccountBindings_External;
  102. private GetCanonicalDomainDelegate GetCanonicalDomainDelegate_External;
  103. private SIPAssetPersistor<SIPDialPlan> m_dialPlanPersistor;
  104. private string m_impersonationUsername;
  105. private string m_impersonationPassword;
  106. private DateTime? m_rubyCommonLastReload;
  107. private string m_rubyScriptCommonPath;
  108. private string m_rubyScriptCommon;
  109. static DialPlanEngine()
  110. {
  111. logger = AppState.GetLogger("dialplanengine");
  112. }
  113. public DialPlanEngine(
  114. SIPTransport sipTransport,
  115. GetCanonicalDomainDelegate getCanonicalDomain,
  116. SIPMonitorLogDelegate logDelegate,
  117. SIPAssetPersistor<SIPAccount> sipAssetPersistor,
  118. SIPAssetGetListDelegate<SIPRegistrarBinding> getBindings,
  119. SIPAssetPersistor<SIPDialPlan> dialPlanPersistor,
  120. SIPAssetPersistor<SIPDialogueAsset> sipDialoguePersistor,
  121. SIPEndPoint outboundProxySocket,
  122. string rubyScriptCommonPath,
  123. string impersonationUsername,
  124. string impersonationPassword)
  125. {
  126. m_sipTransport = sipTransport;
  127. GetCanonicalDomainDelegate_External = getCanonicalDomain;
  128. LogDelegate_External = logDelegate;
  129. m_sipAccountPersistor = sipAssetPersistor;
  130. GetSIPAccountBindings_External = getBindings;
  131. m_dialPlanPersistor = dialPlanPersistor;
  132. m_sipDialoguePersistor = sipDialoguePersistor;
  133. m_outboundProxySocket = outboundProxySocket;
  134. m_rubyScriptCommonPath = rubyScriptCommonPath;
  135. m_impersonationUsername = impersonationUsername;
  136. m_impersonationPassword = impersonationPassword;
  137. LoadRubyCommonScript();
  138. Thread monitorScriptsThread = new Thread(new ThreadStart(MonitorScripts));
  139. monitorScriptsThread.Name = MONITOR_THREAD_NAME;
  140. monitorScriptsThread.Start();
  141. }
  142. /// <summary>
  143. /// Gets the number of currently executing dial plan scripts for the specified username.
  144. /// </summary>
  145. /// <param name="username">The username to get the execution count for.</param>
  146. /// <returns>The number of currently executing dial plan scripts for the specified user.</returns>
  147. public int GetExecutionCountForUser(string username)
  148. {
  149. try
  150. {
  151. return (from script in m_runningScripts where script.Owner == username select script).Count();
  152. }
  153. catch (Exception excp)
  154. {
  155. logger.Error("Exception GetExecutionCountForUser. " + excp.Message);
  156. return 0;
  157. }
  158. }
  159. public void Execute(
  160. DialPlanContext dialPlanContext,
  161. ISIPServerUserAgent uas,
  162. SIPCallDirection callDirection,
  163. DialogueBridgeCreatedDelegate createBridgeDelegate,
  164. ISIPCallManager callManager)
  165. {
  166. if (dialPlanContext == null)
  167. {
  168. throw new ArgumentNullException("The DialPlanContext parameter cannot be null when attempting to execute a dialplan.");
  169. }
  170. if (uas.IsUASAnswered)
  171. {
  172. // This can occur if the call is cancelled by the caller between when the INVITE was received and when the dialplan execution was ready.
  173. logger.Warn("Dialplan execution for " + dialPlanContext.SIPDialPlan.DialPlanName + " for " + uas.CallDirection + " call to " + uas.CallDestination + " did not proceed as call already answered.");
  174. dialPlanContext.DialPlanExecutionFinished();
  175. }
  176. else
  177. {
  178. if (dialPlanContext.ContextType == DialPlanContextsEnum.Line)
  179. {
  180. ThreadPool.QueueUserWorkItem(delegate { ExecuteDialPlanLine((DialPlanLineContext)dialPlanContext, uas, callDirection, createBridgeDelegate, callManager); });
  181. }
  182. else
  183. {
  184. ExecuteDialPlanScript((DialPlanScriptContext)dialPlanContext, uas, callDirection, createBridgeDelegate, callManager);
  185. }
  186. }
  187. }
  188. /// <summary>
  189. /// Processes the matched dial plan command for an outgoing call request. This method is used for "exten =>" formatted dial plans. In addition if the dial
  190. /// plan owner has requested that their dialplan be used for incoming calls it will process those as well.
  191. /// </summary>
  192. /// <param name="localEndPoint">The SIP Proxy socket the request was received on.</param>
  193. /// <param name="remoteEndPoint">The socket the request was recevied from.</param>
  194. /// <param name="transaction">The SIP Invite transaction that initiated the dial plan processing.</param>
  195. /// <param name="manglePrivateAddresses">If true private IP addresses will be subtituted for the remote socket.</param>
  196. /// <param name="canonicalFromDomain">If (and only if) the call is an outgoing call this will be set to the canonical domain of the host in the SIP From
  197. /// header. An outgoing call is one from an authenticated user destined for an external SIP URI. If the call is an incoming this will be null.</param>
  198. /// <param name="canonicalToDomain">If (and only if) the call is an incoming call this will be set to the canonical domain of the host in the SIP URI
  199. /// request. An incoming call is one from an external caller to a URI corresponding to a hosted domain on this SIP Proxy.</param>
  200. private void ExecuteDialPlanLine(
  201. DialPlanLineContext dialPlanContext,
  202. ISIPServerUserAgent uas,
  203. SIPCallDirection callDirection,
  204. DialogueBridgeCreatedDelegate createBridgeDelegate,
  205. ISIPCallManager callManager)
  206. {
  207. try
  208. {
  209. //SIPRequest sipRequest = uas.CallRequest;
  210. FireProxyLogEvent(new SIPMonitorConsoleEvent(SIPMonitorServerTypesEnum.AppServer, SIPMonitorEventTypesEnum.DialPlan, "Executing line dial plan for call to " + uas.CallDestination + ".", dialPlanContext.Owner));
  211. DialPlanCommand matchedCommand = dialPlanContext.GetDialPlanMatch(uas.CallDestination);
  212. if (matchedCommand == null)
  213. {
  214. FireProxyLogEvent(new SIPMonitorConsoleEvent(SIPMonitorServerTypesEnum.AppServer, SIPMonitorEventTypesEnum.DialPlan, "Destination " + uas.CallDestination + " not found in line dial plan " + dialPlanContext.SIPDialPlan.DialPlanName + ".", dialPlanContext.Owner));
  215. dialPlanContext.CallFailed(SIPResponseStatusCodesEnum.NotFound, null, null);
  216. }
  217. else if (Regex.Match(matchedCommand.Command, "Switch|Dial", RegexOptions.IgnoreCase).Success)
  218. {
  219. if (matchedCommand.Data != null && matchedCommand.Data.Trim().Length > 0)
  220. {
  221. DialStringParser dialStringParser = new DialStringParser(m_sipTransport, dialPlanContext.Owner, dialPlanContext.SIPAccount, dialPlanContext.SIPProviders, m_sipAccountPersistor.Get, GetSIPAccountBindings_External, GetCanonicalDomainDelegate_External, LogDelegate_External, dialPlanContext.SIPDialPlan.DialPlanName);
  222. ForkCall ForkCall = new ForkCall(m_sipTransport, FireProxyLogEvent, callManager.QueueNewCall, dialStringParser, dialPlanContext.Owner, dialPlanContext.AdminMemberId, m_outboundProxySocket, null);
  223. ForkCall.CallProgress += dialPlanContext.CallProgress;
  224. ForkCall.CallFailed += dialPlanContext.CallFailed;
  225. ForkCall.CallAnswered += dialPlanContext.CallAnswered;
  226. Queue<List<SIPCallDescriptor>> calls = dialStringParser.ParseDialString(DialPlanContextsEnum.Line, uas.CallRequest.Copy(), matchedCommand.Data, null, null, null, dialPlanContext.CallersNetworkId, null, null, null, null);
  227. ForkCall.Start(calls);
  228. }
  229. else
  230. {
  231. FireProxyLogEvent(new SIPMonitorConsoleEvent(SIPMonitorServerTypesEnum.AppServer, SIPMonitorEventTypesEnum.DialPlan, "Error processing dialplan Dial command the dial string was empty.", dialPlanContext.Owner));
  232. }
  233. }
  234. //else if (Regex.Match(matchedCommand.Command, "RTSP", RegexOptions.IgnoreCase).Success)
  235. //{
  236. // RTSPApp rtspCall = new RTSPApp(FireProxyLogEvent, (UASInviteTransaction)transaction, dialPlanContext.Owner);
  237. // rtspCall.Start(matchedCommand.Data);
  238. //}
  239. else if (Regex.Match(matchedCommand.Command, "SIPReply", RegexOptions.IgnoreCase).Success)
  240. {
  241. string[] replyFields = matchedCommand.Data.Split(',');
  242. string statusMessage = (replyFields.Length > 1 && replyFields[1] != null) ? replyFields[1].Trim() : null;
  243. SIPResponseStatusCodesEnum status = SIPResponseStatusCodes.GetStatusTypeForCode(Convert.ToInt32(replyFields[0]));
  244. if ((int)status >= 300)
  245. {
  246. dialPlanContext.CallFailed(status, statusMessage, null);
  247. }
  248. else if ((int)status < 200)
  249. {
  250. dialPlanContext.CallProgress(status, statusMessage, null, null, null, null);
  251. }
  252. }
  253. else
  254. {
  255. FireProxyLogEvent(new SIPMonitorConsoleEvent(SIPMonitorServerTypesEnum.AppServer, SIPMonitorEventTypesEnum.Error, "Command " + matchedCommand.Command + " is not a valid dial plan command.", dialPlanContext.Owner));
  256. dialPlanContext.CallFailed(SIPResponseStatusCodesEnum.InternalServerError, "Invalid dialplan command " + matchedCommand.Command, null);
  257. }
  258. }
  259. catch (Exception excp)
  260. {
  261. FireProxyLogEvent(new SIPMonitorConsoleEvent(SIPMonitorServerTypesEnum.AppServer, SIPMonitorEventTypesEnum.Error, "Error executing line dialplan for " + uas.CallRequest.URI.ToString() + ". " + excp.Message, dialPlanContext.Owner));
  262. dialPlanContext.DialPlanExecutionFinished();
  263. }
  264. }
  265. /// <summary>
  266. /// Processes a dialplan script (currently Ruby scripts only) for a received SIP INVITE request.
  267. /// </summary>
  268. /// <param name="coreLogDelegate">A function delegate that passes log/diagnostics events back to the SIP Proxy Core.</param>
  269. /// <param name="createBridgeDelegate">A function delegate that is called in the event that the dial plan command results in a call being answered and a bridge needing to be created.</param>
  270. /// <param name="localEndPoint">The SIP Proxy socket the request was received on.</param>
  271. /// <param name="remoteEndPoint">The socket the request was recevied from.</param>
  272. /// <param name="clientTransaction">The SIP Invite transaction that initiated the dial plan processing.</param>
  273. /// <param name="canonicalFromDomain">If (and only if) the call is an outgoing call this will be set to the canonical domain of the host in the SIP From
  274. /// header. An outgoing call is one from an authenticated user destined for an external SIP URI. If the call is an incoming this will be null.</param>
  275. /// <param name="canonicalToDomain">If (and only if) the call is an incoming call this will be set to the canonical domain of the host in the SIP URI
  276. /// request. An incoming call is one from an external caller to a URI corresponding to a hosted domain on this SIP Proxy.</param>
  277. private void ExecuteDialPlanScript(
  278. DialPlanScriptContext dialPlanContext,
  279. ISIPServerUserAgent uas,
  280. SIPCallDirection callDirection,
  281. DialogueBridgeCreatedDelegate createBridgeDelegate,
  282. ISIPCallManager callManager)
  283. {
  284. try
  285. {
  286. if (uas == null)
  287. {
  288. throw new ArgumentNullException("The ISIPServerUserAgent parameter cannot be null when attempting to execute a dialplan script.");
  289. }
  290. FireProxyLogEvent(new SIPMonitorConsoleEvent(SIPMonitorServerTypesEnum.AppServer, SIPMonitorEventTypesEnum.NewCall, "Executing script dial plan for call to " + uas.CallDestination + ".", dialPlanContext.Owner));
  291. if (!dialPlanContext.DialPlanScript.IsNullOrBlank())
  292. {
  293. DialPlanExecutingScript dialPlanExecutionScript = null;
  294. int runningScriptCount = (from script in m_runningScripts where !script.Complete select script).Count();
  295. if (runningScriptCount < MAX_ALLOWED_SCRIPTSCOPES)
  296. {
  297. m_dialPlanScriptContextsCreated++;
  298. //FireProxyLogEvent(new SIPMonitorConsoleEvent(SIPMonitorServerTypesEnum.AppServer, SIPMonitorEventTypesEnum.DialPlan, "Creating DialPlanExecutingScript number " + m_dialPlanScriptContextsCreated + " for dialplan execution for script owned by " + dialPlanContext.Owner + ".", null));
  299. dialPlanExecutionScript = new DialPlanExecutingScript(FireProxyLogEvent);
  300. }
  301. else
  302. {
  303. FireProxyLogEvent(new SIPMonitorConsoleEvent(SIPMonitorServerTypesEnum.AppServer, SIPMonitorEventTypesEnum.DialPlan, "Running script limit of " + MAX_ALLOWED_SCRIPTSCOPES + " reached.", null));
  304. lock (m_runningScripts)
  305. {
  306. foreach (DialPlanExecutingScript runningScript in m_runningScripts)
  307. {
  308. FireProxyLogEvent(new SIPMonitorConsoleEvent(SIPMonitorServerTypesEnum.AppServer, SIPMonitorEventTypesEnum.DialPlan, " running script owner=" + runningScript.Owner + ", dialplan name=" + runningScript.ExecutingDialPlanContext.SIPDialPlan.DialPlanName + ", start time=" + runningScript.StartTime.ToString("dd MMM yyyy HH:mm:ss") + ", is complete=" + runningScript.Complete + ".", null));
  309. }
  310. }
  311. }
  312. if (dialPlanExecutionScript != null)
  313. {
  314. dialPlanExecutionScript.Initialise(dialPlanContext);
  315. DialPlanScriptFacade planFacade = new DialPlanScriptFacade(
  316. m_sipTransport,
  317. dialPlanExecutionScript,
  318. FireProxyLogEvent,
  319. createBridgeDelegate,
  320. (uas.CallRequest != null) ? uas.CallRequest.Copy() : null, // A different copy to the req object. Stops inadvertent changes in the dialplan.
  321. callDirection,
  322. dialPlanContext,
  323. GetCanonicalDomainDelegate_External,
  324. callManager,
  325. m_sipAccountPersistor,
  326. m_dialPlanPersistor,
  327. m_sipDialoguePersistor,
  328. GetSIPAccountBindings_External,
  329. m_outboundProxySocket);
  330. DialPlanCRMFacade crmFacade = new DialPlanCRMFacade(FireProxyLogEvent, dialPlanContext);
  331. DialPlanLookupFacade lookupFacade = new DialPlanLookupFacade(FireProxyLogEvent, dialPlanContext.Owner);
  332. ScriptScope rubyScope = dialPlanExecutionScript.DialPlanScriptScope;
  333. rubyScope.SetVariable(SCRIPT_HELPEROBJECT_NAME, planFacade);
  334. rubyScope.SetVariable(SCRIPT_CRMOBJECT_NAME, crmFacade);
  335. rubyScope.SetVariable(SCRIPT_LOOKUPOBJECT_NAME, lookupFacade);
  336. if (uas.CallRequest != null)
  337. {
  338. rubyScope.SetVariable(SCRIPT_REQUESTOBJECT_NAME, uas.CallRequest.Copy());
  339. }
  340. dialPlanExecutionScript.DialPlanScriptThread = new Thread(new ParameterizedThreadStart(delegate { ExecuteScript(dialPlanExecutionScript, dialPlanContext, planFacade, m_rubyScriptCommon + dialPlanContext.DialPlanScript); }));
  341. lock (m_runningScripts)
  342. {
  343. m_runningScripts.Add(dialPlanExecutionScript);
  344. }
  345. dialPlanExecutionScript.DialPlanScriptThread.Start();
  346. }
  347. else
  348. {
  349. FireProxyLogEvent(new SIPMonitorConsoleEvent(SIPMonitorServerTypesEnum.AppServer, SIPMonitorEventTypesEnum.Error, "Error processing call " + uas.CallDestination + " there were no script slots available, script could not be executed.", dialPlanContext.Owner));
  350. dialPlanContext.CallFailed(SIPResponseStatusCodesEnum.InternalServerError, "Dial plan script engine was overloaded", null);
  351. }
  352. }
  353. else
  354. {
  355. FireProxyLogEvent(new SIPMonitorConsoleEvent(SIPMonitorServerTypesEnum.AppServer, SIPMonitorEventTypesEnum.DialPlan, "A script dial plan was empty, execution cannot continue.", dialPlanContext.Owner));
  356. dialPlanContext.CallFailed(SIPResponseStatusCodesEnum.InternalServerError, "Dial plan script was empty", null);
  357. }
  358. }
  359. catch (Exception excp)
  360. {
  361. FireProxyLogEvent(new SIPMonitorConsoleEvent(SIPMonitorServerTypesEnum.AppServer, SIPMonitorEventTypesEnum.Error, "Error executing script dialplan for " + uas.CallDestination + ". " + excp.Message, dialPlanContext.Owner));
  362. dialPlanContext.CallFailed(SIPResponseStatusCodesEnum.InternalServerError, "Dial plan exception starting script", null);
  363. }
  364. }
  365. private void ExecuteScript(
  366. DialPlanExecutingScript executingScript,
  367. DialPlanContext dialPlanContext,
  368. DialPlanScriptFacade planFacade,
  369. string script)
  370. {
  371. try
  372. {
  373. Thread.CurrentThread.Name = "dialplanscript-" + executingScript.ScriptNumber;
  374. if (m_impersonationUsername != null && m_impersonationPassword != null)
  375. {
  376. WrapperImpersonationContext impersonationConext = new WrapperImpersonationContext(null, m_impersonationUsername, m_impersonationPassword);
  377. impersonationConext.Enter();
  378. }
  379. //logger.Debug(Thread.CurrentThread.Name + " identity=" + WindowsIdentity.GetCurrent().Name + ".");
  380. FireProxyLogEvent(new SIPMonitorConsoleEvent(SIPMonitorServerTypesEnum.AppServer, SIPMonitorEventTypesEnum.DialPlan, "Dial plan execution starting on thread " + Thread.CurrentThread.Name + " for " + dialPlanContext.Owner + ".", null));
  381. executingScript.DialPlanScriptEngine.Execute(script, executingScript.DialPlanScriptScope);
  382. //FireProxyLogEvent(new SIPMonitorConsoleEvent(SIPMonitorServerTypesEnum.AppServer, SIPMonitorEventTypesEnum.DialPlan, "Dial plan execution finished after full script run on thread " + Thread.CurrentThread.Name + " for " + dialPlanContext.Owner + ".", null));
  383. }
  384. catch (ApplicationException appExcp)
  385. {
  386. if (appExcp.Message != "Script was halted by external intervention.")
  387. {
  388. logger.Error("ApplicationException ExecuteScript. " + appExcp.Message);
  389. FireProxyLogEvent(new SIPMonitorConsoleEvent(SIPMonitorServerTypesEnum.AppServer, SIPMonitorEventTypesEnum.DialPlan, "There was an exception executing your dial plan script: " + appExcp.Message, executingScript.Owner));
  390. FireProxyLogEvent(new SIPMonitorConsoleEvent(SIPMonitorServerTypesEnum.AppServer, SIPMonitorEventTypesEnum.DialPlan, "ApplicationException on user " + executingScript.Owner + "'s dial plan script. " + appExcp.Message, null));
  391. executingScript.ExecutionError = appExcp.Message;
  392. }
  393. else
  394. {
  395. FireProxyLogEvent(new SIPMonitorConsoleEvent(SIPMonitorServerTypesEnum.AppServer, SIPMonitorEventTypesEnum.DialPlan, "Dial plan execution finished after being halted due to execution interrupt on thread " + Thread.CurrentThread.Name + " for " + dialPlanContext.Owner + ".", null));
  396. }
  397. }
  398. //catch (System.Scripting.SyntaxErrorException)
  399. catch (SyntaxErrorException syntaxExcp)
  400. {
  401. logger.Warn("SyntaxErrorException. Owner=" + dialPlanContext.Owner + ", DialPlanName=" + dialPlanContext.SIPDialPlan.DialPlanName + ", Line=" + syntaxExcp.Line + ".");
  402. FireProxyLogEvent(new SIPMonitorConsoleEvent(SIPMonitorServerTypesEnum.AppServer, SIPMonitorEventTypesEnum.DialPlan, "There was a syntax error in your dial plan on line " + syntaxExcp.Line + ", please check.", executingScript.Owner));
  403. executingScript.ExecutionError = "Dial plan syntax error";
  404. }
  405. catch (MissingMethodException missingExcp)
  406. {
  407. logger.Warn("MissingMethodException. " + missingExcp.Message);
  408. FireProxyLogEvent(new SIPMonitorConsoleEvent(SIPMonitorServerTypesEnum.AppServer, SIPMonitorEventTypesEnum.DialPlan, "There was a missing method exception in your dial plan: " + missingExcp.Message + ".", executingScript.Owner));
  409. executingScript.ExecutionError = "Dial plan missing method";
  410. }
  411. catch (ThreadAbortException) { }
  412. catch (Exception excp)
  413. {
  414. logger.Error("Exception ExecuteScript (" + excp.GetType() + "). " + excp.Message);
  415. FireProxyLogEvent(new SIPMonitorConsoleEvent(SIPMonitorServerTypesEnum.AppServer, SIPMonitorEventTypesEnum.DialPlan, "There was an exception executing your dial plan script: " + excp.Message, executingScript.Owner));
  416. FireProxyLogEvent(new SIPMonitorConsoleEvent(SIPMonitorServerTypesEnum.AppServer, SIPMonitorEventTypesEnum.DialPlan, "Exception on user " + executingScript.Owner + "'s dial plan script (" + excp.GetType() + "). " + excp.Message, null));
  417. executingScript.ExecutionError = "Dial plan exception";
  418. }
  419. finally
  420. {
  421. executingScript.StopExecution();
  422. }
  423. }
  424. private void MonitorScripts()
  425. {
  426. try
  427. {
  428. while (!StopScriptMonitoring)
  429. {
  430. DialPlanExecutingScript[] killScripts = null;
  431. bool lockTaken = false;
  432. Monitor.TryEnter(m_runningScripts, 100, ref lockTaken);
  433. if (lockTaken)
  434. {
  435. try
  436. {
  437. killScripts = (from script in m_runningScripts
  438. where
  439. script.Complete ||
  440. DateTime.Now > script.EndTime ||
  441. DateTime.Now.Subtract(script.StartTime).TotalSeconds > ABSOLUTEMAX_SCRIPTPROCESSING_SECONDS
  442. select script).ToArray();
  443. }
  444. finally
  445. {
  446. Monitor.Exit(m_runningScripts);
  447. }
  448. }
  449. else
  450. {
  451. logger.Warn("Dialplan engine monitoring thread could not acquire a lock on the running scripts list.");
  452. }
  453. if (killScripts != null)
  454. {
  455. for (int index = 0; index < killScripts.Length; index++)
  456. {
  457. DialPlanExecutingScript killScript = killScripts[index];
  458. DialPlanContext dialPlanContext = killScript.ExecutingDialPlanContext;
  459. if (!killScript.Complete)
  460. {
  461. killScript.LogDelegate(new SIPMonitorConsoleEvent(SIPMonitorServerTypesEnum.AppServer, SIPMonitorEventTypesEnum.DialPlan, "Long running dialplan script is being forcefully terminated.", killScript.Owner));
  462. }
  463. try
  464. {
  465. if (dialPlanContext != null && !dialPlanContext.IsAnswered)
  466. {
  467. // The dialplan script has finished but the client call has not been answered. There could have be an
  468. // error executing the script or the dialplan could have completed without getting an answer.
  469. if (!killScript.ExecutionError.IsNullOrBlank())
  470. {
  471. FireProxyLogEvent(new SIPMonitorConsoleEvent(SIPMonitorServerTypesEnum.AppServer, SIPMonitorEventTypesEnum.DialPlan, "Dial plan execution completed without answering and had an execution error message of " + killScript.ExecutionError + ".", killScript.Owner));
  472. dialPlanContext.CallFailed(SIPResponseStatusCodesEnum.InternalServerError, killScript.ExecutionError, null);
  473. }
  474. else if (killScript.LastFailureStatus != SIPResponseStatusCodesEnum.None)
  475. {
  476. FireProxyLogEvent(new SIPMonitorConsoleEvent(SIPMonitorServerTypesEnum.AppServer, SIPMonitorEventTypesEnum.DialPlan, "Dial plan execution completed without answering and a last failure status of " + killScript.LastFailureStatus + " " + killScript.LastFailureReason + ".", killScript.Owner));
  477. dialPlanContext.CallFailed(killScript.LastFailureStatus, killScript.LastFailureReason, null);
  478. }
  479. else
  480. {
  481. FireProxyLogEvent(new SIPMonitorConsoleEvent(SIPMonitorServerTypesEnum.AppServer, SIPMonitorEventTypesEnum.DialPlan, "Dial plan execution completed without answering and with no last failure status.", killScript.Owner));
  482. dialPlanContext.CallFailed(SIPResponseStatusCodesEnum.TemporarilyUnavailable, null, null);
  483. }
  484. }
  485. else
  486. {
  487. FireProxyLogEvent(new SIPMonitorConsoleEvent(SIPMonitorServerTypesEnum.AppServer, SIPMonitorEventTypesEnum.DialPlan, "Dial plan execution completed with normal clearing.", killScript.Owner));
  488. }
  489. long gcMemory = GC.GetTotalMemory(false);
  490. long physicalMemory = System.Diagnostics.Process.GetCurrentProcess().PrivateMemorySize64;
  491. FireProxyLogEvent(new SIPMonitorConsoleEvent(SIPMonitorServerTypesEnum.AppServer, SIPMonitorEventTypesEnum.DialPlan, "Dial plan finished for " + killScript.Owner + ", gc memory=" + gcMemory + ", physical memory=" + physicalMemory + ", running script count=" + ScriptCount + ".", null));
  492. }
  493. catch (Exception finallyExcp)
  494. {
  495. logger.Error("Exception MonitorScripts Kill. " + finallyExcp.Message);
  496. }
  497. finally
  498. {
  499. try
  500. {
  501. /*if (killScript.DialPlanScriptThread != null && killScript.DialPlanScriptThread.IsAlive)
  502. {
  503. //logger.Debug("Aborting dialplan script thread.");
  504. killScript.DialPlanScriptThread.Abort();
  505. }*/
  506. killScript.StopExecution();
  507. }
  508. catch (ThreadStateException) { } // This exception is thrown when aborting a thread in a suspended state and is expected behaviour.
  509. catch (Exception killExcp)
  510. {
  511. logger.Error("Exception MonitorScripts aborting thread (" + killExcp.GetType().ToString() + "). " + killExcp.Message);
  512. }
  513. lock (m_runningScripts)
  514. {
  515. m_runningScripts.Remove(killScript);
  516. }
  517. logger.Debug("Executing script " + killScript.DialPlanScriptThread.Name + " removed from running scripts list, running script count=" + ScriptCount + ".");
  518. }
  519. }
  520. }
  521. Thread.Sleep(500);
  522. }
  523. if (StopScriptMonitoring)
  524. {
  525. if (m_runningScripts.Count > 0)
  526. {
  527. for (int index = 0; index < m_runningScripts.Count; index++)
  528. {
  529. if (m_runningScripts[index].DialPlanScriptThread != null && m_runningScripts[index].DialPlanScriptThread.IsAlive)
  530. {
  531. try
  532. {
  533. m_runningScripts[index].DialPlanScriptThread.Abort();
  534. }
  535. catch (Exception finalKill)
  536. {
  537. logger.Debug("Exception on script final kill. " + finalKill.Message);
  538. }
  539. }
  540. }
  541. }
  542. }
  543. }
  544. catch (Exception excp)
  545. {
  546. logger.Error("Exception MonitorScripts. " + excp);
  547. }
  548. }
  549. private void LoadRubyCommonScript()
  550. {
  551. if (m_rubyScriptCommonPath != null && File.Exists(m_rubyScriptCommonPath))
  552. {
  553. ReloadCommonRubyScript(null, null);
  554. string dir = Path.GetDirectoryName(m_rubyScriptCommonPath);
  555. string file = Path.GetFileName(m_rubyScriptCommonPath);
  556. logger.Debug("Starting file watch on Ruby Common Script " + dir + @"\" + file + ".");
  557. FileSystemWatcher rubyCommonWatcher = new FileSystemWatcher(dir, file);
  558. rubyCommonWatcher.Changed += new FileSystemEventHandler(ReloadCommonRubyScript);
  559. rubyCommonWatcher.EnableRaisingEvents = true;
  560. }
  561. }
  562. private void ReloadCommonRubyScript(object sender, FileSystemEventArgs e)
  563. {
  564. try
  565. {
  566. if (m_rubyCommonLastReload == null || DateTime.Now.Subtract(m_rubyCommonLastReload.Value).TotalSeconds > RUBY_COMMON_RELOAD_INTERVAL)
  567. {
  568. m_rubyCommonLastReload = DateTime.Now;
  569. logger.Debug("Ruby common script file changed, reloading ruby common script.");
  570. File.Copy(m_rubyScriptCommonPath, m_rubyScriptCommonPath + RUBY_COMMON_COPY_EXTEN, true);
  571. StreamReader proxyRuntimeReader = new StreamReader(m_rubyScriptCommonPath + RUBY_COMMON_COPY_EXTEN);
  572. string rubyScriptCommon = proxyRuntimeReader.ReadToEnd();
  573. proxyRuntimeReader.Close();
  574. m_rubyScriptCommon = rubyScriptCommon;
  575. }
  576. }
  577. catch (Exception excp)
  578. {
  579. logger.Error("Exception rubyScriptWatcher_Changed. " + excp);
  580. }
  581. }
  582. private void FireProxyLogEvent(SIPMonitorEvent monitorEvent)
  583. {
  584. try
  585. {
  586. if (LogDelegate_External != null)
  587. {
  588. LogDelegate_External(monitorEvent);
  589. }
  590. }
  591. catch (Exception excp)
  592. {
  593. logger.Error("Exception FireProxyLogEvent DialPlanEngine. " + excp.Message);
  594. }
  595. }
  596. #region Unit testing.
  597. #if UNITTEST
  598. [TestFixture]
  599. public class SIPDialPlanUnitTest
  600. {
  601. [TestFixtureSetUp]
  602. public void Init()
  603. { }
  604. [TestFixtureTearDown]
  605. public void Dispose()
  606. { }
  607. [Test]
  608. public void SampleTest()
  609. {
  610. Console.WriteLine("--> " + System.Reflection.MethodBase.GetCurrentMethod().Name);
  611. Assert.IsTrue(true, "True was false.");
  612. Console.WriteLine("---------------------------------");
  613. }
  614. [Test]
  615. public void SimpleDialPlanTest()
  616. {
  617. Console.WriteLine("--> " + System.Reflection.MethodBase.GetCurrentMethod().Name);
  618. string testDialPlan = @"
  619. exten => 100,1,Switch(""anonymous.invalid"", ""password"", ""612@freeworlddialup.com"")
  620. exten => 101,1,Switch(""username"", ""password"", ""303@sip.blueface.ie"")
  621. ";
  622. SIPDialPlan dialPlan = new SIPDialPlan(null, null, null, testDialPlan, SIPDialPlanScriptTypesEnum.Asterisk);
  623. DialPlanLineContext dialPlanContext = new DialPlanLineContext(null, null, null, null, null, dialPlan, null, null, null);
  624. Console.WriteLine("dst=" + dialPlanContext.m_commands[0].Destination + ", data=" + dialPlanContext.m_commands[0].Data + ".");
  625. Console.WriteLine("dst=" + dialPlanContext.m_commands[1].Destination + ", data=" + dialPlanContext.m_commands[1].Data + ".");
  626. Assert.IsTrue(dialPlanContext.m_commands.Count == 2, "The dial plan was not correctly parsed.");
  627. Assert.IsTrue(dialPlanContext.m_commands[0].Operation == DialPlanOpsEnum.Equals, "Command 1 oeration not correct.");
  628. Assert.IsTrue(dialPlanContext.m_commands[1].Operation == DialPlanOpsEnum.Equals, "Command 2 oeration not correct.");
  629. Assert.IsTrue(dialPlanContext.m_commands[0].Destination == "100", "Command 1 destination not correct.");
  630. Assert.IsTrue(dialPlanContext.m_commands[1].Destination == "101", "Command 2 destination not correct.");
  631. Assert.IsTrue(dialPlanContext.m_commands[0].Command == "Switch", "Command 1 command not correct.");
  632. Assert.IsTrue(dialPlanContext.m_commands[1].Command == "Switch", "Command 2 command not correct.");
  633. Assert.IsTrue(dialPlanContext.m_commands[0].Data == "\"anonymous.invalid\", \"password\", \"612@freeworlddialup.com\"", "Command 1 data not correct.");
  634. Assert.IsTrue(dialPlanContext.m_commands[1].Data == "\"username\", \"password\", \"303@sip.blueface.ie\"", "Command 2 data not correct.");
  635. Console.WriteLine("---------------------------------");
  636. }
  637. [Test]
  638. public void CommentOnLineTest()
  639. {
  640. Console.WriteLine("--> " + System.Reflection.MethodBase.GetCurrentMethod().Name);
  641. string testDialPlan = @"
  642. exten => 100,1,Switch(anonymous.invalid, password, 612@freeworlddialup.com) ; Comment
  643. exten => 101,1,Switch(""username"", ""password"", ""303@sip.blueface.ie)
  644. ";
  645. SIPDialPlan dialPlan = new SIPDialPlan(null, null, null, testDialPlan, SIPDialPlanScriptTypesEnum.Asterisk);
  646. DialPlanLineContext dialPlanContext = new DialPlanLineContext(null, null, null, null, null, dialPlan, null, null, null);
  647. Console.WriteLine("dst=" + dialPlanContext.m_commands[0].Destination + ", data=" + dialPlanContext.m_commands[0].Data + ".");
  648. Console.WriteLine("dst=" + dialPlanContext.m_commands[1].Destination + ", data=" + dialPlanContext.m_commands[1].Data + ".");
  649. Assert.IsTrue(dialPlanContext.m_commands.Count == 2, "The dial plan was not correctly parsed.");
  650. Assert.IsTrue(dialPlanContext.m_commands[0].Command == "Switch", "The dial plan command was not correct.");
  651. Assert.IsTrue(dialPlanContext.m_commands[0].Data == "anonymous.invalid, password, 612@freeworlddialup.com", "The dial plan data was not correct.");
  652. Console.WriteLine("---------------------------------");
  653. }
  654. [Test]
  655. public void DifferentOperatorsDialPlanTest()
  656. {
  657. Console.WriteLine("--> " + System.Reflection.MethodBase.GetCurrentMethod().Name);
  658. string testDialPlan = @"
  659. exten == 100,1,Switch(""anonymous.invalid"", ""password"", ""612@freeworlddialup.com"")
  660. exten =~ 101,1,Switch(""username"", ""password"", ""303@sip.blueface.ie"")
  661. exten = 103,1,Switch(""username"", ""password"", ""303@sip.blueface.ie)
  662. ";
  663. SIPDialPlan dialPlan = new SIPDialPlan(null, null, null, testDialPlan, SIPDialPlanScriptTypesEnum.Asterisk);
  664. DialPlanLineContext dialPlanContext = new DialPlanLineContext(null, null, null, null, null, dialPlan, null, null, null);
  665. Assert.IsTrue(dialPlanContext.m_commands.Count == 3, "The dial plan was not correctly parsed.");
  666. Assert.IsTrue(dialPlanContext.m_commands[0].Operation == DialPlanOpsEnum.Equals, "Command 1 operation was incorrect.");
  667. Assert.IsTrue(dialPlanContext.m_commands[1].Operation == DialPlanOpsEnum.Regex, "Command 2 operation was incorrect.");
  668. Assert.IsTrue(dialPlanContext.m_commands[2].Operation == DialPlanOpsEnum.Equals, "Command 3 operation was incorrect.");
  669. Console.WriteLine("---------------------------------");
  670. }
  671. [Test]
  672. public void NoMatchDialPlanTest()
  673. {
  674. Console.WriteLine("--> " + System.Reflection.MethodBase.GetCurrentMethod().Name);
  675. string testDialPlan = @"
  676. exten => _3100,1,Switch(anon, password, 1@sip.blueface.ie)
  677. exten => _3300,1,Switch(anon, password, 2@sip.blueface.ie)
  678. exten => _3000,1,Switch(anon, password, 3@sip.blueface.ie)
  679. ";
  680. SIPDialPlan dialPlan = new SIPDialPlan(null, null, null, testDialPlan, SIPDialPlanScriptTypesEnum.Asterisk);
  681. SIPRequest request = new SIPRequest(SIPMethodsEnum.INVITE, SIPURI.ParseSIPURI("sip:3200@sip.mysipswitch.com"));
  682. DialPlanLineContext dialPlanContext = new DialPlanLineContext(null, null, null, null, null, dialPlan, null, null, null);
  683. DialPlanCommand commandMatch = dialPlanContext.GetDialPlanMatch(request);
  684. Assert.IsTrue(dialPlanContext.m_commands.Count == 3, "The dial plan was not correctly parsed.");
  685. Assert.IsNull(commandMatch, "The dial plan produced a match when it should not have.");
  686. Console.WriteLine("---------------------------------");
  687. }
  688. [Test]
  689. public void NMatchDialPlanTest()
  690. {
  691. Console.WriteLine("--> " + System.Reflection.MethodBase.GetCurrentMethod().Name);
  692. string testDialPlan = @"
  693. exten => _3100,1,Switch(anon, password, 1@sip.blueface.ie)
  694. exten => _3N00,1,Switch(anon, password, 2@sip.blueface.ie)
  695. exten => _3000,1,Switch(anon, password, 3@sip.blueface.ie)
  696. ";
  697. SIPDialPlan dialPlan = new SIPDialPlan(null, null, null, testDialPlan, SIPDialPlanScriptTypesEnum.Asterisk);
  698. SIPRequest request = new SIPRequest(SIPMethodsEnum.INVITE, SIPURI.ParseSIPURI("sip:3200@sip.mysipswitch.com"));
  699. DialPlanLineContext dialPlanContext = new DialPlanLineContext(null, null, null, null, null, dialPlan, null, null, null);
  700. DialPlanCommand commandMatch = dialPlanContext.GetDialPlanMatch(request);
  701. Assert.IsTrue(dialPlanContext.m_commands.Count == 3, "The dial plan was not correctly parsed.");
  702. Assert.IsTrue(commandMatch.Data == "anon, password, 2@sip.blueface.ie", "The dial plan command match was not correct.");
  703. Console.WriteLine("---------------------------------");
  704. }
  705. [Test]
  706. public void ZMatchDialPlanTest()
  707. {
  708. Console.WriteLine("--> " + System.Reflection.MethodBase.GetCurrentMethod().Name);
  709. string testDialPlan = @"
  710. exten => _3000,1,Switch(anon, password, 1@sip.blueface.ie)
  711. exten => _3001,1,Switch(anon, password, 2@sip.blueface.ie)
  712. exten => _3Z00,1,Switch(anon, password, 3@sip.blueface.ie)
  713. ";
  714. SIPDialPlan dialPlan = new SIPDialPlan(null, null, null, testDialPlan, SIPDialPlanScriptTypesEnum.Asterisk);
  715. SIPRequest request = new SIPRequest(SIPMethodsEnum.INVITE, SIPURI.ParseSIPURI("sip:3100@sip.mysipswitch.com"));
  716. DialPlanLineContext dialPlanContext = new DialPlanLineContext(null, null, null, null, null, dialPlan, null, null, null);
  717. DialPlanCommand commandMatch = dialPlanContext.GetDialPlanMatch(request);
  718. Assert.IsTrue(dialPlanContext.m_commands.Count == 3, "The dial plan was not correctly parsed.");
  719. Assert.IsTrue(commandMatch.Data == "anon, password, 3@sip.blueface.ie", "The dial plan command match was not correct.");
  720. Console.WriteLine("---------------------------------");
  721. }
  722. [Test]
  723. public void SingleXMatchDialPlanTest()
  724. {
  725. Console.WriteLine("--> " + System.Reflection.MethodBase.GetCurrentMethod().Name);
  726. string testDialPlan = "exten => _3X.,1,Switch(anon, password, 1@sip.blueface.ie)";
  727. SIPDialPlan dialPlan = new SIPDialPlan(null, null, null, testDialPlan, SIPDialPlanScriptTypesEnum.Asterisk);
  728. SIPRequest request = new SIPRequest(SIPMethodsEnum.INVITE, SIPURI.ParseSIPURI("sip:300@sip.mysipswitch.com"));
  729. DialPlanLineContext dialPlanContext = new DialPlanLineContext(null, null, null, null, null, dialPlan, null, null, null);
  730. DialPlanCommand commandMatch = dialPlanContext.GetDialPlanMatch(request);
  731. Assert.IsNotNull(commandMatch, "The dial plan should have returned a match.");
  732. Console.WriteLine("---------------------------------");
  733. }
  734. [Test]
  735. public void SingleZMatchDialPlanTest()
  736. {
  737. Console.WriteLine("--> " + System.Reflection.MethodBase.GetCurrentMethod().Name);
  738. string testDialPlan = "exten => _3Z.,1,Switch(anon, password, 1@sip.blueface.ie)";
  739. SIPDialPlan dialPlan = new SIPDialPlan(null, null, null, testDialPlan, SIPDialPlanScriptTypesEnum.Asterisk);
  740. SIPRequest request = new SIPRequest(SIPMethodsEnum.INVITE, SIPURI.ParseSIPURI("sip:310@sip.mysipswitch.com"));
  741. DialPlanLineContext dialPlanContext = new DialPlanLineContext(null, null, null, null, null, dialPlan, null, null, null);
  742. DialPlanCommand commandMatch = dialPlanContext.GetDialPlanMatch(request);
  743. Assert.IsNotNull(commandMatch, "The dial plan should have returned a match.");
  744. Console.WriteLine("---------------------------------");
  745. }
  746. [Test]
  747. public void SingleNMatchDialPlanTest()
  748. {
  749. Console.WriteLine("--> " + System.Reflection.MethodBase.GetCurrentMethod().Name);
  750. string testDialPlan = "exten => _3N.,1,Switch(anon, password, 1@sip.blueface.ie)";
  751. SIPDialPlan dialPlan = new SIPDialPlan(null, nu

Large files files are truncated, but you can click here to view the full file