如何使用"quoted-printable" content-transfer-encoding 与BizTalk AS2 接收?

How to use "quoted-printable" content-transfer-encoding with BizTalk AS2 receiving?

我目前正在使用 BizTalk Server 2013 R2 与许多不同的贸易伙伴交换 EDI 以及使用 AS2 的 non-EDI 文档。我最近添加了一个新的贸易伙伴,在成功收到一些文件后,我开始看到这个错误时不时发生:

An output message of the component "Microsoft.BizTalk.EdiInt.PipelineComponents" in receive pipeline "Microsoft.BizTalk.EdiInt.DefaultPipelines.AS2Receive, Microsoft.BizTalk.Edi.EdiIntPipelines, Version=3.0.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" is suspended due to the following error: The content transfer encoding quoted-printable is not supported.. The sequence number of the suspended message is 2.

经过一些调查,我发现有问题的贸易伙伴的AS2平台有时会将MIME body部分的Content-Transfer-Encoding设置为quoted-printable 当包含的 XML 负载包含 non-ASCII 个字符时。发生这种情况时,消息将被挂起 (non-resumable),并出现上述错误。

从该贸易伙伴收到的消息经过加密和签名,但未压缩 - 并使用配置有 out-of-the-box [=32= 的 HTTP request-response (two-way) 端口接收]AS2Receive 管道。我试过将自定义管道与 AS 解码器、S/MIME 解码器和 AS2 反汇编器组件一起使用,但这似乎没有任何效果 - 错误保持不变。

我也试过从贸易伙伴那里接收未加密的消息(通过双方同意),但似乎在这里做错了,传递到消息框的消息最终没有被正确分解(MIME部分边界和 AS2 签名在实际消息负载中仍然可见)。由于贸易伙伴无论如何都不允许在生产环境中发送未加密的消息,因此我需要使用加密来实现这一点。他们也无法更改其平台的行为,因为据报道这会影响所有其他贸易伙伴。

这是在暂停时收到的加密和签名的 AS2 消息的展开 HTTP headers(省略号表示编辑值):

Date: Mon, 20 Jan 2020 17:30:53 GMT
Content-Length: 8014
Content-Type: application/pkcs7-mime; name="smime.p7m"; smime-type=enveloped-data
From: ...
Host: ...
User-Agent: Jakarta Commons-HttpClient/3.1
AS2-To: ...
Subject: AS2 Message from ... to ...
Message-Id: <1C20200120-173053-740219@xxx.xxx.130.163>
Disposition-Notification-To: <mailto:...> ...
Disposition-Notification-Options: signed-receipt-protocol=optional, pkcs7-signature; signed-receipt-micalg=optional, sha1
AS2-From: ...
AS2-Version: 1.1
content-disposition: attachment; filename="smime.p7m"
X-Original-URL: /as2


这是未加密的(省略号表示编辑的内容)有效负载,当从源方发送完全相同的消息而不加密时:

------=_Part_16155_1587439544.1579506174880
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: quoted-printable

...
------=_Part_16155_1587439544.1579506174880
Content-Type: application/pkcs7-signature; name=smime.p7s; smime-type=signed-data
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="smime.p7s"
Content-Description: S/MIME Cryptographic Signature

...
------=_Part_16155_1587439544.1579506174880--


问题:BizTalk Server是否支持quoted-printable编码方式?如果是这样,我做错了什么?如果没有,我有哪些解决方法?

我已就此问题向 Microsoft BizTalk 技术支持开票。他们的回应是

The quoted-printable encoding is not supported by MS BizTalk Server 2013R2" and most likely is not supported by MS BizTalk Server 2020

对于可能遇到同样问题的任何其他人,我想我会分享我最终得到的解决方案。

由于在 AS2 接收管道处理过程中遇到错误,自然我的解决方案侧重于创建一个自定义接收管道组件,其功能与 out-of-the-box AS2 解码器组件大致相同,但支持quoted-printable编码方式:


1.解码解密CMS/PKCS#7数据信封

这实际上是最简单的步骤,仅需 5 行代码:

