微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

使用AWS Lambda函数从Twilio多部分/表单数据响应中解析传真数据时,PDF损坏了

如何解决使用AWS Lambda函数从Twilio多部分/表单数据响应中解析传真数据时,PDF损坏了

我正在尝试使用Twilio可编程传真API通过AWS Lambda(Node.js环境)和API网关安全地接收PDF格式的传真。

有两个Lambda函数涉及接收传真文档。第一个是在Twilio管理员中配置的Webhook,第二个是在我从第一个Webhook对Twilio的TwiML响应中指定的。

这是我的webhook的实现:

module.exports.faxSentSecure = async (event,context) => {
  const twiml = `
  <Response>
    <Receive action="https://bd3j05rkk8.execute-api.ca-central-1.amazonaws.com/dev/v1/webhooks/twilio/fax/received/secure" storeMedia="false"/>
  </Response>
  `;

  const response = {
    statusCode: 200,headers: {
      "Content-Type": "text/xml",},body: twiml,};

  console.log(response);

  return response;
};

在使用上述TwiML响应后不久,Twilio将调用我在action参数中指定的端点。

此时需要注意的几件事:

  • 出于合规性原因,我在上面的TwiML response中指定 storeMedia="true"导致Twilio avoid media storage,并用传真数据multipart/form-data调用我指定的操作端点,其中实际的PDF数据嵌入在称为Media的参数中。
  • 显然,为了使用AWS Lambda函数解析multipart/form-data,根据我的研究,我需要将其指定为binary type in my API gateway settings。通过这种配置,在我对上述TwiML做出响应之后,我的Lambda函数调用,并且我收到的body对象的event参数是一个Base 64编码的字符串(与UTF-8相反,如果未指定二进制类型。)

我遇到的问题是,当我尝试解析要存储在某处(在我的情况下是AWS S3)的PDF数据时,我最终得到了一个损坏/无法读取的PDF文档。

这是我第二个Webhook的实现,它接收了传真文档:

"use strict";

const querystring = require("querystring");
const fetch = require("node-fetch");
const stream = require("stream");
const Busboy = require("busboy");
const AWS = require("aws-sdk");

const s3 = new AWS.S3();

