29 July 2016

Hacking Around With Virgin Atlantic's InFlight WiFi

DISCLAIMER:I use the word 'hacking' in the truest sense, I didn't probe the plane network or attempt to get anywhere I wasn't supposed to. I simply captured / repeated traffic that my browser would have been sending if sat on the Captive Portal landing page

I recently flew back from the United States on a Virgin Atlantic plane that offered in-flight wifi, unfortunately I didn't connect till a few hours into my flight but once I did connect I found something interesting.

When you connect to the onboard WiFi you are redirected to a landing page that tells you about the system and how to pay etc, as I was hunting for a credit card (£14.99 to get Internet connectivity at 40k feet seems OK) I noticed that my laptop was still sending and receiving traffic on the network. Curious as to what it was I loaded the browser debug tools and noticed that the browser was issuing a GET request to http://api.airpana.com/inflight/services/flightdata/v1/flightdata every second. The information returned was very interesting!

{
"td_id_decompression":"0",
"td_id_weight_on_wheels":"0",
"td_id_x2_pa_state":"0",
"td_id_fltdata_ground_speed":"0528",
"td_id_fltdata_time_to_destination":"0228",
"td_id_fltdata_wind_speed":"-026",
"td_id_fltdata_mach":"0857",
"td_id_fltdata_true_heading":"0068",
"td_id_fltdata_gmt":"0422",
"td_id_fltdata_outside_air_temp":"8044",
"td_id_fltdata_head_wind_speed":"",
"td_id_fltdata_date":"00260716",
"td_id_fltdata_distance_to_destination":"00001926",
"td_id_fltdata_altitude":"00041002",
"td_id_fltdata_present_position_latitude":"00050405",
"td_id_fltdata_present_position_longitude":"80051148",
"td_id_fltdata_destination_latitude":"00051280",
"td_id_fltdata_destination_longitude":"80000265",
"td_id_fltdata_destination_id":"EGLL",
"td_id_fltdata_departure_id":"KJFK",
"td_id_fltdata_flight_number":"VS046",
"td_id_fltdata_destination_baggage_id":"LHR",
"td_id_fltdata_departure_baggage_id":"JFK",
"td_id_airframe_tail_number":"G-VBZZ",
"td_id_flight_phase":"7",
"td_id_gmt_offset_departure":"80005.00",
"td_id_gmt_offset_destination":"00000.00",
"td_id_route_id":"43",
"td_id_fltdata_distance_from_origin":"00001120",
"td_id_fltdata_estimated_arrival_time":"0910",
"td_id_fltdata_time_at_takeoff":"002607160213",
"td_id_fltdata_departure_latitude":"00040375",
"td_id_fltdata_departure_longitude":"80073465",
"td_id_pdi_fltdata_departure_iata":"",
"td_id_pdi_fltdata_departure_time_scheduled":"",
"td_id_pdi_fltdata_arrival_iata":"",
"td_id_fltdata_wind_direction":"0267",
"td_id_media_date":"20160726",
"td_id_extv_channel_listing_version":""
}

Yup, that's live flight telemetry! I set up a while loop to grab this data every 10 seconds and dump it to disk.

Graphs

The graphs below show some interesting information such as temperatures at certain heights, the relationship between altitude and ground speed or that at one point we flew a touch further away from our destination.







GPS

The interesting thing to note about the GPS is that it is very jagged compared to data from an entity such as FlightAware



Understanding the other fields

I downloaded a copy of the various javascript files that make up the landing page e.g. engine_min.js (1.1Mb big!) and inflight.js to try and understand some of the other fields.

The first answer we get is that the flight phase entry, of which we saw 7 and 8, has 16 phases

            FlightPhase: {
                UNKNOWN: 0,
                POWER_UP: 1,
                ENGINES_START: 2,
                TAKE_OFF_POWER: 3,
                ACCELERATING: 4,
                LIFT_OFF: 5,
                CLIMB: 6,
                CRUISING: 7,
                DESCENDING: 8,
                APPROACH: 9,
                GO_AROUND: 10,
                FLARE: 11,
                TOUCHDOWN: 12,
                TAXI_TO_STOP: 13,
                ENGINES_STOP: 14,
                MAINTENANCE: 15
            }

When generating the graphs I had to deal with a decision made by a programmer to represent minus values as having an 8 at the beginning and padded with zeroes. This guess was shown correct by finding the following;

        r = function(a) {
            var c = parseFloat(a.slice(1, 5)),
                c = c + parseFloat(a.slice(5, 7)) / 60,
                c = c + parseFloat(a.slice(7)) /
                600;
            "8" === a[0] && (c *= -1);
            return c
        },

