Sunday, March 27, 2011

C# REST Client for Amazon Route 53

This example implements GET and POST requests for an Amazon Route 53 client in C sharp. The AWS SDK for .NET does not currently support Route 53, and the Perl script tool, called DNSCurl.pl, doesn't work on Windows without considerable adaptation. The code available at http://integral-design.s3.amazonaws.com/CSharp-REST-Client-Amazon-Route53.htm runs the API methods described in the AWS documentation with XML post data, result data, and error messages. 

>>> The following text and code are for search engines.  Formatting is mangled, so use this link for easier reading: http://integral-design.s3.amazonaws.com/CSharp-REST-Client-Amazon-Route53.htm <<<

This code runs the API methods described in the AWS Route 53 API documentation with XML post data, result data, and error messages.
To use any of the Route 53 API methods, you have to create an authentication signature. The Route 53 signature is based on the date and the user’s access key. The following method encrypts the signature using the HMACSHA1 algorithm. The method gets the formatted date as an input parameter from a method described with those following this method. An instance of the System.Security.Cryptography.HMACSHA1 class does the work as a variable named MySigner. After the signature is created, it is base 64 encoded using a static method of the Convert class.

public static string GetAWSR53_SHA1AuthorizationValue(string AWSAccessKeyId, string AWSSecretAccessKey, string AmzDate)

{

System.Security.Cryptography.HMACSHA1 MySigner =

new System.Security.Cryptography.HMACSHA1(System.Text.Encoding.UTF8.GetBytes(AWSSecretAccessKey));


string SignatureValue =

Convert.ToBase64String(MySigner.ComputeHash(System.Text.Encoding.UTF8.GetBytes(AmzDate)));


string AuthorizationValue =

"AWS3-HTTPS AWSAccessKeyId=" + System.Uri.EscapeDataString(AWSAccessKeyId) + ",Algorithm=HmacSHA1,Signature=" + SignatureValue;


return AuthorizationValue;

}


The date is obtained from the Amazon Route 53 API by the following unsecured method.

public static string GetRoute53Date()

{

string url = "https://route53.amazonaws.com/date";

HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;

request.Method = "GET";

HttpWebResponse response;

response = request.GetResponse() as HttpWebResponse;

return response.Headers["Date"];

}


The code for a complete client project is shown below. Each method is a REST implementation that interacts with an AWS Route 53 API method described in the Amazon Web Services documentation. The most interesting in the group is the method ChangeResourceRecordSet, which requires POST data as described in the API documentation topic. To use this method, or for that matter, any of these methods, some research into the Domain Name System (DNS) will be helpful. Amazon’s implementation of ChangeResourceRecordSet uses XML data that is identified by the last parameter of this method, postFile. The data is sent as an HttpWebRequest POST. One thing that can create problems in this procedure is that the request may be initialized automatically to expect a 100 response before it sends the data. This code works around this by inserting the line of code: request.ServicePoint.Expect100Continue = false;

Beyond that, the POST sets up the headers, including the Route 53 date and authentication signature from the methods described above. The POST data is read into a byte array and then into the request stream by the following code segment.


Stream streamPostData = request.GetRequestStream();

streamPostData.Write(data, 0, data.Length);

streamPostData.Close();


The request is attempted and response obtained, if successful, inside the try block. The response or an exception is displayed by the console.

public static void ChangeResourceRecordSet(string accessKeyId, string secretKey, string zoneId, string postFile)

