Sabre API Updates For PCI TLS v1.2 + High Availability URLs

I’ve been receiving email from Sabre regarding Payment Card Industry (PCI) TLS v1.2 security deadlines and Sabre’s High Availability (HA) infrastructure. (Sabre has introduced new URLs for use with Sabre APIs).

I’ve navigated a maze of URLs and PDFs from Sabre. The majority of these were not helpful to me but I muddled through and reduced the upgrade process to a few steps. Note that these steps handle both the high availability URL updates and the TLS v1.2 updates.

The steps below are accomplished using Visual Studio Professional 2017. If you’re not using it yet, you should be.

Step 1 – Update to use the High Availability URLs

In VS, Find and Replace in the entire solution (*.cs files only)
From this : https://webservices.sabre.com
To this : https://webservices.havail.sabre.com

At this point, I tried to run against the new HA URLs .. but no luck, here’s the error:
Exception Message : The request was aborted: Could not create SSL/TLS secure channel.

Step 2 – Update to the .NET Framework 4.6.1

I updated ALL of my projects to .NET Framework 4.6.1, this handles the TLS v1.2 upgrade.

Step 3 – Recompile and Run

Nothing else to do, I’m running now with the new HA URLs and also using TLS v1.2.

Where’s my seat?

Summary: Sabre TravelItineraryReadService TravelItineraryReadRQ version 2.2.0 returns incomplete Seat data. TravelItineraryReadRQ version 3.4.0 returns all of the Seat data.

I had bug in my PNR capture code – it was throwing an exception on my persist flight details stored procedure call because I wasn’t giving it a required seat number. It was a trivial code fix, I needed to pass an empty string to the stored procedure (not a null value).

But did I have a bug further upstream in my TravelItineraryReadRS parsing and mapping code? Or is the data really missing?

My current version of the TravelItineraryReadService is using TravelItineraryReadRQ version 2.2.0. Below is the snippet from the TravelItineraryReadRS. You can see that it’s only returning a seat number for segment 2, all of the other segments have no value for the seat number – the attribute is missing entirely. So I don’t have any parsing/mapping issues, sabre isn’t giving me the data.

<TravelItineraryReadRS Version="2.2.0">
...
  <Seats>
	<Seat NameNumber="01.01" SegmentNumber="0001" Status="HRQ">
	  <FlightSegment>
		<DestinationLocation LocationCode="PHX" />
		<OriginLocation LocationCode="SBA" />
	  </FlightSegment>
	</Seat>
	<Seat Changed="N" NameNumber="01.01" Number="04A" SegmentNumber="0002" SegmentStatus="HK" SmokingPreference="N" Status="HRS" TypeTwo="WLMI">
	  <FlightSegment>
		<DestinationLocation LocationCode="ORD" />
		<OriginLocation LocationCode="PHX" />
	  </FlightSegment>
	</Seat>
	<Seat NameNumber="01.01" SegmentNumber="0003" Status="HRQ">
	  <FlightSegment>
		<DestinationLocation LocationCode="PHX" />
		<OriginLocation LocationCode="ORD" />
	  </FlightSegment>
	</Seat>
	<Seat NameNumber="01.01" SegmentNumber="0004" Status="HRQ">
	  <FlightSegment>
		<DestinationLocation LocationCode="SBA" />
		<OriginLocation LocationCode="PHX" />
	  </FlightSegment>
	</Seat>
  </Seats>
...
</TravelItineraryReadRS>

I’m in the middle of evaluating (and implementing) TravelItineraryReadRQ version 3.4.0. Cool, it has my seat data! Maybe I need to roll this into production sooner than planned…

