See change log for details

This commit is contained in:
skrusty_cp 2013-08-21 03:31:26 -07:00
parent 7cb7869462
commit 93c5659ab7
14 changed files with 462 additions and 28 deletions

View file

@ -90,7 +90,7 @@
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\Asterisk.NET.1.6.3.1\Asterisk.NET.1.6.3.1\Asterisk.NET\Asterisk.NET.csproj">
<ProjectReference Include="..\..\Asterisk.NET\Asterisk.NET.csproj">
<Project>{bc6e7dba-c05a-45fe-a2a3-b1637ce16274}</Project>
<Name>Asterisk.NET</Name>
</ProjectReference>

View file

@ -5,6 +5,8 @@ using Asterisk.NET.Manager.Action;
using Asterisk.NET.Manager.Response;
using Asterisk.NET.FastAGI;
using Asterisk.NET.Manager.Event;
using Asterisk.NET.FastAGI.MappingStrategies;
using System.Collections.Generic;
namespace Asterisk.NET.Test
{
@ -27,7 +29,10 @@ namespace Asterisk.NET.Test
[STAThread]
static void Main()
{
// Comment me out if you don't want to run the AMI sample
checkManagerAPI();
// Comment me out if you don't want to run the FastAGI sample
checkFastAGI();
}
@ -44,6 +49,24 @@ See CustomIVR.cs and fastagi-mapping.resx to detail.
Ctrl-C to exit");
AsteriskFastAGI agi = new AsteriskFastAGI();
agi.BindPort = 8675;
// Remove the lines below to enable the default (resource based) MappingStrategy
// You can use an XML file with XmlMappingStrategy, or simply pass in a list of
// ScriptMapping.
// If you wish to save it to a file, use ScriptMapping.SaveMappings and pass in a path.
// This can then be used to load the mappings without having to change the source code!
agi.MappingStrategy = new GeneralMappingStrategy(new List<ScriptMapping>()
{
new ScriptMapping() {
ScriptClass = "Asterisk.NET.Test.CustomIVR",
ScriptName = "customivr"
}
});
//agi.SC511_CAUSES_EXCEPTION = true;
//agi.SCHANGUP_CAUSES_EXCEPTION = true;
agi.Start();
}
#endregion

View file

@ -115,6 +115,9 @@
<Compile Include="FastAGI\Command\WaitForDigitCommand.cs" />
<Compile Include="FastAGI\Exceptions\InvalidCommandSyntaxException.cs" />
<Compile Include="FastAGI\Exceptions\InvalidOrUnknownCommandException.cs" />
<Compile Include="FastAGI\IMappingStrategy.cs" />
<Compile Include="FastAGI\MappingStrategies\GeneralMappingStrategy.cs" />
<Compile Include="FastAGI\MappingStrategies\ResourceMappingStrategy.cs" />
<Compile Include="FastAGI\MappingStrategy.cs" />
<Compile Include="FastAGI\Script\AGINoAction.cs" />
<Compile Include="IO\ServerSocket.cs" />

View file

@ -15,7 +15,7 @@ namespace Asterisk.NET
/// <summary>Line separator</summary>
public const string LINE_SEPARATOR = "\r\n";
public static Regex ASTERISK_VERSION = new Regex("^Asterisk\\s+([0-9].[0-9]+.[0-9]+).*", RegexOptions.Compiled | RegexOptions.IgnoreCase);
public static Regex ASTERISK_VERSION = new Regex("^Asterisk\\s+([0-9]+.[0-9]+.[0-9]+).*", RegexOptions.Compiled | RegexOptions.IgnoreCase);
public static Regex SHOW_VERSION_FILES_PATTERN = new Regex("^([\\S]+)\\s+Revision: ([0-9\\.]+)");
public static char[] RESPONSE_KEY_VALUE_SEPARATOR = new char[] { ':' };
public static char[] MINUS_SEPARATOR = new char[] { '-' };

View file