{

string xmlText = System.IO.File.ReadAllText(postFile);

UTF8Encoding encoder = new UTF8Encoding();

byte[] data = encoder.GetBytes(xmlText);


string url = "https://route53.amazonaws.com/2010-10-01/hostedzone/" + zoneId + "/rrset";


Uri uri = new Uri(url);

HttpWebRequest request = WebRequest.Create(uri) as HttpWebRequest;

request.ServicePoint.Expect100Continue = false;


request.Method = "POST";

request.ContentType = "text/xml";

request.ContentLength = data.Length;


WebHeaderCollection headers = request.Headers;

string httpDate = GetRoute53Date();

headers.Add("x-amz-date", httpDate);

string authenticationSig =

GetAWSR53_SHA1AuthorizationValue(accessKeyId, secretKey, httpDate);

headers.Add("X-Amzn-Authorization", authenticationSig);


Stream streamPostData = request.GetRequestStream();

streamPostData.Write(data, 0, data.Length);

streamPostData.Close();


try

{

WebResponse response = request.GetResponse();

Stream stream = response.GetResponseStream() as Stream;

StreamReader streamReader = new StreamReader(stream);

string responseString = streamReader.ReadToEnd();

System.Console.Write(responseString);


stream.Close();

response.Close();

}

catch (WebException ex)

{

Stream stream = ex.Response.GetResponseStream();

byte[] errorBuffer = new byte[stream.Length];

stream.Read(errorBuffer, 0, (Int32)stream.Length);


UTF8Encoding encoding = new UTF8Encoding();

Console.Write(encoding.GetString(errorBuffer));

}

}


The complete client code is shown below. All the methods are explained further in the AWS Route 53 API documentation.

using System;

using System.Text;

using System.Net;

using System.IO;

using System.Security.Cryptography;

using System.Configuration;

using System.Collections.Generic;

using System.Collections.Specialized;

using System.Xml;

using System.Web;


namespace Route53REST

