Ikon Reservation Notifier

Ikon Reservation Notifier site (down indefinitely)
github

For the 2020-2021 ski season, both main ski pass operators (Ikon and Epic) enacted a reservation system for the busier resorts to reduce mountain capacity in hopes of curbing COVID-19 risk. This wasn’t an issue early in the season, but scarcity of reservations for certain resorts became painfully clear the first popular week of skiing, the week between Christmas and New Years.

I figured this problem would persist at least through the end of the season for holiday weeks and weekends. I was more correct than I wished unfortunately - resorts like Jackson Hole and Crystal Mountain saw almost entire months fill up in advance. The last I looked in mid-February, Jackson Hole did not have a single open reservation on a Saturday or Sunday from that point until the end of the season.

I wanted to build an automatic monitoring system that would notify me whenever any reservations opened for dates that I was planning on skiing. Many tools like this exist for national park permits or campground reservations (anybody who’s ever tried to camp in CA knows this pain), but I couldn’t find anything built yet for these new systems.

Ikon’s website is a pretty heavy SPA, which ruled out scraping (not because it was impossible but because it was annoying). I’m honestly not a huge fan of scraping data from HTML anyway, it feels clunky and brittle. By examining the API requests made when walking through the form for creating a reservation on their website, I pieced together a fairly simple process of a few API calls to get resort information and reservation data for each location.

Attempting to curl these few API calls resulting in a 403, which was to be expected. I just needed to figure out how to properly auth and store however they keep auth state.

This step took me embarrassingly long to figure out, but in my defense I’m much more used to app-based authentication where you have persistent storage, not web-based auth surrounding cookies. I even sniffed the traffic from the Ikon app to see if it handled auth differently, but it just loads a web viewer and runs everything through that, the same as a browser. Alterra (Ikon’s parent company) also wraps all their domains in an ingress/egress service call Incapsula (more on this guy later) so a lot of the tokens and cookies are very obfuscated and confusing.

An example of what an Ikon API request / response looks like. This is retrieving the reservation data for resort with an ID of 14, or Jackson Hole.

An example of what an Ikon API request / response looks like. This is retrieving the reservation data for resort with an ID of 14, or Jackson Hole.

After working around the problem that Incapsula deliberately returns a malformed cookie (scripts and scrapes break on it, browsers discard it), I realized that I had to match the set of auth cookies set after a login POST with credentials with the matching CSRF token given to the browser on first page-load. The login auth also expired every hour or so and needed to be refreshed. The auth cookies were easy to get - the token was more annoying. At the time, I didn’t realize it came in a header on first page-load, I thought it was generated client-side in the obfuscated JS.

My solution to this was to use headless Chrome to load and scrape the login page, send a fake login request by “clicking” the login button (since just using the API call wouldn’t get me the CSRF token included in the request by the JS), then yank the token out of the headers sent to the server. Once I had the token, then I could make the “real” auth request using the existing cookies from the first page-load and the CSRF token pulled from the dummy request.

The cookies returned could then be used with any API request to successfully prove authentication. When talking through some later problems with a friend (with much more web security experience than me) he kindly pointed out that the CSRF token has to be generated server-side - how else would the server know how to verify it. Sure enough, the token came in a separate header on page load, rendering my whole headless-chrome-dummy-request solution woefully overengineered.

After I had API access, it was simple enough to set up a basic CLI to choose a resort, get the reservation dates from that resort, and see which were open and which were full. I translated the CLI to a basic Handlebars-rendered HTML site that a user could walk through with their preferred resort and date. If reservations were full for the date and location chosen, I would save the user’s email, resort, and chosen date in a local database (read: appending lines to a CSV text file). That completed the ability to save reservation notifications.

After selecting a resort, the user could choose the date they wanted to monitor for notifications and save their email alongside it

After selecting a resort, the user could choose the date they wanted to monitor for notifications and save their email alongside it

To monitor reservations, I set up a separate script that ran on a 10-minute cron job. It reused most of the same code as the reservation-saving process, only this time if it found an open date, it would remove that line from the local text-file storage and email the address within the entry saying it was open. Any reservation dates still full it would ignore.

I ran this system for about a month with just myself using it and it worked flawlessly. Since I was running on a 10-minute cycle and not always at my computer, sometimes I would get a notification email and by the time I checked Ikon’s website, it was full again. Simple enough just to add the notification again using the web interface.

After I had proven that it works (aka I got all the reservation dates that I wanted) I gave it to a couple of friends who had complained about not being able to get reservations. When they also had success with it, I posted it on the /r/skiing subreddit and got a tiny bit of traction.

I used a lightweight analytics client in the page JS which showed me # of visitors to each individual page, along with some small geographic and device demographics. Before the shutdown, I had over 100 visits to the final “notification saved” page, which meant over 100 reservation notifications were saved. This doesn’t mean there were over 100 unique users (almost certainly not), but it showed me it was getting a good amount of use.

Alas, the fateful day arrived when I was doing some further local development and realized my headless chrome instance could no longer find the login button on the login page. After 30 minutes of confused debugging, I tried to open up account.ikonpass.com in my browser to be met with a nice big error page.

The error page shown when trying to load the Ikon login page. The error code varied between 13 and 16.

The error page shown when trying to load the Ikon login page. The error code varied between 13 and 16.

It appeared that Alterra blocked the public IP my server was running behind from accessing any of its domains, not just the Ikon one. No problem, I thought, I’ll just spin it up on a real cloud instance like I should have in the first place, get a new IP, and keep rocking.

I instantiated an EC2 instance, cloned my repo, setup up the env variables and installed all the dependencies, and started up the app. Boom - 503, couldn’t load the login page.

Over the next couple of days, I tried multiple solutions: different cloud providers, switching from headless chrome to a simpler HTTP request (since I didn’t need the headless to begin with, remember?), mocking everything I could think of in the HTTP request (User-Agent, Referrer, etc.) to “act” like a browser even though I wasn’t. Nothing worked. Every time I tried with a new IP address I could get maybe one or two successful requests, but after 5 or 10 minutes that IP would be blocked with the Ikon error screen.

My only explanation is that they realized I and/or others had figured out how to access their API and had upped the level of security that their authorization / authentication wrapper, Incapsula, was using. I still don’t know how they differentiate between scripts and browsers - maybe loading a small piece of test JS, or mixing with cookies in a way the browsers and scripts respond differently.

In any event, Ikon Reservation Notifier is dead for at least the 2020-2021 ski season. I would not be surprised if certain resorts carry the reservation system forward (looking at you Jackson), so there might be potential for reviving it next year. If Ikon keeps their security the same, however, I’m going to need some help.

Who knows, maybe I’ll switch back to an Epic pass next year and see if I can carve up their system too.

'