685 lines
24 KiB
C#
685 lines
24 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Xml;
|
|
using System.Security;
|
|
using System.Security.Cryptography;
|
|
using System.Security.Cryptography.Xml;
|
|
using System.Data;
|
|
|
|
//JSON KEY
|
|
using System.Text;
|
|
using Newtonsoft.Json;
|
|
using Org.BouncyCastle.Security;
|
|
using Org.BouncyCastle.Crypto;
|
|
using Org.BouncyCastle.OpenSsl;
|
|
|
|
namespace GroundZero.KeyCodes
|
|
{
|
|
/// <summary>
|
|
/// AyaNova .net key reader and validator
|
|
/// Used to rehydrate a key from a past key for add-on / subscription renewal etc
|
|
/// </summary>
|
|
public class KeyReader
|
|
{
|
|
|
|
private bool _IsValid;
|
|
private string _RegisteredTo;
|
|
private int _ScheduleableUsers;
|
|
// private bool _FeatureWebInterface;
|
|
private object _Expires;
|
|
private string _status;
|
|
private DateTime _Generated;
|
|
private DateTime _InstallableUntil;
|
|
private bool _Installable;
|
|
private double _SchemaVersion;
|
|
|
|
//private bool _FeatureMBIInterface;
|
|
private bool _RequestedTrial;
|
|
private DataTable dtPlugins;
|
|
private bool _Lite;
|
|
private bool _SubscriptionLicense;
|
|
private bool _Lockout;
|
|
private DateTime _LockoutDate;
|
|
|
|
|
|
|
|
|
|
|
|
public string RegisteredTo
|
|
{
|
|
get
|
|
{
|
|
return _RegisteredTo;
|
|
}
|
|
|
|
}
|
|
|
|
public string Status
|
|
{
|
|
get
|
|
{
|
|
return _status;
|
|
}
|
|
|
|
}
|
|
|
|
public int ScheduleableUsers
|
|
{
|
|
get
|
|
{
|
|
return _ScheduleableUsers;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
public bool IsValid
|
|
{
|
|
get
|
|
{
|
|
return _IsValid;
|
|
}
|
|
|
|
}
|
|
|
|
public DateTime Generated
|
|
{
|
|
get
|
|
{
|
|
return _Generated;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
public object Expires
|
|
{
|
|
get
|
|
{
|
|
return _Expires;
|
|
}
|
|
|
|
}
|
|
|
|
public DateTime InstallableUntil
|
|
{
|
|
get
|
|
{
|
|
return _InstallableUntil;
|
|
}
|
|
|
|
}
|
|
|
|
public bool Installable
|
|
{
|
|
get
|
|
{
|
|
return _Installable;
|
|
}
|
|
|
|
}
|
|
|
|
public double SchemaVersion
|
|
{
|
|
get
|
|
{
|
|
return _SchemaVersion;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public bool RequestedTrial
|
|
{
|
|
get
|
|
{
|
|
return _RequestedTrial;
|
|
}
|
|
|
|
}
|
|
|
|
public DataTable Plugins
|
|
{
|
|
get
|
|
{
|
|
return dtPlugins;
|
|
}
|
|
}
|
|
|
|
|
|
public bool Lite
|
|
{
|
|
get
|
|
{
|
|
return _Lite;
|
|
}
|
|
}
|
|
|
|
public bool SubscriptionLicense
|
|
{
|
|
get
|
|
{
|
|
return _SubscriptionLicense;
|
|
}
|
|
}
|
|
|
|
public bool Lockout
|
|
{
|
|
get
|
|
{
|
|
return _Lockout;
|
|
}
|
|
}
|
|
|
|
public DateTime LockoutDate
|
|
{
|
|
get
|
|
{
|
|
return _LockoutDate;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public KeyReader(string Key)
|
|
{
|
|
_IsValid = false;
|
|
// _FeatureWebInterface = false;
|
|
_Installable = false;
|
|
|
|
|
|
this._RegisteredTo = "";
|
|
this._ScheduleableUsers = 0;
|
|
this._SchemaVersion = 0;
|
|
this._status = "";
|
|
this._Expires = System.DateTime.Now;
|
|
_RequestedTrial = false;
|
|
this._SubscriptionLicense = false;//default for old license processing
|
|
_Lockout = false;
|
|
_LockoutDate = DateTime.MaxValue;
|
|
|
|
dtPlugins = new DataTable();
|
|
dtPlugins.Columns.Add("Plugin");
|
|
dtPlugins.Columns.Add("Version");
|
|
//case 2094
|
|
dtPlugins.Columns.Add("SubscriptionExpires");
|
|
_Installable = false;
|
|
|
|
|
|
|
|
//Ensure we have a key and strip out any unnecessary text
|
|
string sKeyType = "AyaNovaLiteLicenseKey";
|
|
_Lite = true;
|
|
if (Key.Contains("AyaNovaLicenseKey"))
|
|
{
|
|
sKeyType = "AyaNovaLicenseKey";
|
|
_Lite = false;
|
|
}
|
|
|
|
|
|
bool containsXML = Key.Contains("<?xml version=\"1.0\" encoding=\"utf-16\" standalone=\"yes\"?>");
|
|
bool containsJSON = Key.Contains("[KEY");
|
|
|
|
if (!containsXML && !containsJSON)
|
|
{
|
|
_IsValid = false;
|
|
_status = "Error: could not find license key in text provided";
|
|
return;
|
|
}
|
|
|
|
|
|
if (containsXML)
|
|
{
|
|
#region PARSE XML KEY
|
|
int nStart = Key.IndexOf("<?xml version=\"1.0\" encoding=\"utf-16\" standalone=\"yes\"?>");
|
|
int nEnd = Key.IndexOf("</" + sKeyType + ">");
|
|
if (nEnd == -1 || nStart == -1 || nEnd < nStart)
|
|
{
|
|
_IsValid = false;
|
|
_status = "Error: could not find license XML in license key provided";
|
|
|
|
return;
|
|
|
|
}
|
|
Key = Key.Substring(nStart, (nEnd + (("</" + sKeyType + ">").Length)) - nStart);
|
|
|
|
|
|
//validate key signature then get settings within key
|
|
// Get the XML content from the embedded XML public key.
|
|
Stream s = null;
|
|
string xmlkey = string.Empty;
|
|
try
|
|
{
|
|
|
|
// System.Reflection.Assembly ass=System.Reflection.Assembly.GetExecutingAssembly();
|
|
// string [] items=ass.GetManifestResourceNames();
|
|
// foreach(string ss in items)
|
|
// {
|
|
// string xy=ss;
|
|
// string ab=xy;
|
|
//
|
|
// }
|
|
|
|
System.Reflection.Assembly a = typeof(KeyReader).Assembly;
|
|
|
|
s = a.GetManifestResourceStream("GroundZero.KeyCodes.pubkey.xml");
|
|
|
|
// Read-in the XML content.
|
|
StreamReader reader = new StreamReader(s);
|
|
xmlkey = reader.ReadToEnd();
|
|
reader.Close();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_IsValid = false;
|
|
_status = "Error: could not import public key. " + ex.Message + "\r\n" + ex.InnerException;
|
|
|
|
return;
|
|
}
|
|
|
|
// Create an RSA crypto service provider from the embedded
|
|
// XML document resource (the public key).
|
|
RSACryptoServiceProvider csp = new RSACryptoServiceProvider();
|
|
csp.FromXmlString(xmlkey);
|
|
|
|
// Load the signed XML license file.
|
|
XmlDocument xmldoc = new XmlDocument();
|
|
try
|
|
{
|
|
xmldoc.LoadXml(Key);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_IsValid = false;
|
|
_status = "Error: exception in LoadXml:\r\n." + ex.Message + "\r\n" + ex.InnerException;
|
|
return;
|
|
|
|
}
|
|
|
|
// Create the signed XML object.
|
|
SignedXml sxml = new SignedXml(xmldoc);
|
|
|
|
try
|
|
{
|
|
// Get the XML Signature node and load it into the signed XML object.
|
|
XmlNode dsig = xmldoc.GetElementsByTagName("Signature",
|
|
SignedXml.XmlDsigNamespaceUrl)[0];
|
|
sxml.LoadXml((XmlElement)dsig);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_IsValid = false;
|
|
_status = "Error: no signature found." + ex.Message + "\r\n" + ex.InnerException;
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
// Verify the signature.
|
|
if (!sxml.CheckSignature(csp))
|
|
{
|
|
_IsValid = false;
|
|
_status = "Error: Signature not valid, key has been tampered with.";
|
|
return;
|
|
|
|
}
|
|
|
|
_IsValid = true;
|
|
|
|
|
|
|
|
try
|
|
{
|
|
|
|
_SchemaVersion = XmlConvert.ToDouble(xmldoc.SelectSingleNode("/" + sKeyType + "/SchemaVersion").InnerText);
|
|
_RegisteredTo = xmldoc.SelectSingleNode("/" + sKeyType + "/RegisteredTo").InnerText;
|
|
_ScheduleableUsers = XmlConvert.ToInt32(xmldoc.SelectSingleNode("/" + sKeyType + "/TotalScheduleableUsers").InnerText);
|
|
_Generated = XmlConvert.ToDateTime(xmldoc.SelectSingleNode("/" + sKeyType + "/Created").InnerText, XmlDateTimeSerializationMode.Local);
|
|
_InstallableUntil = XmlConvert.ToDateTime(xmldoc.SelectSingleNode("/" + sKeyType + "/InstallableUntil").InnerText, XmlDateTimeSerializationMode.Local);
|
|
|
|
#region Revoked key check
|
|
string DigestValue = xmldoc.GetElementsByTagName("DigestValue")[0].InnerText;
|
|
switch (DigestValue)
|
|
{
|
|
#region Case 428
|
|
|
|
//<RegisteredTo>Braintek</RegisteredTo>,
|
|
case "mpBdutEgT9w8hWT/T1uwNmLGeLQ=":
|
|
|
|
//<RegisteredTo>MCCAIN RESEARCH LAB , INC</RegisteredTo>,
|
|
case "2pYzGIlMU18QomByUC4D72ViOyE=":
|
|
|
|
//<RegisteredTo>Standby Power System Consultants, Inc.</RegisteredTo>,
|
|
case "PlzMfZoVI4iR0Ws11yqaAf+4g5U=":
|
|
|
|
//<RegisteredTo>Snyder Services</RegisteredTo>,
|
|
case "EMmFEcwm78pskcWAIZ7z5CKdOxk=":
|
|
#endregion
|
|
|
|
#region Case 874
|
|
|
|
//<RegisteredTo>Vrobel's Heating & Cooling</RegisteredTo>,
|
|
case "dhkBn04QOYxysTnXTql/9VHkEtI=":
|
|
#endregion
|
|
|
|
#region Case 1167
|
|
//<RegisteredTo>Verhoeven kantoortotaal</RegisteredTo>
|
|
case "b75SpAQzm71JsRLNtosk0ty1Tsw=":
|
|
|
|
#endregion
|
|
|
|
#region Case 1191
|
|
// <RegisteredTo>CF Can Group</RegisteredTo>
|
|
case "i9/LC/6RVsoIT1X5YKlzBRA2T08=":
|
|
|
|
#endregion
|
|
|
|
#region Case 1364
|
|
|
|
|
|
// <RegisteredTo>Leavenworth County</RegisteredTo>
|
|
case "W2inW3VDw8FPzMdBHgE8k+U6gSg=":
|
|
|
|
// <RegisteredTo>mbernardinelli</RegisteredTo>
|
|
case "13OUStnOVvlJszgrUIcVxMcLkhw=":
|
|
|
|
// <RegisteredTo>Penney Lawn Service</RegisteredTo>
|
|
case "OSQOreX+pT0TczLrxb6ucOv4rF8=":
|
|
|
|
#endregion
|
|
|
|
#region Case 1862
|
|
// <RegisteredTo>Morris Waterworks</RegisteredTo>
|
|
case "8lPDmvrFCHnVxUDGzscSZKmGtgM=":
|
|
#endregion
|
|
|
|
_IsValid = false;
|
|
_status = "Error: Key is not valid due to NON-PAYMENT revoke list.";
|
|
return;
|
|
|
|
}
|
|
#endregion revoked key check
|
|
|
|
if (xmldoc.SelectSingleNode("/" + sKeyType + "/Expires") != null)
|
|
_Expires = XmlConvert.ToDateTime(xmldoc.SelectSingleNode("/" + sKeyType + "/Expires").InnerText, XmlDateTimeSerializationMode.Local);
|
|
else
|
|
_Expires = null;
|
|
|
|
|
|
|
|
//Case 999
|
|
if (xmldoc.SelectSingleNode("/" + sKeyType + "/RequestedTrial") != null)
|
|
_RequestedTrial = XmlConvert.ToBoolean(xmldoc.SelectSingleNode("/" + sKeyType + "/RequestedTrial").InnerText);
|
|
else
|
|
_RequestedTrial = false;
|
|
|
|
|
|
//Case 2094
|
|
if (xmldoc.SelectSingleNode("/" + sKeyType + "/Sub") != null)
|
|
_SubscriptionLicense = XmlConvert.ToBoolean(xmldoc.SelectSingleNode("/" + sKeyType + "/Sub").InnerText);
|
|
else
|
|
{
|
|
_SubscriptionLicense = false;
|
|
//default expiry to yesterday on v7 keys forcing new date to be selected in UI
|
|
_Expires = DateTime.Now.AddDays(-1);
|
|
}
|
|
|
|
//case 2094
|
|
if (xmldoc.SelectSingleNode("/" + sKeyType + "/LockDate") != null)
|
|
{
|
|
_Lockout = true;
|
|
_LockoutDate = XmlConvert.ToDateTime(xmldoc.SelectSingleNode("/" + sKeyType + "/LockDate").InnerText, XmlDateTimeSerializationMode.Local);
|
|
}
|
|
else
|
|
{
|
|
_Lockout = false;
|
|
_LockoutDate = DateTime.MaxValue;
|
|
}
|
|
|
|
|
|
//Case 2094
|
|
//deal with v7.3 licenses WBI
|
|
bool hasv7wbi = false;
|
|
if (!_SubscriptionLicense)
|
|
{
|
|
if (xmldoc.SelectSingleNode("/" + sKeyType + "/FeatureWebBrowserInterface") != null)
|
|
hasv7wbi = XmlConvert.ToBoolean(xmldoc.SelectSingleNode("/" + sKeyType + "/FeatureWebBrowserInterface").InnerText);
|
|
else
|
|
{
|
|
hasv7wbi = false;
|
|
//default expiry to yesterday on v7 keys forcing new date to be selected in UI
|
|
_Expires = DateTime.Now.AddDays(-1);
|
|
}
|
|
}
|
|
if (hasv7wbi)
|
|
{
|
|
//add it as a v7.4 plugin
|
|
DataRow dr = dtPlugins.NewRow();
|
|
dr["Plugin"] = "WBI - Web browser interface";
|
|
dr["SubscriptionExpires"] = DateTime.Now.AddDays(-1);
|
|
dtPlugins.Rows.Add(dr);
|
|
}
|
|
|
|
|
|
//Case 2094
|
|
//deal with v7.3 license MBI
|
|
bool hasv7mbi = false;
|
|
if (!_SubscriptionLicense)
|
|
{
|
|
if (xmldoc.SelectSingleNode("/" + sKeyType + "/FeatureMBIInterface") != null)
|
|
hasv7mbi = XmlConvert.ToBoolean(xmldoc.SelectSingleNode("/" + sKeyType + "/FeatureMBIInterface").InnerText);
|
|
else
|
|
{
|
|
hasv7mbi = false;
|
|
//default expiry to yesterday on v7 keys forcing new date to be selected in UI
|
|
_Expires = DateTime.Now.AddDays(-1);
|
|
}
|
|
}
|
|
if (hasv7mbi)
|
|
{
|
|
//add it as a v7.4 plugin
|
|
DataRow dr = dtPlugins.NewRow();
|
|
dr["Plugin"] = "MBI - Minimal browser interface";
|
|
dr["SubscriptionExpires"] = DateTime.Now.AddDays(-1);
|
|
dtPlugins.Rows.Add(dr);
|
|
}
|
|
|
|
|
|
|
|
|
|
//case 1053 plugins
|
|
XmlNodeList plugs = xmldoc.SelectNodes("//Plugin");
|
|
if (plugs != null && plugs.Count > 0)
|
|
{
|
|
foreach (XmlNode n in plugs)
|
|
{
|
|
DataRow dr = dtPlugins.NewRow();
|
|
dr["Plugin"] = n.SelectSingleNode("Item").InnerText;
|
|
|
|
//case 2094 SubscriptionExpires key
|
|
if (_SubscriptionLicense)
|
|
{
|
|
if (n.SelectSingleNode("SubscriptionExpires") != null)
|
|
dr["SubscriptionExpires"] = XmlConvert.ToDateTime(n.SelectSingleNode("SubscriptionExpires").InnerText, XmlDateTimeSerializationMode.Local);
|
|
}
|
|
else
|
|
{
|
|
dr["Version"] = n.SelectSingleNode("Version").InnerText;
|
|
//default expiry to yesterday on v7 keys forcing new date to be selected in UI
|
|
dr["SubscriptionExpires"] = DateTime.Now.AddDays(-1);
|
|
}
|
|
dtPlugins.Rows.Add(dr);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ETC
|
|
_Installable = true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_IsValid = false;
|
|
_status = "Error: Signature not valid - exception processing fields:\r\n." + ex.Message + "\r\n" + ex.InnerException;
|
|
return;
|
|
|
|
}
|
|
#endregion read XML format key
|
|
}
|
|
else
|
|
{
|
|
#region PARSE JSON KEY
|
|
string licenseFileData = Key;
|
|
//extract between [KEY and KEY]
|
|
if (!licenseFileData.Contains("[KEY") ||
|
|
!licenseFileData.Contains("KEY]") ||
|
|
!licenseFileData.Contains("[SIGNATURE") ||
|
|
!licenseFileData.Contains("SIGNATURE]"))
|
|
throw new System.FormatException("KEY IS NOT VALID! Missing one or more required delimiters");
|
|
|
|
string keyNoWS = System.Text.RegularExpressions.Regex.Replace(ExtractString(licenseFileData, "[KEY", "KEY]").Trim(), "(\"(?:[^\"\\\\]|\\\\.)*\")|\\s+", "$1");
|
|
string keySig = ExtractString(licenseFileData, "[SIGNATURE", "SIGNATURE]").Trim();
|
|
|
|
#region Check Signature
|
|
//***** NOTE: this is our real 2016 public key
|
|
var publicPem = @"-----BEGIN PUBLIC KEY-----
|
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz7wrvLDcKVMZ31HFGBnL
|
|
WL08IodYIV5VJkKy1Z0n2snprhSiu3izxTyz+SLpftvKHJpky027ii7l/pL9Bo3J
|
|
cjU5rKrxXavnE7TuYPjXn16dNLd0K/ERSU+pXLmUaVN0nUWuGuUMoGJMEXoulS6p
|
|
JiG11yu3BM9fL2Nbj0C6a+UwzEHFmns3J/daZOb4gAzMUdJfh9OJ0+wRGzR8ZxyC
|
|
99Na2gDmqYglUkSMjwLTL/HbgwF4OwmoQYJBcET0Wa6Gfb17SaF8XRBV5ZtpCsbS
|
|
tkthGeoXZkFriB9c1eFQLKpBYQo2DW3H1MPG2nAlQZLbkJj5cSh7/t1bRF08m6P+
|
|
EQIDAQAB
|
|
-----END PUBLIC KEY-----";
|
|
|
|
|
|
|
|
PemReader pr = new PemReader(new StringReader(publicPem));
|
|
var KeyParameter = (Org.BouncyCastle.Crypto.AsymmetricKeyParameter)pr.ReadObject();
|
|
var signer = SignerUtilities.GetSigner("SHA256WITHRSA");
|
|
signer.Init(false, KeyParameter);
|
|
var expectedSig = Convert.FromBase64String(keySig);
|
|
var msgBytes = Encoding.UTF8.GetBytes(keyNoWS);
|
|
signer.BlockUpdate(msgBytes, 0, msgBytes.Length);
|
|
if (!signer.VerifySignature(expectedSig))
|
|
{
|
|
_IsValid = false;
|
|
_status = "Error: Signature not valid, key has been tampered with.";
|
|
return;
|
|
}
|
|
|
|
#endregion check signature
|
|
|
|
//valid key (has valid signature)
|
|
_IsValid = true;
|
|
try
|
|
{
|
|
#region Get Values
|
|
Newtonsoft.Json.Linq.JToken token = Newtonsoft.Json.Linq.JObject.Parse(keyNoWS);
|
|
_SchemaVersion = (double)token.SelectToken(sKeyType + ".SchemaVersion");
|
|
_RegisteredTo = (string)token.SelectToken(sKeyType + ".RegisteredTo");
|
|
_ScheduleableUsers = (int)token.SelectToken(sKeyType + ".TotalScheduleableUsers");
|
|
_Generated = (DateTime)token.SelectToken(sKeyType + ".Created");
|
|
_InstallableUntil = (DateTime)token.SelectToken(sKeyType + ".InstallableUntil");
|
|
|
|
//REVOKED KEY CHECK HERE IN FUTURE LIKE XML VERSION DOES?
|
|
|
|
if (token.SelectToken(sKeyType + ".Expires") != null)
|
|
{
|
|
_Expires = (DateTime)token.SelectToken(sKeyType + ".Expires");
|
|
if (((DateTime)_Expires).Year == 1)
|
|
throw new System.ArgumentOutOfRangeException("Expires out of range");
|
|
}
|
|
else
|
|
{
|
|
_Expires = DateTime.MaxValue;
|
|
}
|
|
|
|
if (token.SelectToken(sKeyType + ".RequestedTrial") != null)
|
|
{
|
|
_RequestedTrial = (bool)token.SelectToken(sKeyType + ".RequestedTrial");
|
|
}
|
|
else
|
|
{
|
|
_RequestedTrial = false;
|
|
}
|
|
|
|
|
|
if (token.SelectToken(sKeyType + ".Sub") != null)
|
|
{
|
|
_SubscriptionLicense = (bool)token.SelectToken(sKeyType + ".Sub");
|
|
}
|
|
else
|
|
{
|
|
_SubscriptionLicense = false;
|
|
//default expiry to yesterday on v7 keys forcing new date to be selected in UI
|
|
_Expires = DateTime.Now.AddDays(-1);
|
|
}
|
|
|
|
if (token.SelectToken(sKeyType + ".LockDate") != null)
|
|
{
|
|
_Lockout = true;
|
|
_LockoutDate = (DateTime)token.SelectToken(sKeyType + ".LockDate");
|
|
}
|
|
else
|
|
{
|
|
_LockoutDate = DateTime.MaxValue;
|
|
_Lockout = false;
|
|
}
|
|
|
|
//PLUGINS
|
|
Newtonsoft.Json.Linq.JArray p = (Newtonsoft.Json.Linq.JArray)token.SelectToken("AyaNovaLicenseKey.Plugins.Plugin");
|
|
for (int x = 0; x < p.Count; x++)
|
|
{
|
|
DataRow dr = dtPlugins.NewRow();
|
|
|
|
dr["Plugin"] = (string)p[x].SelectToken("Item");
|
|
dr["SubscriptionExpires"] = (DateTime)p[x].SelectToken("SubscriptionExpires");
|
|
dtPlugins.Rows.Add(dr);
|
|
}
|
|
|
|
|
|
|
|
|
|
_Installable = true;
|
|
#endregion get values
|
|
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_IsValid = false;
|
|
_status = "Error: Signature not valid - exception processing fields:\r\n." + ex.Message + "\r\n" + ex.InnerException;
|
|
return;
|
|
|
|
}
|
|
#endregion parse json key
|
|
}
|
|
|
|
|
|
|
|
}//end of function
|
|
|
|
/// <summary>
|
|
/// Extract string between tokens
|
|
/// </summary>
|
|
/// <param name="s"></param>
|
|
/// <param name="openTag"></param>
|
|
/// <param name="closeTag"></param>
|
|
/// <returns></returns>
|
|
string ExtractString(string s, string openTag, string closeTag)
|
|
{
|
|
int startIndex = s.IndexOf(openTag) + openTag.Length;
|
|
int endIndex = s.IndexOf(closeTag, startIndex);
|
|
return s.Substring(startIndex, endIndex - startIndex);
|
|
}
|
|
|
|
//=======================================
|
|
//eoc
|
|
}
|
|
}
|