{

class Program

{

static void Main(string[] args)

{

NameValueCollection appConfig = ConfigurationManager.AppSettings;

string accessKey = appConfig["gmdAccessKey"];

string secretKey = appConfig["gmdSecretKey"];


//GetHostedZoneData(accessKey, secretKey, "Z3RBMCKQL808BN");


//ListResourceRecordSets(accessKey, secretKey, "Z34VXO1XXCAVM9", "mcle-carl.com");

//GetResourceRecordSet(accessKey, secretKey, "Z3IJVPDYHW4PIQ", "integral-design.net");

//GetResourceRecordSet(accessKey, secretKey, " Z3RBMCKQL808BN ", "mcle-grant.net");


ChangeResourceRecordSet(accessKey, secretKey, "Z34VXCAVO1XXM9", "ChangeData3.xml");

//CreateHostedZone(accessKey, secretKey, "AddHostedZoneMcleGrantNet.xml");

//System.Console.Write(GetChangeStatus(accessKey, secretKey, "C3P9ATRH5BN74U"));

//System.Console.Write(GetChangeStatus(accessKey, secretKey, "C15YPPE6CYFTRV"));

Console.Read();

}


public static void ChangeResourceRecordSet(string accessKeyId, string secretKey, string zoneId, string postFile)

{

string xmlText = System.IO.File.ReadAllText(postFile);

UTF8Encoding encoder = new UTF8Encoding();

byte[] data = encoder.GetBytes(xmlText);


string url = "https://route53.amazonaws.com/2010-10-01/hostedzone/" + zoneId + "/rrset";


Uri uri = new Uri(url);

HttpWebRequest request = WebRequest.Create(uri) as HttpWebRequest;

request.ServicePoint.Expect100Continue = false;


request.Method = "POST";

request.ContentType = "text/xml";

request.ContentLength = data.Length;


WebHeaderCollection headers = request.Headers;

string httpDate = GetRoute53Date();

headers.Add("x-amz-date", httpDate);

string authenticationSig =

GetAWSR53_SHA1AuthorizationValue(accessKeyId, secretKey, httpDate);

headers.Add("X-Amzn-Authorization", authenticationSig);


Stream streamPostData = request.GetRequestStream();

streamPostData.Write(data, 0, data.Length);

streamPostData.Close();


try

{

WebResponse response = request.GetResponse();

Stream stream = response.GetResponseStream() as Stream;

StreamReader streamReader = new StreamReader(stream);

string responseString = streamReader.ReadToEnd();

System.Console.Write(responseString);


stream.Close();

response.Close();

}

catch (WebException ex)

{

Stream stream = ex.Response.GetResponseStream();

byte[] errorBuffer = new byte[stream.Length];

stream.Read(errorBuffer, 0, (Int32)stream.Length);


UTF8Encoding encoding = new UTF8Encoding();

Console.Write(encoding.GetString(errorBuffer));

}

}


public static string GetChangeStatus(string accessKeyId, string secretKey, string changeId)

{

string url = "https://route53.amazonaws.com/2010-10-01/change/" + changeId;

HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;

request.Method = "GET";

WebHeaderCollection headers = (request as HttpWebRequest).Headers;


string httpDate = GetRoute53Date();

headers.Add("x-amz-date", httpDate);


string authenticationSig = GetAWSR53_SHA1AuthorizationValue(accessKeyId, secretKey, httpDate);

headers.Add("X-Amzn-Authorization", authenticationSig);


HttpWebResponse response = request.GetResponse() as HttpWebResponse;


// read the response stream and put it into a byte array

Stream stream = response.GetResponseStream() as Stream;

byte[] buffer = new byte[32 * 1024];

int nRead = 0;


MemoryStream ms = new MemoryStream();

do

{

nRead = stream.Read(buffer, 0, buffer.Length);

ms.Write(buffer, 0, nRead);

} while (nRead > 0);


// Convert bytes to string.

ASCIIEncoding encoding = new ASCIIEncoding();

string responseString = encoding.GetString(ms.ToArray());


return responseString;

}


public static void CreateHostedZone(string accessKeyId, string secretKey, string postFile)

{

StringBuilder strBuilder = new StringBuilder();

foreach (string line in File.ReadAllLines(postFile, Encoding.Default))

strBuilder.Append(line);


string hostedZoneXml = strBuilder.ToString();


XmlDocument xmlPostData = new XmlDocument();

xmlPostData.LoadXml(hostedZoneXml);


// Get post data as byte[].

byte[] buffer = Encoding.ASCII.GetBytes(xmlPostData.ToString());


string url = "https://route53.amazonaws.com/2010-10-01/hostedzone";

HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;

request.Method = "POST";

request.ContentType = "text/xml";

request.ContentLength = buffer.Length;


Stream postData = request.GetRequestStream();

// Write the stream.

postData.Write(buffer, 0, buffer.Length);


WebHeaderCollection headers = (request as HttpWebRequest).Headers;


string httpDate = GetRoute53Date();

headers.Add("x-amz-date", httpDate);


string authenticationSig = GetAWSR53AuthorizationValue(accessKeyId, secretKey, httpDate);

headers.Add("X-Amzn-Authorization", authenticationSig);


HttpWebResponse response = request.GetResponse() as HttpWebResponse;


// Read the response stream and put it into a byte array

Stream stream = response.GetResponseStream() as Stream;

byte[] bufferResponse = new byte[32 * 1024];

int nRead = 0;


MemoryStream ms = new MemoryStream();

do

{

nRead = stream.Read(bufferResponse, 0, bufferResponse.Length);

ms.Write(bufferResponse, 0, nRead);

} while (nRead > 0);


// Convert bytes to string.

ASCIIEncoding encoding = new ASCIIEncoding();

string responseString = encoding.GetString(ms.ToArray());

System.Console.Write(responseString);


postData.Close();

stream.Close();


}



public static string GetAWSR53AuthorizationValue(string AWSAccessKeyId, string AWSSecretAccessKey, string AmzDate)

{

System.Security.Cryptography.HMACSHA256 MySigner =

new System.Security.Cryptography.HMACSHA256(System.Text.Encoding.UTF8.GetBytes(AWSSecretAccessKey));


string SignatureValue =

Convert.ToBase64String(MySigner.ComputeHash(System.Text.Encoding.UTF8.GetBytes(AmzDate)));


string AuthorizationValue =

"AWS3-HTTPS AWSAccessKeyId=" + System.Uri.EscapeDataString(AWSAccessKeyId) + ",Algorithm=HmacSHA256,Signature=" + SignatureValue;


return AuthorizationValue;

}


public static void ListResourceRecordSets(string awsKeyId, string secretKey,

string zoneId, string domainName)

{

string url = "https://route53.amazonaws.com/2010-10-01/hostedzone/" + zoneId +

"/rrset?type=A&name=" + domainName + "&maxitems=15";

HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;

request.Method = "GET";

WebHeaderCollection headers = (request as HttpWebRequest).Headers;


string httpDate = GetRoute53Date(); // DateTime.UtcNow.ToString("ddd, dd MMM yyyy HH:mm:ss ") + "GMT";

headers.Add("x-amz-date", httpDate);


string authenticationSig = GetAWSR53_SHA1AuthorizationValue(awsKeyId, secretKey, httpDate);


headers.Add("X-Amzn-Authorization", authenticationSig);


HttpWebResponse response = request.GetResponse() as HttpWebResponse;


// read the response stream and put it into a byte array

Stream stream = response.GetResponseStream() as Stream;

byte[] buffer = new byte[32 * 1024];

int nRead = 0;


MemoryStream ms = new MemoryStream();

do

{

nRead = stream.Read(buffer, 0, buffer.Length);

ms.Write(buffer, 0, nRead);

} while (nRead > 0);


// Convert bytes to string.

ASCIIEncoding encoding = new ASCIIEncoding();

string responseString = encoding.GetString(ms.ToArray());

System.Console.Write(responseString);

}


public static void GetHostedZoneData(string awsId, string secretId, string zoneId)

{

string url = "https://route53.amazonaws.com/2010-10-01/hostedzone/" + zoneId ;

HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;

request.Method = "GET";

WebHeaderCollection headers = (request as HttpWebRequest).Headers;


string httpDate = GetRoute53Date(); // DateTime.UtcNow.ToString("ddd, dd MMM yyyy HH:mm:ss ") + "GMT";

headers.Add("x-amz-date", httpDate);


string authenticationSig = GetAWSR53_SHA1AuthorizationValue(awsId, secretId, httpDate);


headers.Add("X-Amzn-Authorization", authenticationSig);


HttpWebResponse response = request.GetResponse() as HttpWebResponse;


// read the response stream

Stream stream = response.GetResponseStream() as Stream;

string responseString = new StreamReader(stream).ReadToEnd();

System.Console.Write(responseString);

}


public static void ListHostedZones(string awsId, string secretId)

{

string url = "https://route53.amazonaws.com/2010-10-01/hostedzone";

HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;

request.Method = "GET";

WebHeaderCollection headers = (request as HttpWebRequest).Headers;


// the canonical string is the date string

string httpDate = GetRoute53Date(); // DateTime.UtcNow.ToString("ddd, dd MMM yyyy HH:mm:ss ") + "GMT";

headers.Add("x-amz-date", httpDate);


// Both the following methods work!

string authenticationSig = GetAWSR53_SHA1AuthorizationValue(awsId, secretId, httpDate);

//string authenticationSig = GetAWSR53AuthorizationValue(awsId, secretId, httpDate);

headers.Add("X-Amzn-Authorization", authenticationSig);


HttpWebResponse response = request.GetResponse() as HttpWebResponse;


// read the response stream and put it into a byte array

Stream stream = response.GetResponseStream() as Stream;

byte[] buffer = new byte[32 * 1024];

int nRead = 0;


MemoryStream ms = new MemoryStream();

do

{

nRead = stream.Read(buffer, 0, buffer.Length);

ms.Write(buffer, 0, nRead);

} while (nRead > 0);


// Convert bytes to string.

ASCIIEncoding encoding = new ASCIIEncoding();

string responseString = encoding.GetString(ms.ToArray());

System.Console.Write(responseString);

}


public static string GetAWSR53_SHA1AuthorizationValue(string AWSAccessKeyId, string AWSSecretAccessKey, string AmzDate)

{

System.Security.Cryptography.HMACSHA1 MySigner =

new System.Security.Cryptography.HMACSHA1(System.Text.Encoding.UTF8.GetBytes(AWSSecretAccessKey));


string SignatureValue =

Convert.ToBase64String(MySigner.ComputeHash(System.Text.Encoding.UTF8.GetBytes(AmzDate)));


string AuthorizationValue =

"AWS3-HTTPS AWSAccessKeyId=" + System.Uri.EscapeDataString(AWSAccessKeyId) + ",Algorithm=HmacSHA1,Signature=" + SignatureValue;


return AuthorizationValue;

}


public static string GetRoute53Date()

{

string url = "https://route53.amazonaws.com/date";

HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;

request.Method = "GET";

HttpWebResponse response;

response = request.GetResponse() as HttpWebResponse;

return response.Headers["Date"];

}

}

}