@ -9,16 +9,26 @@ namespace Asterisk.NET.FastAGI
private AGIReader agiReader;
private AGIReply agiReply;
public AGIChannel(IO.SocketConnection socket)
private bool _SC511_CAUSES_EXCEPTION = false;
private bool _SCHANGUP_CAUSES_EXCEPTION = false;
public AGIChannel(IO.SocketConnection socket, bool SC511_CAUSES_EXCEPTION, bool SCHANGUP_CAUSES_EXCEPTION)
{
this.agiWriter = new AGIWriter(socket);
this.agiReader = new AGIReader(socket);
this._SC511_CAUSES_EXCEPTION = SC511_CAUSES_EXCEPTION;
this._SCHANGUP_CAUSES_EXCEPTION = SCHANGUP_CAUSES_EXCEPTION;
}
public AGIChannel(AGIWriter agiWriter, AGIReader agiReader)
public AGIChannel(AGIWriter agiWriter, AGIReader agiReader, bool SC511_CAUSES_EXCEPTION, bool SCHANGUP_CAUSES_EXCEPTION)
{
this.agiWriter = agiWriter;
this.agiReader = agiReader;
this._SC511_CAUSES_EXCEPTION = SC511_CAUSES_EXCEPTION;
this._SCHANGUP_CAUSES_EXCEPTION = SCHANGUP_CAUSES_EXCEPTION;
}
/// <summary>
@ -38,6 +48,10 @@ namespace Asterisk.NET.FastAGI
throw new InvalidOrUnknownCommandException(command.BuildCommand());
else if (status == (int)AGIReplyStatuses.SC_INVALID_COMMAND_SYNTAX)
throw new InvalidCommandSyntaxException(agiReply.GetSynopsis(), agiReply.GetUsage());
else if (status == (int)AGIReplyStatuses.SC_DEAD_CHANNEL && _SC511_CAUSES_EXCEPTION)
throw new AGIHangupException();
else if ((status == 0) && agiReply.FirstLine == "HANGUP" && _SCHANGUP_CAUSES_EXCEPTION)
throw new AGIHangupException();
return agiReply;
}
}

View file

@ -17,7 +17,9 @@ namespace Asterisk.NET.FastAGI
#endif
private static readonly LocalDataStoreSlot channel = Thread.AllocateDataSlot();
private IO.SocketConnection socket;
private MappingStrategy mappingStrategy;
private IMappingStrategy mappingStrategy;
private bool _SC511_CAUSES_EXCEPTION = false;
private bool _SCHANGUP_CAUSES_EXCEPTION = false;
#region Channel
/// <summary>
@ -39,10 +41,12 @@ namespace Asterisk.NET.FastAGI
/// </summary>
/// <param name="socket">the socket connection to handle.</param>
/// <param name="mappingStrategy">the strategy to use to determine which script to run.</param>
public AGIConnectionHandler(IO.SocketConnection socket, MappingStrategy mappingStrategy)
public AGIConnectionHandler(IO.SocketConnection socket, IMappingStrategy mappingStrategy, bool SC511_CAUSES_EXCEPTION, bool SCHANGUP_CAUSES_EXCEPTION)
{
this.socket = socket;
this.mappingStrategy = mappingStrategy;
this._SC511_CAUSES_EXCEPTION = SC511_CAUSES_EXCEPTION;
this._SCHANGUP_CAUSES_EXCEPTION = SCHANGUP_CAUSES_EXCEPTION;
}
#endregion
@ -53,7 +57,7 @@ namespace Asterisk.NET.FastAGI
AGIReader reader = new AGIReader(socket);
AGIWriter writer = new AGIWriter(socket);
AGIRequest request = reader.ReadRequest();
AGIChannel channel = new AGIChannel(writer, reader);
AGIChannel channel = new AGIChannel(writer, reader, this._SC511_CAUSES_EXCEPTION, this._SCHANGUP_CAUSES_EXCEPTION);
AGIScript script = mappingStrategy.DetermineScript(request);
Thread.SetData(AGIConnectionHandler.channel, channel);

View file

@ -19,6 +19,11 @@ namespace Asterisk.NET.FastAGI
/// </summary>
SC_INVALID_OR_UNKNOWN_COMMAND = 510,
/// <summary>
/// Status code (511) indicating Asterisk was unable to process the
/// AGICommand because the channel is dead.
/// </summary>
SC_DEAD_CHANNEL = 511,
/// <summary>
/// Status code (520) indicating Asterisk was unable to process the
/// AGICommand because the syntax used was not correct. This is most likely
/// due to missing required parameters or additional parameters sent that are

