1

I'm trying to integrate the Royal Mail SOAP API into my .NET Code. I have followed the advice here Consume WCF Royal Mail API in c# Console Application and here C# WCF Namespaces Move To Header & Use NS Prefix.

I have created a custom IClientMessageFormatter to be able to attach the namespaces to the beginning of the soap envelope, but I still can't seem to get this to work. I keep receiving the following error. Could not establish trust relationship for the SSL/TLS secure channel with authority 'api.royalmail.com', and the inner exception is: The remote certificate is invalid according to the validation procedure.

I am using Visual Studio 13 and .Net version 3.5, I've tried numerous other versions but with no further progress. When I debug I can see that the normal message been passed into the RoyalMailMessage but when it runs OnWriteStartEnvelope I can't see any changes to the _message object. I've created a trace to see what soap request is been sent.

I have sent my XML request to Royal Mail support who validate that the reason it is failing is due to the namespaces not been declared in the envelope and the missing prefixes.

RoyalMail.cs

internal class RoyalMail
{
    private readonly X509Certificate2 _certificate;
    private readonly Config _config;

    public RoyalMail()
    {
        _config = new Config();
        _config.LoadConfig();

        // Load The SSL Certificate (Check The File Exists)
        var certificatePath = (Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location) + @"\" + _config.GetCertificateName());

        if (!File.Exists(certificatePath))
        {
            throw new Exception(@"The Royal Mail Certificate Is Missing From The Plugins Directory. Please Place The File " + _config.GetCertificateName() + " In The Same Directory As The Plugin DLL File & Relaunch FileMaker.\n\n" + certificatePath);
        }

        _certificate = new X509Certificate2(certificatePath, _config.GetCertificatePassword());

        // Check It's In The Certificate 
        var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
        store.Open(OpenFlags.ReadWrite);
        if (!store.Certificates.Contains(_certificate))
        {
            store.Add(_certificate);
            MessageBox.Show("Certificate Was Installed Into Computer Trust Store");
        }
        store.Close(); 
    }


    /*
     * 
     * SOAP Service & Methods
     * 
     */

    private shippingAPIPortTypeClient GetProxy()
    {

        var myBinding = new BasicHttpBinding(BasicHttpSecurityMode.Transport)
        {
            MaxReceivedMessageSize = 2147483647
        };
        myBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Certificate;
        var uri = new Uri(_config.GetEndpointUrl());
        var endpointIdentity = EndpointIdentity.CreateDnsIdentity("api.royalmail.com");

        var shippingClient = new shippingAPIPortTypeClient(myBinding, new EndpointAddress(uri, endpointIdentity, new AddressHeaderCollection()));
        if (shippingClient.ClientCredentials != null)
            shippingClient.ClientCredentials.ClientCertificate.Certificate = _certificate;

        foreach (var od in shippingClient.Endpoint.Contract.Operations)
        {
           od.Behaviors.Add(new RoyalMailIEndpointBehavior());
        }

        return shippingClient;
    }

