A quick peek behind the curtain of the Nascar Mobile client from Sprint

Sprint has this app that shows where the cars are during a Nascar race. Well, and video from inside them, stats, commentary, etc – they really lay it on thick with the stats which is very nice for people who like Nascar (they also sponsor the main series, the Sprint Cup). With their latest tiers they’re one of the cheapest (possibly even the cheapest) cell service, so we’re now using them. My wife considers the Nascar tracking a big bonus. I don’t use it really, but I’m happy she’s happy.

They have another thing on the computer called RaceView which is possible to buy separately (or could, I haven’t kept track) which we had before and I tried to see if any of those heaps of numbers were accessible for a third party client. After all, there’s always some custom stat you’d perhaps like to have (say, how fast is my driver going compared to the driver ahead or the leader, how much is that number changing and what will happen if the trends hold) since it’s kind of a matter of taste. It wasn’t really doable then, or at least didn’t seem like it. Nearly all giant binary blobs with very little to say what was what. It wasn’t exactly protected so much as a mess – reverse engineering it would have been a major undertaking.

This time, I started with simply opening Pymiproxy, a little man-in-the-middle proxy that can be customized to change/log/etc the traffic you run through it. Simple, but very handy when you’re debugging web stuff. Anything would do really, I only ended up using it as a logger. I first set it just spit out the headers and it said:

>>['GET /config-version-mapping.json HTTP/1.1', 'x-newrelic-id: (** BASE64BLOB **)', 'User-Agent: Dalvik/1.6.0 (Linux; U; Android 4.4.2; LG-LS980 Build/KOT49I.LS980ZVC)', 'Host: mobile.nascar.com.edgesuite.net', 'Accept-Encoding: gzip', 'Proxy-Connection: close', 'Connection: close']

<<['HTTP/1.1 200 OK', 'Server: Apache', 'ETag: "b3b4354512563eff89182e170abb6707:1406390366"', 'Last-Modified: Sat, 26 Jul 2014 15:59:26 GMT', 'Accept-Ranges: bytes', 'Content-Length: 7336', 'Content-Type: text/plain', 'Date: Sun, 03 Aug 2014 17:31:45 GMT', 'Connection: close'] >>['GET /3.4.0/config/json/config_master.json HTTP/1.1', 'x-newrelic-id: (** BASE64BLOB **)', 'User-Agent: Dalvik/1.6.0 (Linux; U; Android 4.4.2; LG-LS980 Build/KOT49I.LS980ZVC)', 'Host: mobile.nascar.com.edgesuite.net', 'Accept-Encoding: gzip', 'Proxy-Connection: close', 'Connection: close']

<<['HTTP/1.1 200 OK', 'Server: Apache', 'ETag: "0b15e11168ab37296d472b227e3f2571:1407086977"', 'Last-Modified: Sun, 03 Aug 2014 17:29:37 GMT', 'Accept-Ranges: bytes', 'Content-Length: 19107', 'Content-Type: text/plain', 'Date: Sun, 03 Aug 2014 17:31:46 GMT', 'Connection: close'] >>['GET /3.4.0/config/json/config_coors.json HTTP/1.1', 'x-newrelic-id: (** BASE64BLOB **)', 'User-Agent: Dalvik/1.6.0 (Linux; U; Android 4.4.2; LG-LS980 Build/KOT49I.LS980ZVC)', 'Host: mobile.nascar.com.edgesuite.net', 'Accept-Encoding: gzip', 'Proxy-Connection: close', 'Connection: close']

>>['GET /data/cacher/production/2014/race-list-basic.json HTTP/1.1', 'x-newrelic-id: (** BASE64BLOB **)', 'User-Agent: Dalvik/1.6.0 (Linux; U; Android 4.4.2; LG-LS980 Build/KOT49I.LS980ZVC)', 'Host: mobile.nascar.com.edgesuite.net', 'Accept-Encoding: gzip', 'Proxy-Connection: close', 'Connection: close']

>>['GET /3.4.0/config/json/config_alerts_menu.json HTTP/1.1', 'x-newrelic-id: (** BASE64BLOB **)', 'User-Agent: Dalvik/1.6.0 (Linux; U; Android 4.4.2; LG-LS980 Build/KOT49I.LS980ZVC)', 'Host: mobile.nascar.com.edgesuite.net', 'Accept-Encoding: gzip', 'Proxy-Connection: close', 'Connection: close']