By looking through the source code we can also find what measurement units the various values are;

        z = {
            ALTITUDE_FEET: "altitude_feet",
            CURRENT_COORDINATES: "current_coordinates",
            DISTANCE_FROM_DEPARTURE_NAUTICAL_MILES: "distance_from_departure_nautical_miles",
            DISTANCE_TO_DESTINATION_NAUTICAL_MILES: "distance_to_destination_nautical_miles",
            GROUND_SPEED_KNOTS: "ground_speed_knots",
            FLIGHT_SPEED_MACH: "flight_speed_mach",
            TIME_TO_DESTINATION_MINUTES: "time_to_destination_minutes",
            OUTSIDE_AIR_TEMP_CELSIUS: "outside_air_temp_celsius"
        };

Other URLs

DISCLAIMER: I did NOT crawl these URLs.

Looking at the source code we find a function that constructs URLs for making requests, grepping for that function call exposes a lot of other URLs that could be of interest.

baseURL:InFlight.makeServiceURL("/event/v1/eventstream")
h=InFlight.makeServiceURL("/network/v1/ping"),
url:InFlight.makeServiceURL("/airport_info/v1/multi_airport_info?icao_codes="+c.icao_codes+"&"), 
url:InFlight.makeServiceURL("/analytics/v1/log"),
url:InFlight.makeServiceURL("/apiapi/v1/index"),
url:InFlight.makeServiceURL("/captcha/v1/new_challenge")
url:InFlight.makeServiceURL("/captcha/v1/solution"),
url:InFlight.makeServiceURL("/exconnect/v1/status")
url:InFlight.makeServiceURL("/exconnect/v1/enable_device_for_whitelist")
url:InFlight.makeServiceURL("/exconnect/v1/check_captcha_requirement"),
url:InFlight.makeServiceURL("/exconnect/v1/wisp"),
url:InFlight.makeServiceURL("/exconnect/v1/device_state")
url:InFlight.makeServiceURL("/extv_metadata/v1/commissioning_status")
url:InFlight.makeServiceURL("/extv_metadata/v1/stations"),
url:InFlight.makeServiceURL("/extv_metadata/v1/station_status"),
url:InFlight.makeServiceURL("/flightdata/v1/flightdata")
url:InFlight.makeServiceURL("/livetextnews/v1/providers"),
url:InFlight.makeServiceURL("/livetextnews/v1/categories"),
url:InFlight.makeServiceURL("/livetextnews/v1/article"),
url:InFlight.makeServiceURL("/livetextnews/v1/current_weather"),
url:InFlight.makeServiceURL("/livetextnews/v1/forecast_weather"),
url:InFlight.makeServiceURL("/maps/v1/flight_map")
url:InFlight.makeServiceURL("/metadata/v1/media_keytype_count"),
url:InFlight.makeServiceURL("/metadata/v1/categories"),
url:InFlight.makeServiceURL("/metadata/v1/category_media"),
url:InFlight.makeServiceURL("/metadata/v1/media_metadata"),
url:InFlight.makeServiceURL("/metadata/v1/child_media"),
url:InFlight.makeServiceURL("/one_media/v1/banner"),
url:InFlight.makeServiceURL("/one_media/v1/interstitial"),
e=InFlight.makeServiceURL("/payment/v1/form",!0),
url:InFlight.makeServiceURL("/payment/v1/get_token",!0),
url:InFlight.makeServiceURL("/cmi/payment/v1/all_orders"),
url:InFlight.makeServiceURL("/cmi/payment/v1/order"),
url:InFlight.makeServiceURL("/cmi/payment/v1/void_orders"),
url:InFlight.makeServiceURL("/cmi/payment/v1/refund"),
url:InFlight.makeServiceURL("/cmi/payment/v1/generate_comp_code"),
url:InFlight.makeServiceURL("/cmi/payment/v1/update_comp_code"),
url:InFlight.makeServiceURL("/cmi/payment/v1/all_comp_codes"),
url:InFlight.makeServiceURL("/cmi/payment/v1/delete_comp_code"),
url:InFlight.makeServiceURL("/cmi/payment/v1/validate_crew_id"),
url:InFlight.makeServiceURL("/ppv/v1/check_payment_requirement",!0),
url:InFlight.makeServiceURL("/ppv/v1/check_purchase_requirement",!0),
url:InFlight.makeServiceURL("/ppv/v1/purchase",!0),
url:InFlight.makeServiceURL("/ppv/v2/check_payment_requirement",!0),
url:InFlight.makeServiceURL("/ppv/v2/check_purchase_requirement",!0),
url:InFlight.makeServiceURL("/ppv/v2/purchase",!0),
url:InFlight.makeServiceURL("/cmi/route_control/v1/data"),
url:InFlight.makeServiceURL("/status/v1/status")

Conclusion

I didn't quite make the plane go sidewards but it was interesting to see this data made available to users on the WiFi. Whether this is a good thing or not might be a discussion for another day, personally I'm looking forward to someone releasing an app that makes use of this API!