<TravelItineraryReadRS Version="3.4.0">
...
  <Seats>
	<Seat Changed="N" NameNumber="01.01" Number="02A" SegmentNumber="0001" SegmentStatus="PN" SmokingPreference="N" Status="HRS" TypeTwo="">
	  <FlightSegment>
		<DestinationLocation LocationCode="PHX" />
		<OriginLocation LocationCode="SBA" />
	  </FlightSegment>
	</Seat>
	<Seat Changed="N" NameNumber="01.01" Number="04A" SegmentNumber="0002" SegmentStatus="HK" SmokingPreference="N" Status="HRS" TypeTwo="WLMI">
	  <FlightSegment>
		<DestinationLocation LocationCode="ORD" />
		<OriginLocation LocationCode="PHX" />
	  </FlightSegment>
	</Seat>
	<Seat Changed="N" NameNumber="01.01" Number="03A" SegmentNumber="0003" SegmentStatus="PN" SmokingPreference="N" Status="HRS" TypeTwo="">
	  <FlightSegment>
		<DestinationLocation LocationCode="PHX" />
		<OriginLocation LocationCode="ORD" />
	  </FlightSegment>
	</Seat>
	<Seat Changed="N" NameNumber="01.01" Number="03F" SegmentNumber="0004" SegmentStatus="PN" SmokingPreference="N" Status="HRS" TypeTwo="">
	  <FlightSegment>
		<DestinationLocation LocationCode="SBA" />
		<OriginLocation LocationCode="PHX" />
	  </FlightSegment>
	</Seat>
  </Seats>
...
</TravelItineraryReadRS>

Solution for my bad dates

Following up my previous post “I have bad dates“, here’s my solution. I may have to tweak this some at the end of the year but this seems to be working for now.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//
public static DateTime wsGetDate(string badSabreDate, DateTime srcDT)
{
    // force the bad sabre date into a leap year
    var leapYear = "2000-" + badSabreDate;
 
    var year = srcDT.Year;
    DateTime dt = DateTime.Now.AddYears(-1);
    if (DateTime.TryParse(leapYear, out dt))
    {
	if (dt.Month < srcDT.Month)
	{
	    // parsed month is less than srcDT, increment my year
	    year++;
	}
 
	return new DateTime(year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second);
    }
 
    var msg2 = string.Format("Can't parse {0} into a valid DateTime, {1}", badSabreDate, leapYear);
    throw new ApplicationException(msg2);
}
//