View file

@ -1,3 +1,4 @@
using Asterisk.NET.FastAGI.MappingStrategies;
using System.IO;
using System.Net;
using System.Text;
@ -6,6 +7,21 @@ namespace Asterisk.NET.FastAGI
{
public class AsteriskFastAGI
{
#region Flags
/// <summary>
/// If set to true, causes the AGIChannel to throw an exception when a status code of 511 (Channel Dead) is returned.
/// This is set to false by default to maintain backwards compatibility
/// </summary>
public bool SC511_CAUSES_EXCEPTION = false;
/// <summary>
/// If set to true, causes the AGIChannel to throw an exception when return status is 0 and reply is HANGUP.
/// This is set to false by default to maintain backwards compatibility
/// </summary>
public bool SCHANGUP_CAUSES_EXCEPTION = false;
#endregion
#region Variables
#if LOGGER
private Logger logger = Logger.Instance();
@ -25,7 +41,7 @@ namespace Asterisk.NET.FastAGI
/// <summary>
/// The strategy to use for bind AGIRequests to AGIScripts that serve them.
/// </summary>
private MappingStrategy mappingStrategy;
private IMappingStrategy mappingStrategy;
private Encoding socketEncoding = Encoding.ASCII;
#endregion
@ -61,7 +77,7 @@ namespace Asterisk.NET.FastAGI
/// The default mapping is a MappingStrategy.
/// </summary>
/// <seealso cref="MappingStrategy" />
public MappingStrategy MappingStrategy
public IMappingStrategy MappingStrategy
{
set { this.mappingStrategy = value; }
}
@ -84,7 +100,7 @@ namespace Asterisk.NET.FastAGI
this.address = Common.AGI_BIND_ADDRESS;
this.port = Common.AGI_BIND_PORT;
this.poolSize = Common.AGI_POOL_SIZE;
this.mappingStrategy = new MappingStrategy();
this.mappingStrategy = new ResourceMappingStrategy();
}
#endregion
@ -97,7 +113,28 @@ namespace Asterisk.NET.FastAGI
this.address = Common.AGI_BIND_ADDRESS;
this.port = Common.AGI_BIND_PORT;
this.poolSize = Common.AGI_POOL_SIZE;
this.mappingStrategy = new MappingStrategy(mappingStrategy);
this.mappingStrategy = new ResourceMappingStrategy(mappingStrategy);
}
#endregion
#region Constructor - AsteriskFastAGI()
/// <summary>
/// Creates a new AsteriskFastAGI.
/// </summary>
public AsteriskFastAGI(IMappingStrategy mappingStrategy)
{
this.address = Common.AGI_BIND_ADDRESS;
this.port = Common.AGI_BIND_PORT;
this.poolSize = Common.AGI_POOL_SIZE;
this.mappingStrategy = mappingStrategy;
}
public AsteriskFastAGI(IMappingStrategy mappingStrategy, string ipaddress, int port, int poolSize)
{
this.address = ipaddress;
this.port = port;
this.poolSize = poolSize;
this.mappingStrategy = mappingStrategy;
}
#endregion
@ -113,7 +150,7 @@ namespace Asterisk.NET.FastAGI
this.address = Common.AGI_BIND_ADDRESS;
this.port = port;
this.poolSize = poolSize;
this.mappingStrategy = new MappingStrategy();
this.mappingStrategy = new ResourceMappingStrategy();
}
#endregion
@ -130,7 +167,7 @@ namespace Asterisk.NET.FastAGI
this.address = ipaddress;
this.port = port;
this.poolSize = poolSize;
this.mappingStrategy = new MappingStrategy();
this.mappingStrategy = new ResourceMappingStrategy();
}
#endregion
@ -168,7 +205,7 @@ namespace Asterisk.NET.FastAGI
#if LOGGER
logger.Info("Received connection.");
#endif
connectionHandler = new AGIConnectionHandler(socket, mappingStrategy);
connectionHandler = new AGIConnectionHandler(socket, mappingStrategy, this.SC511_CAUSES_EXCEPTION, this.SCHANGUP_CAUSES_EXCEPTION);
pool.AddJob(connectionHandler);
}
}