    private SecurityHeaderType GetSecurityHeaderType()
    {
        var securityHeader = new SecurityHeaderType();

        var creationDate = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ");

        var nonce = (new Random().Next(0, int.MaxValue)).ToString();

        var hashedPassword = GetSha1(_config.GetPassword());

        var concatednatedDigestInput = string.Concat(nonce, creationDate, Encoding.Default.GetString(hashedPassword));
        var digest = GetSha1(concatednatedDigestInput);

        var passwordDigest = Convert.ToBase64String(digest);

        var encodedNonce = Convert.ToBase64String(Encoding.Default.GetBytes(nonce));

        var doc = new XmlDocument();
        using (var writer = doc.CreateNavigator().AppendChild())
        {
            writer.WriteStartDocument();
            writer.WriteStartElement("wsse", "Security", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
            writer.WriteStartElement("wsse", "UsernameToken", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
            writer.WriteElementString("wsse", "Username", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", _config.GetUsername());
            writer.WriteElementString("wsse", "Password", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", passwordDigest);
            writer.WriteElementString("wsse", "Nonce", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", encodedNonce);
            writer.WriteElementString("wsse", "Created", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", creationDate);
            writer.WriteEndElement();
            writer.WriteEndElement();
            writer.WriteEndDocument();
            writer.Flush();
        }

        if (doc.DocumentElement != null)
        {
            doc.DocumentElement.RemoveAllAttributes();

            var headers = doc.DocumentElement.ChildNodes.Cast<XmlElement>().ToArray();

            securityHeader.Any = headers;
        }

        return securityHeader;
    }

    private integrationHeader GetIntegrationHeader()
    {

        var header = new integrationHeader();

        var created = DateTime.Now;
        var createdAt = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ");

        header.dateTime = created;
        header.version = int.Parse(_config.GetVersion());
        header.dateTimeSpecified = true;
        header.versionSpecified = true;

        var idStructure = new identificationStructure {applicationId = _config.GetApplicationId()};

        var nonce = new Random().Next(0, int.MaxValue).ToString();

        idStructure.transactionId = CalculateMd5Hash(nonce + createdAt);

        header.identification = idStructure;

        return header;
    }

    private static byte[] GetSha1(string input)
    {
        return SHA1Managed.Create().ComputeHash(Encoding.Default.GetBytes(input));
    }

    public string CalculateMd5Hash(string input)
    {
        // step 1, calculate MD5 hash from input
        var md5 = MD5.Create();
        var inputBytes = Encoding.ASCII.GetBytes(input);
        var hash = md5.ComputeHash(inputBytes);

        // step 2, convert byte array to hex string
        var sb = new StringBuilder();
        foreach (var t in hash)
        {
            sb.Append(t.ToString("X2"));
        }
        return sb.ToString();
    }

    /*
     * Check Response Footer For Errors & Warnings From Service
     * If Error Return True So We Can Inform File maker Of Error
     * Ignore Warnings For Now
     * 
     */
    private static void CheckErrorsAndWarnings(integrationFooter integrationFooter)
    {
        if (integrationFooter != null)
        {
            if (integrationFooter.errors != null && integrationFooter.errors.Length > 0)
            {
                var errors = integrationFooter.errors;
                foreach (var error in errors)
                {
                    MessageBox.Show("Royal Mail Request Error: " + error.errorDescription + ". " + error.errorResolution, "Royal Mail Request Error", MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1);
                }
                if (errors.Length > 0)
                {
                    return;
                }
            }

            if (integrationFooter.warnings != null && integrationFooter.warnings.Length > 0)
            {
                var warnings = integrationFooter.warnings;
                foreach (var warning in warnings)
                {
                    MessageBox.Show("Royal Mail Request Warning: " + warning.warningDescription + ". " + warning.warningResolution, "Royal Mail Request Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button1);
                }
            }
        }
    }

    /*
     * Show Message Box With SOAP Error If We Receive A Fault Code Back From Service
     *
     */
    private static void ShowSoapException(FaultException e)
    {
        var message = e.CreateMessageFault();
        var errorDetail = message.GetDetail<XmlElement>();
        var errorDetails = errorDetail.ChildNodes;
        var fullErrorDetails = "";

        for (var i = 0; i < errorDetails.Count; i++)
        {
            var xmlNode = errorDetails.Item(i);
            if (xmlNode != null)
                fullErrorDetails += xmlNode.Name + ": " + xmlNode.InnerText + "\n";
        }

        MessageBox.Show("An Error Occured With Royal Mail Service: " + message.Reason + "\n\n" + fullErrorDetails, "Royal Mail SOAP Error", MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1);
    }

    public createShipmentResponse SendCreateShipmentRequest(CreateShipmentForm shippingForm)
    {
        var client = GetProxy();

        try
        {
            var request = new createShipmentRequest {integrationHeader = GetIntegrationHeader()};
            var shipment = new requestedShipment();

            // Shipment Type Code (Delivery or Return)
            var shipmentType = new referenceDataType {code = shippingForm.ShippingType};
            shipment.shipmentType = shipmentType;

            // Service Type Code (1:24H 1st Class, 2: 48H 2nd Class, D: Special Delivery Guaranteed, H: HM Forces (BFPO), I: International, R: Tracked Returns, T: Tracked Domestic)
            var serviceType = new referenceDataType {code = shippingForm.ServiceType};
            shipment.serviceType = serviceType;

            // Service Offering (See Royal Mail Service Offering Type Codes. Too Many To List)
            var serviceOfferingTypeContainer = new serviceOfferingType();
            var serviceOffering = new referenceDataType {code = shippingForm.ServiceOffering};
            serviceOfferingTypeContainer.serviceOfferingCode = serviceOffering;
            shipment.serviceOffering = serviceOfferingTypeContainer;

            // Service Format Code
            var serviceFormatTypeContainer = new serviceFormatType();
            var serviceFormat = new referenceDataType {code = shippingForm.ServiceFormat};
            serviceFormatTypeContainer.serviceFormatCode = serviceFormat;
            shipment.serviceFormat = serviceFormatTypeContainer;

            // Shipping Date
            shipment.shippingDate = shippingForm.ShippingDate;
            shipment.shippingDateSpecified = true;

            shipment.signature = true;

            shipment.signatureSpecified = true;

            // Sender Reference Number (e.g. Invoice Number or RA Number)
            shipment.senderReference = shippingForm.InvoiceNumber;

            /*
             * Service Enhancements
            */

            var serviceEnhancements = new List<serviceEnhancementType>();
            shipment.serviceEnhancements = serviceEnhancements.ToArray();


            /*
             * Recipient Contact Details
            */

            var recipientContact = new contact();
            recipientContact.complementaryName = shippingForm.Company;
            recipientContact.name = shippingForm.Name;

            if(!shippingForm.EmailAddress.Equals("")) {
                var email = new digitalAddress {electronicAddress = shippingForm.EmailAddress};
                recipientContact.electronicAddress = email;
            }

            if(!shippingForm.MobileNumber.Equals("")) {
                var tel = new telephoneNumber();
                var phoneRegex = new Regex(@"[^\d]");
                tel.telephoneNumber1 = phoneRegex.Replace(shippingForm.MobileNumber, "");
                tel.countryCode = "00" + shippingForm.CountryDiallingCode;
                recipientContact.telephoneNumber = tel;
            }

            shipment.recipientContact = recipientContact;

            /*
             * Recipient Address
             * 
            */
            var recipientAddress = new address
            {
                addressLine1 = shippingForm.AddressLine1,
                addressLine2 = shippingForm.AddressLine2,
                addressLine3 = shippingForm.AddressLine3,
                addressLine4 = shippingForm.County,
                postTown = shippingForm.Town
            };
            var country = new countryType();
            var countryCode = new referenceDataType { code = shippingForm.CountryCode };
            country.countryCode = countryCode;
            recipientAddress.country = country;
            recipientAddress.postcode = shippingForm.PostCode;

            recipientAddress.stateOrProvince = new stateOrProvinceType {stateOrProvinceCode = new referenceDataType()};

            shipment.recipientAddress = recipientAddress;

            // Shipment Items

            var items = new List<item> ();

            foreach(var i in shippingForm.Items) {
                var item = new item
                {
                    numberOfItems = i.Products.Count.ToString(),
                    weight = new dimension
                    {
                        value = i.Weight*1000,
                        unitOfMeasure = new unitOfMeasureType {unitOfMeasureCode = new referenceDataType {code = "g"}}
                    }
                };

                items.Add(item);
            }

            if (shippingForm.ServiceType.Contains("international"))
            {
                var internationalInfo = new internationalInfo
                {
                    shipperExporterVatNo = _config.GetVatNumber(),
                    documentsOnly = false,
                    shipmentDescription = "Invoice Number: " + shippingForm.InvoiceNumber,
                    invoiceDate = DateTime.Now,
                    termsOfDelivery = "EXW",
                    invoiceDateSpecified = true,
                    purchaseOrderRef = shippingForm.InvoiceNumber
                };

                var parcels = new List<parcel>();
                foreach (var i in shippingForm.Items)
                {
                    var parcel = new parcel
                    {
                        weight = new dimension
                        {
                            value = i.Weight*1000,
                            unitOfMeasure = new unitOfMeasureType
                            {
                                unitOfMeasureCode = new referenceDataType {code = "g"}
                            }
                        },
                        invoiceNumber = shippingForm.InvoiceNumber,
                        purposeOfShipment = new referenceDataType {code = "31"}
                    };


                    var contents = new List<contentDetail>();
                    foreach (var product in i.Products)
                    {
                        var contentDetail = new contentDetail
                        {
                            articleReference = product.Sku,
                            countryOfManufacture = new countryType
                            {
                                countryCode = new referenceDataType
                                {
                                    code = product.CountryOfManufacture
                                }
                            },
                            currencyCode = new referenceDataType {code = product.CurrencyCode},
                            description = product.Name,
                            unitQuantity = product.Qty.ToString(),
                            unitValue = product.Price,
                            unitWeight = new dimension
                            {
                                value = Convert.ToSingle(product.Weight*1000),
                                unitOfMeasure = new unitOfMeasureType
                                {
                                    unitOfMeasureCode = new referenceDataType {code = "g"}
                                }
                            }
                        };

                        contents.Add(contentDetail);
                    }

                    //Parcel.contentDetails = Contents.ToArray();

                    parcels.Add(parcel);
                }

                internationalInfo.parcels = parcels.ToArray();

                shipment.internationalInfo = internationalInfo;
            }
            else
            {
                shipment.items = items.ToArray();
            }

            request.requestedShipment = shipment;

            var response = client.createShipment(GetSecurityHeaderType(), request);

            // Show Errors And Warnings
            CheckErrorsAndWarnings(response.integrationFooter);

            return response;

        }
        catch (TimeoutException e)
        {
            client.Abort();
            MessageBox.Show("Request Timed Out: " + e.Message, "Request Timeout", MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1);
        }
        catch (FaultException e)
        {
            client.Abort();
            ShowSoapException(e);
        }
        catch (CommunicationException e)
        {
            client.Abort();
            MessageBox.Show("A communication error has occurred: " + e.Message + " - " + e.StackTrace, "Communication Error", MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1);
        }
        catch (Exception e)
        {
            client.Abort();
            MessageBox.Show(e.Message, "Royal Mail Error", MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1);
        }

        return null;
    }
}

RoyalmailMessage.cs

class RoyalMailMessage : Message
{
    public Message _message;

    public RoyalMailMessage(Message message)
    {
        _message = message;
    }

    public override MessageHeaders Headers
    {
        get
        {
            return _message.Headers;
        }
    }
    public override MessageProperties Properties
    {
        get
        {
            return _message.Properties;
        }
    }

    public override MessageVersion Version
    {
        get
        {
            return _message.Version;
        }
    }

    protected override void OnWriteStartBody(XmlDictionaryWriter writer)
    {
        writer.WriteStartElement("Body", "http://schemas.xmlsoap.org/soap/envelope/");
    }

    protected override void OnWriteBodyContents(XmlDictionaryWriter writer)
    {
        _message.WriteBodyContents(writer);
    }

    protected override void OnWriteStartEnvelope(XmlDictionaryWriter writer)
    {
        writer.WriteStartElement("soapenv", "Envelope", "http://schemas.xmlsoap.org/soap/envelope/");
        writer.WriteAttributeString("xmlns", "v2", null, "http://www.royalmailgroup.com/api/ship/V2");
        writer.WriteAttributeString("xmlns", "v1", null, "http://www.royalmailgroup.com/integration/core/V1");
        writer.WriteAttributeString("xmlns", "xsi", null, "http://www.w3.org/2001/XMLSchema-instance");
        writer.WriteAttributeString("xmlns", "xsd", null, "http://www.w3.org/2001/XMLSchema");
    }
}

RoyalMailMessageFormatter.cs

public class RoyalMailMessageFormatter : IClientMessageFormatter
{
    private readonly IClientMessageFormatter _formatter;

    public RoyalMailMessageFormatter(IClientMessageFormatter formatter)
    {
        _formatter = formatter;
    }

    public object DeserializeReply(Message message, object[] parameters)
    {
        return _formatter.DeserializeReply(message, parameters);
    }

    public Message SerializeRequest(MessageVersion messageVersion, object[] parameters)
    {
        var message = _formatter.SerializeRequest(messageVersion, parameters);
        return new RoyalMailMessage(message);
    }
}

RoyalMailIEndpointBehavior.cs

internal class RoyalMailIEndpointBehavior : IOperationBehavior
{
    public void ApplyClientBehavior(OperationDescription description, ClientOperation proxy)
    {
        proxy.Formatter = new RoyalMailMessageFormatter(proxy.Formatter);
    }

    public void AddBindingParameters(OperationDescription operationDescription, BindingParameterCollection bindingParameters)
    {

    }

    public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation)
    {

    }

    public void Validate(OperationDescription operationDescription)
    {

    }

}
Community
  • 1
  • 1
Aimson
  • 61
  • 1
  • 5

2 Answers2

1

The error you are getting is basically because of the certificate.

Having said that, I think you should use the v2 of the API as although it is still awful, there are examples out there and you don't need to use a cert.

Rick Strahl has successfully changed the namespaces in the v2 version, see here https://weblog.west-wind.com/posts/2016/Apr/02/Custom-Message-Formatting-in-WCF-to-add-all-Namespaces-to-the-SOAP-Envelope .

Ryan O'Neill
  • 5,410
  • 4
  • 46
  • 69
0

There is one new Royal Mail Shipping API 2 available , after I've lost many hours try development the integration with Royal Mail I finally found a way. I'm sharing my project in the git. https://github.com/americoa/RoyalMailShippingAPIV2

Americo Arvani
  • 828
  • 1
  • 8
  • 16