>>['GET /CMS/live-na.json HTTP/1.1', 'x-newrelic-id: (** BASE64BLOB **)', 'User-Agent: Dalvik/1.6.0 (Linux; U; Android 4.4.2; LG-LS980 Build/KOT49I.LS980ZVC)', 'Host: mobile.nascar.com.edgesuite.net', 'Accept-Encoding: gzip', 'Proxy-Connection: close', 'Connection: close']

Ok.. It's just headers, but that kind of looks like fairly average http and it looks like there is JSON spoken of. I replicate the very scratch basics of it to see if there's some hidden gotcha to force being on a phone (I did 'curl -H 'x-newrelic-iod: COPIED_B64_BLOB' -A 'Dalvik/1.6.0 (Linux; U; Android 4.4.2; LG-LS980 Build/KOT49I.LS980ZVC)' mobile.nascar.com.edgesuite.net/data/cacher/production/live/live_feed.json') or otherwise do something more exotic for authentication. Nope, seems to be satisfied I can be there (which in fact I can - I don't know how safe it's made from non-customers). Ok, pushed ahead and got some more of it, just following my own inital headers:

/config-version-mapping.json:

{
 "master-configuration": {
 "1.0": "http://mobile.nascar.com.edgesuite.net/1.0/config/json/config_master.json",
 "default": "http://mobile.nascar.com.edgesuite.net/1.0/config/json/config_master.json",

..... 

 "3.4.0": "http://mobile.nascar.com.edgesuite.net/3.4.0/config/json/config_master.json",
 "3.5.0": "http://mobile.nascar.com.edgesuite.net/3.5.0/config/json/config_master.json",
 "x.x.x": "http://master-config-url-for-version-x-x-x"
 },
 "master-configuration-ios": {
 "1.0": "http://mobile.nascar.com.edgesuite.net/1.0/config/json/config_master.json",
 "default": "http://mobile.nascar.com.edgesuite.net/1.0/config/json/config_master.json",
 "2.0.1": "http://mobile.nascar.com.edgesuite.net/2.0.1/config/json/config_master.json",
   ......
 "3.4.0": "http://mobile.nascar.com.edgesuite.net/3.4.0/config/json/config_master.json",
 "3.5.0": "http://mobile.nascar.com.edgesuite.net/3.5.0/config/json/config_master.json",
 "x.x.x": "http://master-config-url-for-version-x-x-x"
 },
 "master-configuration-android": {
 "1.0": "http://mobile.nascar.com.edgesuite.net/1.0/config/json/config_master.json",
 "default": "http://mobile.nascar.com.edgesuite.net/1.0/config/json/config_master.json",
        -------------
 "3.4.0": "http://mobile.nascar.com.edgesuite.net/3.4.0/config/json/config_master.json",
 "3.5.0": "http://mobile.nascar.com.edgesuite.net/3.5.0/config/json/config_master.json",
 "x.x.x": "http://master-config-url-for-version-x-x-x"
 },
 "update-configuration-ios": {
 "actual-version": "3.4.0",
 "force-update": "true"
 },
 "update-configuration-android": {
 "actual-version": "14003400",
 "force-update": "true"
 }
}
--

/config_master.json