View file

@ -0,0 +1,8 @@
namespace Asterisk.NET.FastAGI
{
public interface IMappingStrategy
{
AGIScript DetermineScript(AGIRequest request);
void Load();
}
}

View file

@ -0,0 +1,180 @@
using System;
using System.Collections;
using System.Resources;
using System.Reflection;
using System.Collections.Generic;
using System.IO;
using System.Xml.Serialization;
namespace Asterisk.NET.FastAGI.MappingStrategies
{
internal class MappingAssembly
{
public string ClassName { get; set; }
public Assembly LoadedAssembly { get; set; }
public AGIScript CreateInstance()
{
AGIScript rtn = null;
try
{
if (LoadedAssembly != null)
rtn = (AGIScript)LoadedAssembly.CreateInstance(ClassName);
else
rtn = (AGIScript)Assembly.GetEntryAssembly().CreateInstance(ClassName);
}
catch (Exception ex)
{
}
return rtn;
}
}
public class ScriptMapping
{
/// <summary>
/// The name of the script as called by FastAGI
/// </summary>
public string ScriptName { get; set; }
/// <summary>
/// The class containing the AGIScript to be run
/// </summary>
public string ScriptClass{ get; set; }
/// <summary>
/// The name of the assembly to load, that contains the ScriptClass. Optional, if not specified, the class will be loaded from the current assembly
/// </summary>
public string ScriptAssmebly { get; set; }
public static List<ScriptMapping> LoadMappings(string pathToXml)
{
// Load ScriptMappings XML File
XmlSerializer xs = new XmlSerializer(typeof(List<ScriptMapping>));
try
{
using (FileStream fs = File.OpenRead(pathToXml))
{
return (List<ScriptMapping>)xs.Deserialize(fs);
}
}
catch
{
return new List<ScriptMapping>();
}
}
public static void SaveMappings(string pathToXml, List<ScriptMapping> resources)
{
// Save ScriptMappings XML File
XmlSerializer xs = new XmlSerializer(typeof(List<ScriptMapping>));
using (FileStream fs = File.Open(pathToXml, FileMode.Create, FileAccess.Write, FileShare.None))
{
lock (resources)
{
xs.Serialize(fs, resources);
}
}
}
}
/// <summary>
/// A MappingStrategy that is configured via a an XML file
/// or used by passing in a single or list of SciptMapping
/// This is useful as a general mapping strategy, rather than
/// using the default Resource Reference method.
/// </summary>
public class GeneralMappingStrategy : IMappingStrategy
{
#if LOGGER
private Logger logger = Logger.Instance();
#endif
private List<ScriptMapping> mappings;
private Dictionary<string, MappingAssembly> mapAssemblies;
public string AGIPath = string.Empty;
/// <summary>
///
/// </summary>
public GeneralMappingStrategy()
{
this.mappings = null;
this.mapAssemblies = null;
}
/// <summary>
///
/// </summary>
/// <param name="resources"></param>
public GeneralMappingStrategy(List<ScriptMapping> resources)
{
this.mappings = resources;
this.mapAssemblies = null;
}
/// <summary>
///
/// </summary>
/// <param name="xmlFilePath"></param>
public GeneralMappingStrategy(string xmlFilePath)
{
this.mappings = ScriptMapping.LoadMappings(xmlFilePath);
this.mapAssemblies = null;
}
public AGIScript DetermineScript(AGIRequest request)
{
AGIScript script = null;
if (mapAssemblies != null)
lock (mapAssemblies)
{
if (mapAssemblies.ContainsKey(request.Script))
script = mapAssemblies[request.Script].CreateInstance();
}
return script;
}
public void Load()
{
if (mapAssemblies == null)
mapAssemblies = new Dictionary<string, MappingAssembly>();
lock (mapAssemblies)
{
mapAssemblies.Clear();
try
{
foreach (var de in this.mappings)
{
MappingAssembly ma;
if (mapAssemblies.ContainsKey(de.ScriptName))
throw new AGIException(String.Format("Duplicate mapping name '{0}'", de.ScriptName));
if (!string.IsNullOrEmpty(de.ScriptAssmebly))
{
ma = new MappingAssembly()
{
ClassName = (string)de.ScriptClass,
LoadedAssembly = Assembly.LoadFile(Path.Combine(this.AGIPath, de.ScriptAssmebly))
};
}
else
{
ma = new MappingAssembly()
{
ClassName = (string)de.ScriptClass
};
}
mapAssemblies.Add(de.ScriptName, ma);
}
}
catch (Exception ex)
{
throw new Exception("No mappings were added before 'Load' method called.");
}
}
}
}
}