And here’s a piece of the car segment parsing code that calls the new method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//
if (_Resp.TravelItinerary.ItineraryInfo.ReservationItems[i].Vehicle != null)
{
    #region CAR
    var car = _Resp.TravelItinerary.ItineraryInfo.ReservationItems[i].Vehicle;
    Log.Debug("Car --------------------------------------------------");
    string confID = "";
    if (car.ConfirmationNumber != null) confID = SabreWS.wsString(car.ConfirmationNumber);
 
    int RPHv = SabreWS.wsInt(car.SegmentNumber);
    string statusv = SabreWS.wsString(car.Status);
    string ID = "";
    if (car.POS != null) ID = SabreWS.wsString(car.POS.Source.RequestorID);
 
    Log.Debug(RPHv + statusv.PadLeft(4) + " - " + " requestorID: " + ID);
 
    string PU = SabreWS.wsString(car.VehRentalCore.LocationDetails.LocationCode);
    tmpForeignSts = AirportLookupCache.GetForeignStatus(PU);
    ForeignSts = tmpForeignSts > ForeignSts ? tmpForeignSts : ForeignSts;
 
    DateTime PUDT = wsGetDate(car.VehRentalCore.PickUpDateTime, DateTime.Now);
    Log.Debug("Pickup1: " + PU.PadRight(5) + PUDT.ToString());
 
    string RT = "";
    if (car.VehRentalCore.DropOffLocationDetails != null) RT = SabreWS.wsString(car.VehRentalCore.DropOffLocationDetails.LocationCode);
    if (RT.Length == 0) RT = PU;
    tmpForeignSts = AirportLookupCache.GetForeignStatus(RT);
    ForeignSts = tmpForeignSts > ForeignSts ? tmpForeignSts : ForeignSts;
 
    DateTime RTDT = PUDT;
    try
    {
	RTDT = wsGetDate(car.VehRentalCore.ReturnDateTime, PUDT);
    }
    catch (Exception a)
    {
	a.Message.ToString();
	Log.Error(car.VehRentalCore.ReturnDateTime + " --> error parsing this as the Return DateTime, forcing to PUDT");
    }
...
//

I have bad dates

Some back story … I use Sabre web services to read and extract travel data into a database. The data is used for a suite of travel related products and applications. If you’ve worked on travel applications, you know that it’s some wild west programming – data consistency is awful.

Web services was supposed to fix everything because now it’s structured data, no more screen scraping! Well, it is better than it was in the past but there are still problems.

Sabre web services has an odd bug that only strikes on the last day of the month AND your car/hotel segment starts on that day. See the PickUpDateTime below – it should be 2013 not 2014. (Notice that they have the ReturnDateTime correct.)

1
2
3
4
5
6
7
8
<OTA_TravelItineraryRS Version="2003A.TsabreXML1.15.1">
. . .
    <VehRentalCore PickUpDateTime="2014-07-31T16:30:00" PickUpDay="3" ReturnDateTime="2013-08-02T16:00:00">
      <PickUpLocation LocationCode="ATL" CodeContext="IATA" />
      <ReturnLocation LocationCode="JAX" CodeContext="IATA" />
    </VehRentalCore>
. . .
</OTA_TravelItineraryRS>

Since I’m running an older version of the TravelItinerary web service, thought I’d try the latest and greatest version available to see if the problem is fixed. As you can see below, Sabre’s solution is to drop the year. Nice. Are you kidding me? We went from the wrong date to an invalid ISO format across the board?

1
2
3
4
5
6
7
8
<TravelItineraryReadRS Version="2.2.0">
. . .
    <VehRentalCore PickUpDateTime="07-31T16:30" PickUpDay="3" ReturnDateTime="08-02T16:00">
      <DropOffLocationDetails LocationCode="JAX" />
      <LocationDetails LocationCode="ATL" />
    </VehRentalCore>
. . .
</TravelItineraryReadRS>

…I’d love to hear the reasoning behind that solution hack. Why is there such a lack of quality these days? I have some stories I want to post about this later…

Now I have to write new logic to decode this crap which will replace the old logic I had to fix the original crap.

Frustration …

Frustrated with Sabre web services, grrrrr!

The version of the TravelItineraryRead web service I’m using is “2003A.TsabreXML1.15.1”. Yes, I know its a bit dated but it fits the needs .. until recently. I’m seeing more Rail segments come across and this version does not support them. So, I wanted to test the latest TravelItineraryRead version to see how or if Rail is supported. 

So I built my proxy classes using 
wsdl.exe  http://webservices.sabre.com/wsdl/tpfc/TravelItineraryReadLLS2.2.0RQ.wsdl  /namespace:OTA_TravelItineraryReadNS2_2_0
And setup my code to call the new service  

1
2
3
4
5
. . .
var serviceObj = new TravelItineraryReadService();
serviceObj.MessageHeaderValue = msgHeader;
serviceObj.Security = security;
TravelItineraryReadRS response = serviceObj.TravelItineraryReadRQ(req);

… and I get a null response. No errors or exceptions just a null response. So, I tweak my objects and try again – same null response. Make more changes – same null response. I go through several cycles of this and I’m stuck, I need help – another set of eyes to see what I’m doing wrong. (BTW, Sabre’s documentation on this web service is horrible. This is the root of my problem)

Next I contact Sabre’s web services support through email. This is my frustration … The email conversation goes on for 6 days. Maybe their support group is outside the USA or their policy it to email a maximum of a few times per day. Whichever the case, it is painfully slow dealing with them. The end result of this painfully slow email conversation is nothing, nada, zip, still getting null. The support group doesn’t have aqueduct documentation, no code samples, no code ownership and certainly no expertise.

 Long story shortened, this was my problem:

1
2
3
4
5
6
7
8
9
10
11
12
13
MessageHeader msgHeader = new MessageHeader();
 
// If the msgHeader.Action is not exactly 
// "TravelItineraryReadLLSRQ"
// your response will be null
msgHeader.Action = "TravelItineraryReadLLSRQ";
 
Service service = new Service();
service.Value = "TravelItineraryReadLLSRQ"; // THIS IS NOT USED!!!
service.type = "this object can be null";
msgHeader.Service = service; // THIS CAN BE NULL, IT'S NOT USED
 
. . .

Now I can finally start my research on Rail segments!!

There are not many “open” resources available for Sabre web services. I’ve written a lot of code to consume them…. contact me if you are stuck, maybe I can help.