EnvelopedCms envelopedCms = new EnvelopedCms();
envelopedCms.Decode(encryptedData);
envelopedCms.Decrypt();
byte[] decryptedData = envelopedCms.Encode();
string decryptedMessageString = Encoding.ASCII.GetString(decryptedData);

-encryptedData is a byte-array instantiated from the body-part data stream of the AS2 message received bythe HTTP adapter.

-The Decrypt method automatically searches the user and computer certificate stores for the appropriate certificate private key and uses this to decrypt the AS2 payload. For more information on the `EnvelopedCms' class follow this link.


2。将有效负载中的任何 quoted-printable 内容转换为普通 UTF-8 文本

首先我们必须从解密有效负载开头的内容类型字符串中获取 MIME 边界名称:

int firstBlankLineInMessage = decryptedMessageString.IndexOf(Environment.NewLine + Environment.NewLine);
string contentType = decryptedMessageString.Substring(0, firstBlankLineInMessage);

Regex boundaryRegex = new Regex("boundary=\"(?<boundary>.*)\"");
Match boundaryMatch = boundaryRegex.Match(contentType);
if (!boundaryMatch.Success)
   throw new Exception("Failed to get boundary name from content type");

string boundary = "--" + boundaryMatch.Groups["boundary"].Value;

然后我们将信封和 re-merge 分开,去掉 content-type header 部分:

string[] messageParts = decryptedMessageString.Split(new string[] {boundary}, StringSplitOptions.RemoveEmptyEntries);
string signedMessageString = boundary + messageParts[1] + boundary + messageParts[2] + boundary + "--\r\n";

接下来我们在 MIME body-part header:

中获取 `Content-Transfer-Encoding' 值
int firstBlankLineInBodyPart = messageParts[1].IndexOf(Environment.NewLine + Environment.NewLine);
string partHeaders = messageParts[1].Substring(0, firstBlankLineInBodyPart);

Regex cteRegex = new Regex("Content-Transfer-Encoding: (?<cte>.*)");
Match cteMatch = cteRegex.Match(partHeaders);
if (!cteMatch.Success)
   throw new Exception("Failed to get CTE from body part headers");

string cte = cteMatch.Groups["cte"].Value;
string payload = messageParts[1].Substring(firstBlankLineInBodyPart).Trim(); 

最后我们检查 CTE 并在必要时解码:

string payload = messageParts[1].Substring(firstBlankLineInBodyPart).Trim();
if (cte == "quoted-printable")
{
   // Get charset
   Regex charsetRegex = new Regex("Content-Type: .*charset=(?<charset>.*)");
   Match charsetMatch = charsetRegex.Match(partHeaders);
   if (!charsetMatch.Success)
      throw new Exception("Failed to get charset from body part headers");

   string charset = charsetMatch.Groups["charset"].Value;

   QuotedPrintableDecode(payload, charset);
}

Note: There are many different implementations out there for decoding QP, including a .NET implementation that has (reportedly) been found buggy by some users. I decided to use this implementation shared by Gonzalo.


3。更新 Content-Type HTTP header 和 BizTalk 消息 body-part 流

string httpHeaders = objHttpHeaders.ToString().Replace("Content-Type: application/pkcs7-mime; name=\"smime.p7m\"; smime-type=enveloped-data", "Content-Type: application/xml");
inMessage.Context.Write("InboundHttpHeaders", "http://schemas.microsoft.com/BizTalk/2003/http-properties", httpHeaders);

MemoryStream payloadStream = new MemoryStream(Encoding.UTF8.GetBytes(payload));
payloadStream.Seek(0, SeekOrigin.Begin);
pipelineContext.ResourceTracker.AddResource(payloadStream);
inMessage.BodyPart.Data = payloadStream;

-pipelineContext is the IPipelineContext variable passed to the Execute method of the custom pipeline component
-inMessage is the IBaseMessage variable passed to the Execute method


最后的想法

上面的代码仍然可以通过多种方式进行改进:

  • 在尝试解密之前检查 HTTP header 是否加密
  • Re-encrypting 将消息传递到 AS2 反汇编程序组件之前的有效负载(如果 BizTalk 方配置需要)
  • 添加对压缩的支持

如果您想要源代码的副本,请给我留言,我会考虑将其升级到在线存储库。