View file

@ -0,0 +1,128 @@
using System;
using System.Collections;
using System.Resources;
using System.Reflection;
namespace Asterisk.NET.FastAGI.MappingStrategies
{
/// <summary>
/// A MappingStrategy that is configured via a resource bundle.<br/>
/// The resource bundle contains the script part of the url as key and the fully
/// qualified class name of the corresponding AGIScript as value.<br/>
/// Example:
/// <pre>
/// noopcommand = Asterisk.NET.FastAGI.Command.NoopCommand
/// </pre>
/// NoopCommand must implement the AGIScript interface and have a default constructor with no parameters.<br/>
/// </summary>
public class ResourceMappingStrategy : IMappingStrategy
{
#if LOGGER
private Logger logger = Logger.Instance();
#endif
private string resourceName;
private Hashtable mapping;
public ResourceMappingStrategy()
{
this.resourceName = Common.AGI_DEFAULT_RESOURCE_BUNDLE_NAME;
this.mapping = null;
}
public ResourceMappingStrategy(string resourceName)
{
this.resourceName = resourceName;
this.mapping = null;
}
public AGIScript DetermineScript(AGIRequest request)
{
AGIScript script = null;
if (mapping != null)
lock (mapping.SyncRoot)
{
if (mapping.Contains(request.Script))
script = (AGIScript)mapping[request.Script];
}
return script;
}
public string ResourceBundleName
{
set
{
if (value == null)
{
mapping = null;
resourceName = null;
}
else if (this.resourceName != value)
{
this.resourceName = value;
Load();
}
}
}
public void Load()
{
string scriptName;
string className;
AGIScript agiScript;
if (mapping == null)
mapping = new Hashtable();
lock (mapping)
{
mapping.Clear();
try
{
ResourceReader rr = new ResourceReader(AppDomain.CurrentDomain.BaseDirectory + resourceName);
foreach (DictionaryEntry de in rr)
{
scriptName = (string)de.Key;
className = (string)de.Value;
agiScript = CreateAGIScriptInstance(className);
if(mapping.Contains(scriptName))
throw new AGIException(String.Format("Duplicate mapping name '{0}' in file {1}", scriptName, resourceName));
mapping.Add(scriptName, agiScript);
#if LOGGER
logger.Info("Added mapping for '" + scriptName + "' to class " + agiScript.GetType().FullName);
#endif
}
}
catch (Exception ex)
{
#if LOGGER
logger.Error("Resource bundle '" + resourceName + "' is missing.");
#endif
throw ex;
}
}
}
private AGIScript CreateAGIScriptInstance(string className)
{
Type agiScriptClass;
ConstructorInfo constructor;
AGIScript agiScript;
try
{
agiScriptClass = Type.GetType(className);
constructor = agiScriptClass.GetConstructor(new Type[]{});
agiScript = (AGIScript) constructor.Invoke(new object[]{});
}
catch(Exception ex)
{
#if LOGGER
logger.Error("Unable to create AGIScript instance of type " + className, ex);
return null;
#else
throw new AGIException("Unable to create AGIScript instance of type " + className, ex);
#endif
}
return agiScript;
}
}
}

View file

