ASP Core, XML deserialization, ignore DTD DOCTYPE tag (WorldPay Notification)

488 Views Asked by At

I am trying to write a Web API controller in .NET Core to receive Worldpay notifications.

I have done an AddMvc with the XmlSerializerInputFormatter and it is happily reading a "tweaked" version of their payload.

        services.AddMvc(config =>
        {
            config.InputFormatters.Add(new XmlSerializerInputFormatter(config));
        });

My test controller

    [HttpPost]
    [Route("SendDocument")]
    [Consumes("application/xml")]
    public ActionResult SendDocument([FromBody] paymentService payment)
    {
        return Ok();
    }

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE paymentService PUBLIC "-//Worldpay//DTD Worldpay PaymentService v1//EN"
  "http://dtd.worldpay.com/paymentService_v1.dtd">
<paymentService version="1.4" merchantCode="Your_merchant_code">
<notify>
<orderStatusEvent orderCode="ExampleOrder1"> 
      <payment>
       <paymentMethod>ECMC-SSL</paymentMethod>
          <amount value="2400" currencyCode="EUR" exponent="2" debitCreditIndicator="credit"/>
        <lastEvent>AUTHORISED</lastEvent>       
        <AuthorisationId id="622206"/>
        <balance accountType="IN_PROCESS_AUTHORISED">
          <amount value="2400" currencyCode="EUR" exponent="2" debitCreditIndicator="credit"/>
        </balance>
        <cardNumber>5255********2490</cardNumber>
        <riskScore value="0"/>
      </payment>
</orderStatusEvent>
</notify>
</paymentService>

However, they include a DOCTYPE tag at the start and if I add this back into the payload it fails. Without DOCTYPE, it works perfectly

I have done lots of searching and seen info about Ignoring DTD etc but I have no idea how I can do this with the InputFormatter.

How can I go about getting the InputFormatter to ignore this DTD DOCTYPE tag in the file?

1

There are 1 best solutions below

0
On

After a bit of messing about I got something working which may be of use to others

So my ConfigureServices contains

        services.AddMvc(config =>
        {
            config.InputFormatters.Insert(0, new RawXmlRequestBodyFormatter());
        });

I created a Raw Formatter which passes through the XML as a string having removed any lines starting <!DOCTYPE

public class RawXmlRequestBodyFormatter : InputFormatter
{
    public RawXmlRequestBodyFormatter()
    {
        SupportedMediaTypes.Add(new Microsoft.Net.Http.Headers.MediaTypeHeaderValue("application/xml"));
        SupportedMediaTypes.Add(new Microsoft.Net.Http.Headers.MediaTypeHeaderValue("text/xml"));
    }


    /// <summary>
    /// Allow text/xml, application/xml only
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    public override Boolean CanRead(InputFormatterContext context)
    {
        if (context == null) throw new ArgumentNullException(nameof(context));

        var contentType = context.HttpContext.Request.ContentType;

        return (contentType == "text/xml" || contentType == "application/xml");
    }

    /// <summary>
    /// Handle xml results
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context) 
    {
        var contentType = context.HttpContext.Request.ContentType;
        var httpContext = context.HttpContext;

        if (contentType == "application/xml" || contentType == "text/xml")
        {
            using var reader = new StreamReader(httpContext.Request.Body, Encoding.UTF8);
            StringBuilder xml = new StringBuilder();

            try
            {
                while(true)
                {
                    var Line = await ReadLineAsync(reader, context);

                    if (Line != null)
                        xml.AppendLine(Line);
                    else
                        break;
                }
            }
            catch
            {
                return await InputFormatterResult.FailureAsync();
            }

            return await InputFormatterResult.SuccessAsync(xml.ToString());
        }

        return await InputFormatterResult.FailureAsync();
    }

    /// <summary>
    /// Read next line, Skip any lines starting with DOCTYPE
    /// </summary>
    /// <param name="reader"></param>
    /// <param name="context"></param>
    /// <returns></returns>
    private async Task<string> ReadLineAsync(StreamReader reader, InputFormatterContext context)
    {
        string line = null;

        do
        {
            line = await reader.ReadLineAsync();

        } while (line != null && line.StartsWith("<!DOCTYPE"));

        return line;
    }
}

In my controller I use it like this. I takes the RAW XML string, deserializes it into the paymentService Classes I wrote and then returns it in JSON so you can see it worked in POSTMAN

        [HttpPost]
    [Route("SendDocument")]
    [Consumes("application/xml")]
    public ActionResult SendDocument([FromBody] string raw)
    {
        try
        {
            XmlSerializer serializer = new XmlSerializer(typeof(paymentService), new XmlRootAttribute("paymentService"));
            StringReader stringReader = new StringReader(raw);

            var o = (paymentService)serializer.Deserialize(stringReader);

            return Ok(o);
        }
        catch (Exception ex)
        {
        }

        return Ok();
    }

My paymentService classes are these, provided for completeness

public class amount
{
    public amount()
    {
    }

    [XmlAttribute]
    public string value { get; set; }
    [XmlAttribute]
    public string currencyCode { get; set; }
    [XmlAttribute]
    public string exponent { get; set; }
    [XmlAttribute]
    public string debitCreditIndicator { get; set; }
}

public class balance
{
    public balance()
    {
    }

    [XmlAttribute]
    public string accountType { get; set; }

    [XmlElement]
    public amount amount { get; set; }
}

public class riskScore
{
    public riskScore()
    {
    }

    [XmlAttribute]
    public string value { get; set; }
}

public class authorisationId
{
    public authorisationId()
    {
    }

    [XmlAttribute]
    public string id { get; set; }
}

public class payment
{
    public payment()
    {
    }


    [XmlElement]
    public string paymentMethod { get; set; }

    [XmlElement]
    public amount amount { get; set; }

    [XmlElement]
    public string lastEvent { get; set; }

    [XmlElement]
    public authorisationId AuthorisationId { get; set; }

    [XmlElement]
    public balance balance { get; set; }

    [XmlElement]
    public string cardNumber { get; set; }

    [XmlElement]
    public riskScore riskScore { get; set; }
}

public class orderStatusEvent
{
    public orderStatusEvent()
    {
    }

    [XmlAttribute]
    public string orderCode { get; set; }

    [XmlElement]
    public payment payment { get; set; }
}

public class notify
{
    public notify()
    {
    }

    [XmlElement]
    public orderStatusEvent orderStatusEvent { get; set; }
}

[Serializable]
public class paymentService

{
    public paymentService()
    {
    }

    [XmlAttribute]
    public string version { get; set; }
    [XmlAttribute]
    public string merchantCode { get; set; }

    [XmlElement]
    public notify notify { get; set; }

}

I hope this saves somebody some time. WorldPay are useless at providing info and to get to this point has taken hours over weeks.