A week ago, I found a League of Legends app that allows a player to check all the skins that they have bought on their account (available here). (Edit: with a recent patch, the LoL client can now show all bought skins!) To use the app, I had to let the app know where LoL was installed, then it performed some magic and revealed all the skins on my account.
When I looked through the source to find how it was grabbing the relevant info, I found three endpoints that the app seemed to call. However, there were a few problems:
- These endpoints aren't documented in Riot Games' API docs. They don't even match the format of Riot's APIs.
- There's no API key for Riot's API, nor does the app connect to an external server. It fetches from localhost.
So, it must be getting the player data without Riot's official API.
Also, the endpoints were calling localhost on some variable port. This should mean the app is relying on some locally hosted server for data. Did the app start the server? Then why would the app make a HTTP request to its own server? If the app didn't start it, then what did? And why does the app need LoL to be open to work?
All of these requests are performed with a given port and password. These values are retrieved from the app's Redux store which has an initial empty state, so it must be set sometime later. The port and password are located at
state.app.lcu, and a look at the app's reducers show only one action type changes that value:
LCU_UP. That action type is used by the
up action creator, which in turn is called in a function called
onLockFile is called when the app is first launched and tries to grab a file in the League of Legends root directory called
lockfile. It then watches the file for changes, and calls
onLockFile everytime it does.
Lock files are files that are created when a script or program begins execution. Whenever an instance of the program is started, it first checks if a lock file exists which would indicate a separate instance was already running. File locking restricts access to a file or some data by only allowing one process to access it at one time, usually to prevent race conditions.
In League of Legends, a file called
lockfile is created whenever LoL is opened. While this prevents a player from opening multiple instances of LoL, the lockfile serves a second purpose. Its contents look somewhat like this:
LeagueClient:13268:63569:[random string of characters]:https
The app parses it as such:
Process name : Process ID : Port : Password : "https"
So on startup, LoL picks out some port and a password before loading everything up. A quick ping onto the port shows it is being used while LoL is open.
This means the endpoints found earlier are actually directed to a server setup by LoL. Looking at the requests that the app makes, I can replicate them and test them out myself.
And it works!
It appears that all it checks for is Basic Auth with username
riot and a random password.
But why does LoL have this open?
Chromium Embedded Framework
So far, this is what I think is happening:
- On load, LoL sets up a local server with a randomly selected port and password, and saves the port and password to a lockfile.
- The client grabs data from the local server using provided endpoints.
- The local server requests the data from Riot's servers, and send them back to the client.
- The client gets a webpage from the local server (or at least some form of HTML + JS) and renders it with CEF.
At this point, I'm rather surprised at the intricacy behind the new client. What first seemed like a standard application is actually a front-end for a whole stack. This also got me curious: with a local server avaiable for requests, I could make a LoL companion app to perform other useful tasks than merely displaying skins. What other requests lie hidden behind the client?
Edit: well now we know! Recently, Riot Games revealed some information about the hidden League Client API, which are used for communications between a C++ RTMP library and CEF. Although full official documentation isn't available, the 5th Riot Games API Challenge involved building a project that utilizes the League Client API. There are also third-party programs that scrape the API directly from the client to produce a rudimentary documentation.