@ -15,7 +15,8 @@ namespace Asterisk.NET.FastAGI
/// </pre>
/// NoopCommand must implement the AGIScript interface and have a default constructor with no parameters.<br/>
/// </summary>
public class MappingStrategy
[Obsolete("This class has been depreciated in favour of MappingStrategies.ResourceMappingStrategy", false)]
public class MappingStrategy : IMappingStrategy
{
#if LOGGER
private Logger logger = Logger.Instance();
@ -35,7 +36,7 @@ namespace Asterisk.NET.FastAGI
this.mapping = null;
}
internal AGIScript DetermineScript(AGIRequest request)
public AGIScript DetermineScript(AGIRequest request)
{
AGIScript script = null;
if (mapping != null)
@ -64,7 +65,7 @@ namespace Asterisk.NET.FastAGI
}
}
internal void Load()
public void Load()
{
string scriptName;
string className;
@ -82,7 +83,7 @@ namespace Asterisk.NET.FastAGI
{
scriptName = (string)de.Key;
className = (string)de.Value;
agiScript = createAGIScriptInstance(className);
agiScript = CreateAGIScriptInstance(className);
if(mapping.Contains(scriptName))
throw new AGIException(String.Format("Duplicate mapping name '{0}' in file {1}", scriptName, resourceName));
mapping.Add(scriptName, agiScript);
@ -101,7 +102,7 @@ namespace Asterisk.NET.FastAGI
}
}
private AGIScript createAGIScriptInstance(string className)
private AGIScript CreateAGIScriptInstance(string className)
{
Type agiScriptClass;
ConstructorInfo constructor;

View file

@ -81,6 +81,7 @@ namespace Asterisk.NET.Manager
public delegate void ZapShowChannelsCompleteEventHandler(object sender, Event.ZapShowChannelsCompleteEvent e);
public delegate void ZapShowChannelsEventHandler(object sender, Event.ZapShowChannelsEvent e);
public delegate void ConnectionStateEventHandler(object sender, Event.ConnectionStateEvent e);
public delegate void VarSetEventHandler(object sender, Event.VarSetEvent e);
#endregion
@ -414,6 +415,11 @@ namespace Asterisk.NET.Manager
/// </summary>
public event ConnectionStateEventHandler ConnectionState;
/// <summary>
/// When a variable is set
/// </summary>
public event VarSetEventHandler VarSet;
#endregion
#region Constructor - ManagerConnection()
@ -506,6 +512,8 @@ namespace Asterisk.NET.Manager
Helper.RegisterEventHandler(registeredEventHandlers, 64, typeof(TransferEvent));
Helper.RegisterEventHandler(registeredEventHandlers, 65, typeof(DTMFEvent));
Helper.RegisterEventHandler(registeredEventHandlers, 70, typeof(VarSetEvent));
#endregion
@ -1050,6 +1058,12 @@ namespace Asterisk.NET.Manager
{
DTMF(this, (DTMFEvent)e);
}
break;
case 70:
if (VarSet != null)
{
VarSet(this, (VarSetEvent)e);
}
break;
default:
@ -1352,6 +1366,12 @@ namespace Asterisk.NET.Manager
return Manager.AsteriskVersion.ASTERISK_1_6;
else if (version.StartsWith("1.8."))
return Manager.AsteriskVersion.ASTERISK_1_8;
else if (version.StartsWith("10."))
return Manager.AsteriskVersion.ASTERISK_10;
else if (version.StartsWith("11."))
return Manager.AsteriskVersion.ASTERISK_11;
else if (version.StartsWith("12."))
return Manager.AsteriskVersion.ASTERISK_12;
else
throw new ManagerException("Unknown Asterisk version " + version);
}

View file

@ -1,3 +1,14 @@
21.08.2013 (skrusty)
Added VarSetEventHandler (added as contribution by bacobart)
Added IMappingStrategy to allow different mapping strategies to be created (added as contribution by bacobart) (*note to get documentation added for this)
Added SC_DEAD_CHANNEL result code handling (added as contribution by nuronce, as per work item 1163)
Added SC511_CAUSES_EXCEPTION and SCHANGUP_CAUSES_EXCEPTION flags to enable new behaviour (as per item 1163)
Added new GeneralMappingStrategy class to handle simpler script mappings
Added Obsolete attribute to MappingStrategy and created a new ResourceMappingStrategy to replace it
Fixed Version Parsing for versions later than Asterisk 10 (added as contribution by bacobart)
Changed Test project to use new GeneralMappingStrategy for simpler code and readability, left the existing resource file in for backwards compatibility
28.05.2013 (skrusty)
Fix Fixed issue with SendEventGeneratingAction, see work item: 1000 (https://asternet.codeplex.com/workitem/1000)