module.exports.faxReceivedSecure = async (event,context) => {
  console.log(JSON.stringify(event));
  console.log(JSON.stringify(event.headers));
  console.log(JSON.stringify(event.body));

  // decode the multipart form
  const base64Body = event.body; // base64 body from API gateway
  const decodedBody = Buffer.from(base64Body,"base64").toString("utf8");

  let form;
  try {
    form = await parseForm(event.headers,decodedBody);
    console.log("PARSED FORM",form);
  } catch (parseError) {
    console.log("PARSE ERROR: ",parseError);
  }

  // write the PDF to disk
  if (form) {
    const filename = form.Filename;

    // this results in corrupted PDF data
    const media = form.Media;
    const s3UploadResult = await s3
      .putObject({
        Bucket: process.env.BUCKET,Key: filename,Body: media,})
      .promise();

    console.log("S3 UPLOAD RESULT: ",s3UploadResult);

    const response = {
      statusCode: 200,body: JSON.stringify({
        message: "Fax Received!",s3UploadResult: s3UploadResult,}),};
    return response;
  } else {
    const response = {
      statusCode: 404,body: JSON.stringify({
        message: "Unable to parse fax data!",};
    return response;
  }
};

const parseForm = (headers,body) => {
  return new Promise((resolve,reject) => {
    let form = {};

    const contentType = headers["Content-Type"] || headers["content-type"];
    const busboy = new Busboy({ headers: { "content-type": contentType } });

    busboy.on("field",(fieldname,val) => {
      form[fieldname] = val;
    });

    busboy.on("file",file,filename,encoding,mimetype) => {
      console.log(
        "File [%s]: filename=%j; encoding=%j; mimetype=%j",fieldname,mimetype
      );

      form.Fieldname = fieldname;
      form.Filename = filename;
      form.Encoding = encoding;
      form.Mimetype = mimetype;

      file.on("data",(data) => {
        console.log("File [%s] got %d bytes",data.length);
        form[fieldname] = data;
      });

      file.on("end",() => console.log("File [%s] Finished",fieldname));
    });

    busboy.on("finish",() => {
      resolve(form);
    });

    busboy.on("error",(err) => {
      reject(err);
    });

    busboy.end(body);
  });
};

通过parseForm(headers,body)函数解析的对象看起来像这样:

{
  NumPages: '2',BitRate: '14400',Resolution: 'fine',FaxSid: 'FX467437b3f418ee4dfac5e0838906f226',Fieldname: 'Media',Filename: 'fax_FX467437b3f418ee4dfac5e0838906f226_AC8a237b34db1faa8c9dd6a8ee96ca38ae.pdf',Encoding: '7bit',Mimetype: 'application/pdf',To: '+18332003898',AccountSid: 'AC8a237b34db1faa8c9dd6a8ee96ca38ae',FaxStatus: 'received',RemoteStationId: 'FaxZero.com',From: '+12014799379',ApiVersion: 'v1',Media: <Buffer 25 50 44 46 2d 31 2e 31 20 0a 25 c3 a2 c3 a3 c3 8f c3 93 0a 31 20 30 20 6f 62 6a 0a 3c 3c 20 0a 2f 54 79 70 65 20 2f 43 61 74 61 6c 6f 67 20 0a 2f 50 ... 33847 more bytes>,Status: 'received'
}

我正在使用busboy来解析多部分表单,从上面的输出中可以看到,我能够从表单中提取所有数据。您甚至可以看到Media参数被提取为缓冲区对象。

但是,当我现在尝试通过将此缓冲区对象写入磁盘(使用本地快速测试服务器)或将其上传到S3来存储该缓冲区对象时,我得到的只是外观看起来已损坏的PDF。

我能想到的唯一问题是我没有正确解码主体数据并导致PDF数据损坏。

为此,我已经挠头了好几天,这真让我发疯。帮助将不胜感激。

解决方法

好的,我现在有了一个可行的解决方案。

正如我怀疑这里的问题是我们正在将二进制数据转换为 UTF-8,它本身包含 base64 编码的 PDF 数据。然后我们尝试将 PDF 解释为 base64 而不是 UTF-8,但是如果我们尝试将数据解释为 UTF-8,我们也会丢失/损坏一些信息。理想情况下,我们想要的只是直接处理 base64 数据,而无需在 base6/UTF-8 之间进行转换,因为这是发生数据损坏的地方。

在与 Twilio 支持合作后,我们发现 busboy 实际上能够直接解析 base64 数据,无需先将其转换为 UTF-8。

长话短说,这是工作代码:

"use strict";

const Busboy = require("busboy");
const AWS = require("aws-sdk");

const S3 = new AWS.S3();

module.exports.faxReceivedSecure = async (event,context) => {
  // console.log(JSON.stringify(event));
  // console.log(JSON.stringify(event.headers));
  // console.log(JSON.stringify(event.body));

  // step 1
  // parse the multipart/form-data
  let form;
  try {
    // multipart/form-data is configured as a binary type in
    // API Gateway,therefore the body data is received as a
    // base64 encoded string
    const base64BodyString = event.body;

    // convert to base64 buffer
    const base64Body = Buffer.from(base64BodyString,"base64");
    form = await parseForm(event.headers,base64Body);
    console.log("PARSED FORM",form);
  } catch (parseError) {
    console.log("PARSE ERROR: ",parseError);
  }

  // step 2
  if (form) {
    // upload to S3
    const filename = form.Filename;
    const media = form.Media;
    const params = {
      Body: media,Bucket: process.env.BUCKET,Key: filename,Tagging: "key1=value1&source=twilio&type=fax",};
    const s3UploadResult = await S3.putObject(params).promise();

    console.log("S3 UPLOAD RESULT: ",s3UploadResult);

    const response = {
      statusCode: 200,body: JSON.stringify({
        message: "Fax Received!",s3UploadResult: s3UploadResult,}),};
    return response;
  } else {
    const response = {
      statusCode: 404,body: JSON.stringify({
        message: "Unable to parse fax data!",};
    return response;
  }
};

const parseForm = (headers,body) => {
  return new Promise((resolve,reject) => {
    let form = {};

    const contentType = headers["Content-Type"] || headers["content-type"];
    const busboy = new Busboy({ headers: { "content-type": contentType } });

    busboy.on("field",(fieldname,val) => {
      form[fieldname] = val;
    });

    busboy.on("file",file,filename,encoding,mimetype) => {
      console.log(
        "File [%s]: filename=%j; encoding=%j; mimetype=%j",fieldname,mimetype
      );

      form.Fieldname = fieldname;
      form.Filename = filename;
      form.Encoding = encoding;
      form.Mimetype = mimetype;

      file.on("data",(data) => {
        console.log("File [%s] got %d bytes",data.length);
        form[fieldname] = data;
      });

      file.on("end",() => console.log("File [%s] Finished",fieldname));
    });

    busboy.on("finish",() => {
      resolve(form);
    });

    busboy.on("error",(err) => {
      reject(err);
    });

    busboy.end(body);
  });
};

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。