---
{ "MediaFeeds": { "brightcove_featured_playlist": "20xxxxxxxxxxxxxxx4001", "brightcove_media_api_key": "--------------------", "use_production_brightcove_media_api_key": "true", "brightcove_media_api_key_production": "000000000000000000000000000000000", "brightcove_media_api_key_dev": "0000000000000", "newsAndVideoArticles": "2014/{series_id}/article.json", "newsAndVideoImages": "2014/{series_id}/image.json", "newsAndVideoVideo": "2014/{series_id}/video.json", "newsAndVideo-video-configuration-url": "http://mobile.nascar.com.edgesuite.net/2.6.0/config/json/config_video_categories.json", "missSprintCupArticles": "2014/{series_id}/miss_sprint_cup_article.json", "missSprintCupVideo": "{year}/{series_id}/miss_sprint_cup_video.json", "missSprintCupTwitter": "http://api.massrelevance.com/SapientNASCAR/mobile-14-misssprintcup.json", "race_hub_media_url": "http://mobile.nascar.com.edgesuite.net/data/cacher/production/2014/{series_id}/article.json", "videoLiveConfigPerSeries": { "1": "http://mobile.nascar.com.edgesuite.net/3.4.0/config/json/config_video_live_streams.json", "2": "http://mobile.nascar.com.edgesuite.net/3.4.0/config/json/config_video_live_streams.json", "3": "http://mobile.nascar.com.edgesuite.net/3.4.0/config/json/config_video_live_streams.json" }, "baseUrlInCarAudioPerSeries": { "1": "http://raceviewnscs-lh.akamaihd.net/i/", "2": "http://raceviewnns-lh.akamaihd.net/i/", "3": "http://raceviewncwts-lh.akamaihd.net/i/" }, "inCarAudioFeedPerSeries": { "1": "http://www.nascar.com/config/audio/audio_mapping_1_3.json", "2": "http://www.nascar.com/config/audio/audio_mapping_2_3.json", "3": "http://www.nascar.com/config/audio/audio_mapping_3_3.json" }, "raceRadioAvailableChannelsPerSeries": { "1": ["all"], "2": ["all"], "3": ["radio"] }, "brightcovePlaylistsPerSeries_dev": { "1": "111111111111111", "2": "222222222222222", "3": "3333333333333333", "featured

    ------------------

 "toastMessageEventAlreadyExists": "{event_name} already exists in calendar.", "toastMessageEventWasAdded": "{event_name} was added to the calendar.", "toastMessageEventFailedToAdd": "Can't save event to store, title={event_name}, date={event_date},error={error_text}", "toastMessageNoAccessToCalendar": "Unable to save event, calendar access required.", "toastMessageInvalidEventOrDate": "It is impossible to add event to calendar.", "toastMessageSubscriptionProductLoading": "Loading product information from app store ...", "toastMessageSubscriptionProductLoadFailed": "Load product information from app store failed!", "toastMessageSubscriptionNoConnection": "The application is unable to complete request at this time. Please try again later. Thanks.", "sprint-highlights-additional-text":"", "sprint-highlights-unavailable-text":"Sprint Highlights Will Be Available Soon", "sprint-highlights-locked-for-nonsprint-text":"Sprint Highlights Are not available for non-sprint users", "leaderboard-locked-for-unsubscribed- ...  }
---


... some chopped, I don't have the patience and precision to remove everything that might be an api key. But the rest reads about the same! This is awesome!! It's practically polite - things are labeled and laid out in a reasonably standard format. I didn't dig too much deeper, but did grab /data/cacher/production/live/live_feed.json (the next obvious thing mentioned and which the phone started cycling):

{"lap_number":59,"elapsed_time":4088.0,"flag_state":1,"race_id":4304,"laps_in_race":160,"laps_to_go":101,"vehicles":[{"average_restart_speed":169.453,"average_running_position":7.898,"average_speed":129.969,"best_lap":4,"best_lap_speed":176.201,"best_lap_time":51.078,"vehicle_manufacturer":"Chv","vehicle_number":"42","driver":{"driver_id":4030,"full_name":"Kyle Larson #","first_name":"Kyle","last_name":"Larson #","is_in_chase":false},"vehicle_elapsed_time":4085.5969,"fastest_laps_run":0,"laps_completed":59,"laps_led":[{"start_lap":0,"end_lap":0}],"last_lap_speed":170.467,"last_lap_time":52.796,"passes_made":16,"passing_differential":-8,"pit_stops":[{"positions_gained_lossed":0,"pit_in_elapsed_time":0.0,"pit_in_lap_count":0,"pit_in_leader_lap":0,"pit_out_elapsed_time":0.0},{"positions_gained_lossed":0,"pit_in_elapsed_time":2192.7894,"pit_in_lap_count"             ....                   elapsed_time":728.7847,"pit_in_lap_count":10,"pit_in_leader_lap":11,"pit_out_elapsed_time":0.0},{"positions_gained_lossed":0,"pit_in_elapsed_time":2321.1367,"pit_in_lap_count":31,"pit_in_leader_lap":31,"pit_out_elapsed_time":2377.4462},{"positions_gained_lossed":0,"pit_in_elapsed_time":3449.6685,"pit_in_lap_count":50,"pit_in_leader_lap":51,"pit_out_elapsed_time":0.0}],"qualifying_status":0,"running_position":40,"status":6,"delta":-8.0,"sponsor_name":"Land Castle Title","starting_position":41,"times_passed":5,"quality_passes":0,"is_on_track":false}],"run_id":15,"run_name":"GoBowling.com 400","series_id":1,"time_of_day":65817.0,"track_id":198,"track_length":2.5,"track_name":"Pocono Raceway","run_type":3,"number_of_caution_segments":4,"number_of_caution_laps":14,"number_of_lead_changes":3,"number_of_leaders":3}


It really is pretty much as it seems. Once you grab the key to authenticate yourself, all (that I've browsed through) is pretty self explanatory.

Dug out a few fields from the above and did a quick proof-of-concept in python:

import urllib2
import json

url = 'http://mobile.nascar.com.edgesuite.net/data/cacher/production/live/live_feed.json' # url to the plainest of the live feeds
hdr = [('x-newrelix-iod','SECRET_KEY'), ('User-agent','Dalvik/1.6.0 (Linux; U; Android 4.4.2)')] # headers from phone

opener = urllib2.build_opener() # Construct an url opener
opener.addheaders = hdr # tell it to use the headers

while True: # loop
    page = opener.open(url) # open it
    stats = json.load(page) # grab a heaping load of data
    print chr(27) + '[0;0f' # Awesome ANSI formatting
    print "Lap: " + str(stats['lap_number']) + ' (' + str(stats['laps_in_race']) + ') ' #  Show a few low hanging stats
    
    for a in stats['vehicles']: # Sub-dictionary with per-vehicle stats
        if a['vehicle_number'] == u'14':  # Wife is Tony Stewart Fan
            ind = '-> '
        else:
            ind = ' '

        print ind + '{0:03}'.format(a['running_position']) + ' {0:3}'.format(a['vehicle_number']) + ' ' + a['driver']['full_name'] + ' last lap {} best lap {}'.format(a['last_lap_speed'],a['best_lap_speed'])    # A few more

quit() # Never reached, the end


This could all very easily be turned into something much deeper. Heres the key structure:

{
 "elapsed_time":4088.0,
 "flag_state":1,
 "lap_number":59,
 "laps_in_race":160,
 "laps_to_go":101,
 "number_of_caution_laps":14,
 "number_of_caution_segments":4,
 "number_of_lead_changes":3,
 "number_of_leaders":3,
 "race_id":4304,
 "run_id":15,
 "run_name":"GoBowling.com 400",
 "run_type":3,
 "series_id":1,
 "time_of_day":65817.0,
 "track_id":198,
 "track_length":2.5,
 "track_name":"Pocono Raceway",
 "vehicles":[
 {
 "average_restart_speed":169.453,
 "average_running_position":7.898,
 "average_speed":129.969,
 "best_lap":4,
 "best_lap_speed":176.201,
 "best_lap_time":51.078,
 "delta":8.679,
 "driver":{
 "driver_id":4030,
 "first_name":"Kyle",
 "full_name":"Kyle Larson #",
 "is_in_chase":false,
 "last_name":"Larson #"
 },
 "fastest_laps_run":0,
 "is_on_track":true,
 "laps_completed":59,
 "laps_led":[
 {
 "end_lap":0,
 "start_lap":0
 }
 ],
 "last_lap_speed":170.467,
 "last_lap_time":52.796,
 "passes_made":16,
 "passing_differential":-8,
 "pit_stops":[
 {
 "pit_in_elapsed_time":0.0,
 "pit_in_lap_count":0,
 "pit_in_leader_lap":0,
 "pit_out_elapsed_time":0.0,
 "positions_gained_lossed":0
 },
 {
 "pit_in_elapsed_time":2192.7894,
 "pit_in_lap_count":30,
 "pit_in_leader_lap":30,
 "pit_out_elapsed_time":2228.4684,
 "positions_gained_lossed":0
 }
 ],
 "qualifying_status":0,
 "quality_passes":11,
 "running_position":13,
 "sponsor_name":"Target",
 "starting_position":1,
 "status":1,
 "times_passed":24,
 "vehicle_elapsed_time":4085.5969,
 "vehicle_manufacturer":"Chv",
 "vehicle_number":"42"
 },
 {

 ... 42 Vehicles later ..

 }
 ]
}

--
From:
import json
import sys

dict = json.load(sys.stdin)
print json.dumps(dict, sort_keys=True, indent = 4, separators=(',',':'))
--
The others are fairly similar except about their particular fields, though the actual number crunch on the cars is probably the most vital.

Leave a Reply