By request, this is a follow up to my “Send an AS2 message with .NET” from July 2010. This time we will be receiving AS2 (Applicability Statement 2) messages.
Start with an “ASP.NET Web Application” and then add a “Generic Handler” to it, call it AS2Listener.ashx.
Add the following code to the ProcessRequest method:
public void ProcessRequest(HttpContext context) { string sTo = context.Request.Headers["AS2-To"]; string sFrom = context.Request.Headers["AS2-From"]; string sMessageID = context.Request.Headers["Message-ID"]; if (context.Request.HttpMethod == "POST" || context.Request.HttpMethod == "PUT" || (context.Request.HttpMethod == "GET" && context.Request.QueryString.Count > 0)) { if (sFrom == null || sTo == null) { //Invalid AS2 Request. //Section 6.2 The AS2-To and AS2-From header fields MUST be present // in all AS2 messages if (!(context.Request.HttpMethod == "GET" && context.Request.QueryString[0].Length == 0)) { AS2Receive.BadRequest(context.Response, "Invalid or unauthorized AS2 request received."); } } else { AS2Receive.Process(context.Request, WebConfigurationManager.AppSettings["DropLocation"]); } } else { AS2Receive.GetMessage(context.Response); } }
Now you’ll need to create your AS2Receive class. The simple methods are AS2Receive.BadRequest and AS2Receive.GetMessage:
public static void GetMessage(HttpResponse response) { response.StatusCode = 200; response.StatusDescription = "Okay"; response.Write(@"<!DOCTYPE HTML PUBLIC ""-//W3C//DTD HTML 3.2 Final//EN"">" + @"<HTML><HEAD><TITLE>Generic AS2 Receiver</TITLE></HEAD>" + @"<BODY><H1>200 Okay</H1><HR>This is to inform you that the AS2 interface is working and is " + @"accessable from your location. This is the standard response to all who would send a GET " + @"request to this page instead of the POST context.Request defined by the AS2 Draft Specifications.<HR></BODY></HTML>"); } public static void BadRequest(HttpResponse response, string message) { response.StatusCode = (int)HttpStatusCode.BadRequest; response.StatusDescription = "Bad context.Request"; response.Write(@"<!DOCTYPE HTML PUBLIC ""-//W3C//DTD HTML 3.2 Final//EN"">" + @"<HTML><HEAD><TITLE>400 Bad context.Request</TITLE></HEAD>" + @"<BODY><H1>400 Bad context.Request</H1><HR>There was a error processing this context.Request. The reason given by the server was:" + @"<P><font size=-1>" + message + @"</Font><HR></BODY></HTML>"); }
The one that does all the work is AS2Receive.Process. In this simple example all it will do is read the message and write it to a new text file in the given dropLocation folder.
public static void Process(HttpRequest request, string dropLocation) { string filename = ParseFilename(request.Headers["Subject"]); byte[] data = request.BinaryRead(request.TotalBytes); bool isEncrypted = request.ContentType.Contains("application/pkcs7-mime"); bool isSigned = request.ContentType.Contains("application/pkcs7-signature"); string message = string.Empty; if (isSigned) { string messageWithMIMEHeaders = System.Text.ASCIIEncoding.ASCII.GetString(data); string contentType = request.Headers["Content-Type"]; message = AS2MIMEUtilities.ExtractPayload(messageWithMIMEHeaders, contentType); } else if (isEncrypted) // encrypted and signed inside { byte[] decryptedData = AS2Encryption.Decrypt(data); string messageWithContentTypeLineAndMIMEHeaders = System.Text.ASCIIEncoding.ASCII.GetString(decryptedData); // when encrypted, the Content-Type line is actually stored in the start of the message int firstBlankLineInMessage = messageWithContentTypeLineAndMIMEHeaders.IndexOf(Environment.NewLine + Environment.NewLine); string contentType = messageWithContentTypeLineAndMIMEHeaders.Substring(0, firstBlankLineInMessage); message = AS2MIMEUtilities.ExtractPayload(messageWithContentTypeLineAndMIMEHeaders, contentType); } else // not signed and not encrypted { message = System.Text.ASCIIEncoding.ASCII.GetString(data); } System.IO.File.WriteAllText(dropLocation + filename, message); }
We have some logic at the start to figure out if the message is signed, or if it’s encrypted and signed. Note that this code always assumes that if a message is encrypted it is also signed, and doesn’t allow for the message to be encrypted but not signed – although that is a valid AS2 scenario.
Let’s deal with receiving signed messages first, in AS2MIMEUtilities:
public class AS2MIMEUtilities { public const string MESSAGE_SEPARATOR = "\r\n\r\n"; /// <summary> /// Extracts the payload from a signed message, by looking for boundaries /// Ignores signatures and does checking - should really validate the signature /// </summary> public static string ExtractPayload(string message, string contentType) { string boundary = GetBoundaryFromContentType(contentType); if (!boundary.StartsWith("--")) boundary = "--" + boundary; int firstBoundary = message.IndexOf(boundary); int blankLineAfterBoundary = message.IndexOf(MESSAGE_SEPARATOR, firstBoundary) + (MESSAGE_SEPARATOR).Length; int nextBoundary = message.IndexOf(MESSAGE_SEPARATOR + boundary, blankLineAfterBoundary); int payloadLength = nextBoundary - blankLineAfterBoundary; return message.Substring(blankLineAfterBoundary, payloadLength); } /// <summary> /// Extracts the boundary from a Content-Type string /// </summary> /// <param name="contentType">e.g: multipart/signed; protocol="application/pkcs7-signature"; micalg="sha1"; boundary="_956100ef6a82431fb98f65ee70c00cb9_"</param> /// <returns>e.g: _956100ef6a82431fb98f65ee70c00cb9_</returns> public static string GetBoundaryFromContentType(string contentType) { return Trim(contentType, "boundary=\"", "\""); } /// <summary> /// Trims the string from the end of startString until endString /// </summary> private static string Trim(string str, string start, string end) { int startIndex = str.IndexOf(start) + start.Length; int endIndex = str.IndexOf(end, startIndex); int length = endIndex - startIndex; return str.Substring(startIndex, length); }
Oh, OK, erm the <summary> of ExtractPayload says it all. All I do is truncate the message signature off the message, by looking for the message part boundaries. It should of course really check that the message signature is valid.
Now let’s handle encrypted messages with AS2Encryption.Decrypt:
internal static byte[] Decrypt(byte[] encodedEncryptedMessage) { EnvelopedCms envelopedCms = new EnvelopedCms(); envelopedCms.Decode(encodedEncryptedMessage); envelopedCms.Decrypt(); return envelopedCms.Encode(); }
And that, Dear reader is a simple example of how to receive either: unsigned & unencrypted; signed; or encrypted & signed AS2 messages. What is missing is the necessary checking that the signature is valid, and handling of encrypted but unsigned messages.