using System;
using System.Collections.Generic;
using System.Text;
using System.Globalization;
using Amundsen.Utilities; // http://code.google.com/u/mca.amundsen/updates
using System.Net;
using System.Collections;
using System.Text.RegularExpressions;
using System.Web;
using System.Xml;
namespace Finsel.AzureCommands
{
///
/// The types of command: Delete, Get, Post, Put
///
public enum cmdType
{
///
/// Delete
///
delete,
///
/// Get
///
get,
///
/// Post used for create
///
post,
///
/// Put used for update
///
put,
///
/// Merge
///
merge,
///
/// Head calls return just header information from a GET
///
head
}
///
/// Authentication object containing everything necessary
/// to authenticate against the service
///
public struct Authentication
{
///
/// The account is generally the first part of the Endpoint.
///
public string Account;
///
/// The Endpoint for Azure Table Storage as defined for your account
/// (This will be in the format http://{0}.{1}.core.windows.net/
/// where 0 is your Account and 1 is the service your using
///
public string EndPoint;
///
/// The SharedKey for access
///
public string SharedKey;
///
/// Shared
///
public string KeyType;
///
/// The information used for authentication
///
/// The account name is the first subdomain in your endpoint list.
/// http://myaccount.blob.core.windows.net/ would use myaccount as the account name.
/// Endpoint. This is actually calculated currently but could change in future releases.
/// The shared key, either primary or secondary.
public Authentication(string pAccount, string pEndpoint, string pSharedKey)
{
Account = pAccount;
EndPoint = pEndpoint;
SharedKey = pSharedKey;
KeyType = "SharedKey";
}
}
///
/// Contains results about requests
///
public struct azureResults
{
///
/// The httpResponse body
///
public string Body;
///
/// The CanonicalResource as required for authentication
///
public string CanonicalResource;
///
/// The actual Url that was called.
///
public string Url;
///
/// The HttpStatusCode returned
///
public System.Net.HttpStatusCode StatusCode;
///
/// Whether or not the request was successful. This is set based on
/// the value that the API says should be returned in StatusCode
///
public bool Succeeded;
///
/// The httpResponse headers collection
///
public System.Collections.Hashtable Headers;
}
class azureCommon
{
///
/// ParsedURL represents the domain/folder/page structure used for lookups.
/// Regex help from http://www.cambiaresearch.com/cambia3/snippets/csharp/regex/uri_regex.aspx#parsing
///
struct ParsedURI
{
public string domainName,
folderName,
pageName;
public ParsedURI(string URL)
{
string regexPattern = @"^(?(?[^:/\?#]+):)?(?"
+ @"//(?[^/\?#]*))?(?[^\?#]*)"
+ @"(?\?(?[^#]*))?"
+ @"(?#(?.*))?";
Regex re = new Regex(regexPattern, RegexOptions.ExplicitCapture);
Match m = re.Match(URL);
domainName = m.Groups["a0"].Value;// +" (Authority without //)
";
folderName = m.Groups["p0"].Value.Substring(0, m.Groups["p0"].Value.LastIndexOf("/")); // +" (Path)
";
pageName = m.Groups["p0"].Value.Substring(m.Groups["p0"].Value.LastIndexOf("/") + 1);
// Note: The passed in URL should never have arguments built in but, if it does, this strips
// them out.
//if (pageName.IndexOf(",") != -1)
// pageName = pageName.Substring(0, pageName.IndexOf(",")) + pageName.Substring(pageName.LastIndexOf("."));
}
}
Hashing h = new Hashing();
public System.Collections.Hashtable Headers2Hash(WebHeaderCollection coll)
{
System.Collections.Hashtable retVal = new System.Collections.Hashtable();
// We need to get headers
foreach (string header in coll)
{
retVal.Add(header, coll[header]);
}
return retVal;
}
public string CreateSharedKeyAuth(string method, string resource, string contentMD5, DateTime requestDate, HttpWebRequest client, Authentication auth)
{
Hashtable ht = new Hashtable();
StringBuilder sbHdrs = new StringBuilder();
StringBuilder sbHeader = new StringBuilder();
string rtn = string.Empty;
string fmtStringToSign = "{0}\n{1}\n{2}\n{3:R}\n{4}{5}";
string hdrs = CanonicalizeHeaders(client.Headers);
string authValue = string.Format(fmtStringToSign, method, contentMD5, client.ContentType, "", hdrs, resource);
byte[] signatureByteForm = System.Text.Encoding.UTF8.GetBytes(authValue);
System.Security.Cryptography.HMACSHA256 hasher = new System.Security.Cryptography.HMACSHA256(Convert.FromBase64String(auth.SharedKey));
// Now build the Authorization header
String authHeader = String.Format(CultureInfo.InvariantCulture,
"{0} {1}:{2}",
"SharedKey",
auth.Account,
System.Convert.ToBase64String(hasher.ComputeHash(signatureByteForm)
));
rtn = authHeader;
return rtn;
}
public string CreateSharedKeyAuth(string method, string resource, string contentMD5, DateTime requestDate, HttpClient client, Authentication auth)
{
return CreateSharedKeyAuth(method, resource, contentMD5, requestDate, client, "", auth);
}
/*
Constructing the CanonicalizedHeaders Element
To construct the CanonicalizedHeaders portion of the string required for the signature, follow these steps:
1. Retrieve all headers for the resource that begin with x-ms-, including the x-ms-date header.
2. Convert each HTTP header name to lowercase.
3. Sort the container of headers lexicographically by header name, in ascending order.
4. Combine headers with the same name into one header. The resulting header should be a name-value pair of the format "header-name:comma-separated-value-list", without any white space between values. Important The comma-separated list of headers is not ordered by the header values but by the order in which the headers appear in the request. The list of headers must be in the correct order to properly authenticate the request.
5. Replace any breaking white space with a single space.
6. Trim any white space around the colon in the header.
7. Finally, append a new line character to each canonicalized header in the resulting list. Construct the CanonicalizedHeaders element by concatenating all headers in this list into a single string.
*/
public string CanonicalizeHeaders(WebHeaderCollection hdrCollection)
{
StringBuilder retVal = new StringBuilder();
// Look for header names that start with "x-ms-"
// Then sort them in case-insensitive manner.
ArrayList httpStorageHeaderNameArray = new ArrayList();
Hashtable ht = new Hashtable();
foreach (string key in hdrCollection.AllKeys)
{
if (key.ToLowerInvariant().StartsWith("x-ms-", StringComparison.Ordinal))
{
if (ht.Contains(key.ToLowerInvariant()))
{
ht[key.ToLowerInvariant()] = string.Format("{0},{1}", ht[key.ToLowerInvariant()], hdrCollection[key].ToString().Replace("\n", string.Empty).Replace("\r", string.Empty).Trim());
}
else
{
httpStorageHeaderNameArray.Add(key.ToLowerInvariant());
ht.Add(key.ToLowerInvariant(), hdrCollection[key].ToString().Replace("\n", string.Empty).Replace("\r", string.Empty).Trim());
}
}
}
httpStorageHeaderNameArray.Sort();
// Now go through each header's values in the sorted order and append them to the canonicalized string.
foreach (string key in httpStorageHeaderNameArray)
{
retVal.AppendFormat("{0}:{1}\n", key.Trim(), ht[key].ToString());
}
return retVal.ToString();
}
private Hashtable parseQueryString(string qstring)
{
//simplify our task
qstring = qstring + "&";
Hashtable outc = new Hashtable();
Regex r = new Regex(@"(?[^=&]+)=(?[^&]+)&", RegexOptions.IgnoreCase | RegexOptions.Compiled);
IEnumerator _enum = r.Matches(qstring).GetEnumerator();
while (_enum.MoveNext() && _enum.Current != null)
{
outc.Add(((Match)_enum.Current).Result("${name}"),
((Match)_enum.Current).Result("${value}"));
}
return outc;
}
public string CanonicalizeUrl(string Url)
{
string retVal = string.Empty;
ParsedURI pURI = new ParsedURI(Url);
string canonicalParameters = string.Empty;
System.Collections.Hashtable NVPParameters = new System.Collections.Hashtable();
string[] subdomainCollection = pURI.domainName.Split(".".ToCharArray());
String querystring = null;
int iqs = Url.IndexOf('?');
if (iqs >= 0)
{
querystring = (iqs < Url.Length - 1) ? Url.Substring(iqs + 1) : String.Empty;
Hashtable qscoll = parseQueryString (querystring);
foreach (DictionaryEntry de in qscoll )
{
if (de.Key.ToString() == "comp")
//if (canonicalParameters == string.Empty)
// canonicalParameters = "?";
canonicalParameters += string.Format("{0}={1}", de.Key.ToString().ToLower().Replace("?",""), de.Value);
}
}
retVal = string.Format("/{0}{1}", subdomainCollection[0], pURI.folderName );
//if (pURI.pageName != string.Empty)
retVal = string.Format("{0}/{1}", retVal, pURI.pageName);
if (canonicalParameters != string.Empty)
{
retVal = string.Format("{0}?{1}", retVal, canonicalParameters);
}
return retVal;
}
public string CreateSharedKeyAuth(string method, string resource, string contentMD5, DateTime requestDate, HttpClient client, string contentType, Authentication auth)
{
Hashtable ht = new Hashtable();
StringBuilder sbHdrs = new StringBuilder();
StringBuilder sbHeader = new StringBuilder();
string rtn = string.Empty;
string fmtStringToSign = "{0}\n{1}\n{2}\n{3:R}\n{4}{5}";
string hdr = CanonicalizeHeaders(client.RequestHeaders);
string authValue = string.Format(fmtStringToSign, method, contentMD5, contentType, "", hdr, resource);
byte[] signatureByteForm = System.Text.Encoding.UTF8.GetBytes(authValue);
System.Security.Cryptography.HMACSHA256 hasher = new System.Security.Cryptography.HMACSHA256(Convert.FromBase64String(auth.SharedKey));
// Now build the Authorization header
String authHeader = String.Format(CultureInfo.InvariantCulture,
"{0} {1}:{2}",
"SharedKey",
auth.Account,
System.Convert.ToBase64String(hasher.ComputeHash(signatureByteForm)
));
rtn = authHeader;
return rtn;
}
public string CreateSharedKeyAuthLite(string method, string resource, string contentMD5, DateTime requestDate, string contentType, Authentication auth)
{
string rtn = string.Empty;
string fmtHeader = "{0} {1}:{2}";
string fmtStringToSign = "{0}\n{1}\n{2}\n{3:R}\n{4}";
string authValue = string.Format(fmtStringToSign, method, contentMD5, contentType, requestDate, resource);
string sigValue = h.MacSha(authValue, Convert.FromBase64String(auth.SharedKey));
rtn = string.Format(fmtHeader, auth.KeyType, auth.Account, sigValue);
return rtn;
}
}
///
/// For making calls directly to the Azure Platform with headers and body supplied
///
public class azureDirect
{
HttpClient client = new HttpClient();
Hashing h = new Hashing();
azureCommon ac = new azureCommon();
///
/// Defines the UserAgent string passed in when making HTTP requests
///
string UserAgent = "amundsen-finsel/1.0";
///
/// Create a new AzureCommands object
///
public azureDirect()
{
client.UserAgent = UserAgent;
}
///
/// Create a new AzureCommands object with default settings
///
/// The account is generally the first part of the Endpoint.
/// The Endpoint for Azure Table Storage as defined for your account
/// The SharedKey for access
/// Shared
public azureDirect(string account, string endPoint, string sharedKey, string keyType)
{
auth = new Authentication(account, endPoint, sharedKey);
client.UserAgent = UserAgent;
}
///
/// Create a new AzureCommands object with default settings
///
/// An Azure Authentication Object
public azureDirect(Authentication pAuth)
{
auth = pAuth ;
client.UserAgent = UserAgent;
}
///
/// The authentication object for the Azure Table Storage
///
public Authentication auth = new Authentication();
///
/// Send a request to Azure, adding only the authetication
///
/// Method to execute
/// URL to execute against
/// Body to send
/// Headers to send
/// An Azure Result object
public azureResults ProcessRequest(cmdType cmd, string requestUrl, string body, Hashtable headers)
{
return ProcessRequest(cmd, requestUrl, new System.Text.ASCIIEncoding().GetBytes(body), headers);
}
///
/// Send a request to Azure, adding only the authetication
///
/// Method to execute
/// URL to execute against
/// Body to send
/// Headers to send
/// An Azure Result object
public azureResults ProcessRequest(cmdType cmd, string requestUrl, byte[] body, Hashtable headers)
{
Hashing h = new Hashing();
HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(requestUrl);
req.Method = cmd.ToString().ToUpper();
azureResults retVal = new azureResults();
try
{
StringBuilder sb = new StringBuilder();
string contentMD5 = string.Empty ;
DateTime requestDate = DateTime.UtcNow;
retVal.CanonicalResource = ac.CanonicalizeUrl(requestUrl);
string contentType = string.Empty;
req.ContentLength = body.Length;
if (headers.ContainsKey("Content-Type"))
{
req.ContentType = headers["Content-Type"].ToString().Replace("\r", "");
headers.Remove("Content-Type");
}
foreach (DictionaryEntry de in headers)
{
req.Headers[de.Key.ToString()] = de.Value.ToString();
}
req.Headers["x-ms-version"] = "2009-07-17";
req.Headers["x-ms-date"] = string.Format(CultureInfo.CurrentCulture, "{0:R}", requestDate);
string authHeader = string.Empty;
if (requestUrl.Contains(".table."))
authHeader = ac.CreateSharedKeyAuthLite(req.Method, retVal.CanonicalResource, contentMD5, requestDate, req.ContentType, auth);
else
authHeader = ac.CreateSharedKeyAuth(req.Method, retVal.CanonicalResource, contentMD5, requestDate, req, auth);
req.Headers["authorization"] = authHeader;
retVal.Url = requestUrl;
if (body.Length > 0)
{
System.IO.Stream requestStream = req.GetRequestStream();
requestStream.Write(body, 0, body.Length);
requestStream.Flush();
}
HttpWebResponse response = (HttpWebResponse)req.GetResponse();
// response = (HttpWebResponse)req.GetResponse();
System.IO.StreamReader sr = new System.IO.StreamReader(response.GetResponseStream());
retVal.Body = sr.ReadToEnd();
response.Close();
retVal.StatusCode = response.StatusCode;
retVal.Headers = ac.Headers2Hash(response.Headers);
}
catch (WebException wex)
{
retVal.StatusCode = ((System.Net.HttpWebResponse)(wex.Response)).StatusCode;
//retVal.StatusCode = wex. (HttpStatusCode)hex.GetHttpCode();
retVal.Succeeded = false;
retVal.Body = wex.Message;
}
catch (Exception ex)
{
retVal.StatusCode = HttpStatusCode.SeeOther;
retVal.Body = ex.ToString();
retVal.Succeeded = false;
}
return retVal;
}
}
///
/// A helper class that handles special cases
///
public class azureHelper
{
HttpClient client = new HttpClient();
Hashing h = new Hashing();
azureCommon ac = new azureCommon();
///
/// Defines the UserAgent string passed in when making HTTP requests
///
string UserAgent = "amundsen-finsel/1.0";
///
/// Create a new AzureCommands object
///
public azureHelper()
{
client.UserAgent = UserAgent;
}
///
/// Create a new AzureCommands object with default settings
///
/// The account is generally the first part of the Endpoint.
/// The Endpoint for Azure Table Storage as defined for your account
/// The SharedKey for access
/// Shared
public azureHelper(string account, string endPoint, string sharedKey, string keyType)
{
auth = new Authentication(account, endPoint, sharedKey);
client.UserAgent = UserAgent;
}
///
/// Create a new AzureCommands object with default settings
///
/// Azure Authetication Object
public azureHelper(Authentication pAuth)
{
auth = pAuth;
client.UserAgent = UserAgent;
}
///
/// The authentication object for the Azure Table Storage
///
public Authentication auth = new Authentication();
///
/// Process Entity Group Transactions against a table by taking an XML block of Entity Data and turning it into Multipart and executing it.
///
/// Method to execute
/// Table to execute the data against
/// Entity Data
/// string with results of the group transaction
public string entityGroupTransaction(cmdType cmd,string tableName, string entityData)
{
int entityCounter = 1;
StringBuilder sbResults = new StringBuilder();
StringBuilder sbMultiPart = new StringBuilder();
string singlePart = string.Empty;
string PartitionKey = string.Empty;
string oldPartitionKey = string.Empty;
string boundaryIdentifier = Guid.NewGuid().ToString();
string batchIdentifier = Guid.NewGuid().ToString();
string contentType = string.Format("multipart/mixed; boundary=batch_{0}", boundaryIdentifier);
azureResults ar = new azureResults();
DateTime requestDate = DateTime.UtcNow;
Hashtable headers = new Hashtable();
headers.Add("x-ms-version", "2009-04-14");
headers.Add("Content-Type", contentType);
azureDirect ad = new azureDirect(auth.Account, "", auth.SharedKey, auth.KeyType);
XmlDocument msgDoc = new XmlDocument();
//Instantiate an XmlNamespaceManager object.
System.Xml.XmlNamespaceManager xmlnsManager = new System.Xml.XmlNamespaceManager(msgDoc.NameTable);
//Add the namespaces used in books.xml to the XmlNamespaceManager.
xmlnsManager.AddNamespace("d", "http://schemas.microsoft.com/ado/2007/08/dataservices");
xmlnsManager.AddNamespace("m", "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata");
msgDoc.LoadXml(entityData);
XmlNodeList pnodes = msgDoc.SelectNodes("//m:properties", xmlnsManager);
foreach (XmlNode pnode in pnodes)
{
PartitionKey = pnode.SelectSingleNode("d:PartitionKey", xmlnsManager).InnerText;
string RowKey = pnode.SelectSingleNode("d:RowKey", xmlnsManager).InnerText;
if (PartitionKey != oldPartitionKey || entityCounter > 98)
{
if (sbMultiPart.Length > 0)
{
sbMultiPart.AppendFormat("--changeset_{0}--\n--batch_{1}--", batchIdentifier, boundaryIdentifier);
ar = ad.ProcessRequest(cmdType.post, string.Format("http://{0}.table.core.windows.net/$batch", auth.Account), new System.Text.ASCIIEncoding().GetBytes(sbMultiPart.ToString()), headers);
sbResults.AppendLine(ar.Body);
}
sbMultiPart = new StringBuilder();
boundaryIdentifier = Guid.NewGuid().ToString();
batchIdentifier = Guid.NewGuid().ToString();
contentType = string.Format("multipart/mixed; boundary=batch_{0}", boundaryIdentifier);
headers["Content-Type"] = contentType;
sbMultiPart.AppendFormat(batchTemplate, boundaryIdentifier,batchIdentifier );
sbMultiPart.AppendLine();
sbMultiPart.AppendLine();
entityCounter = 1;
oldPartitionKey = PartitionKey;
}
string postUrl =string.Format("http://{0}.table.core.windows.net/{1}", auth.Account,tableName);
if (cmd != cmdType.post)
postUrl = string.Format("{0}(PartitionKey='{1}',RowKey='{2}')", postUrl, PartitionKey, RowKey);
string atomData = string.Format(createEntityXML, requestDate, pnode.OuterXml);
singlePart = string.Format(entityTemplate, batchIdentifier, cmd.ToString().ToUpper(), postUrl , entityCounter,atomData.Length, atomData );
sbMultiPart.AppendLine(singlePart);
entityCounter++;
}
sbMultiPart.AppendFormat("--changeset_{0}--\n--batch_{1}--", batchIdentifier, boundaryIdentifier);
ar = ad.ProcessRequest(cmdType.post, string.Format("http://{0}.table.core.windows.net/$batch", auth.Account), new System.Text.ASCIIEncoding().GetBytes(sbMultiPart.ToString()), headers);
sbResults.AppendLine(ar.Body);
return sbResults.ToString();
}
string entityTemplate = @"--changeset_{0}
Content-Type: application/http
Content-Transfer-Encoding: binary
{1} {2} HTTP/1.1
Content-ID: {3}
Content-Type: application/atom+xml;type=entry
Content-Length: {4}
{5}";
string createEntityXML = @"
{0:yyyy-MM-ddTHH:mm:ss.fffffffZ}
{1}
";
string batchTemplate = @"--batch_{0}
Content-Type: multipart/mixed; boundary=changeset_{1}";
}
}