Skip to content

Instantly share code, notes, and snippets.

@blha303
Last active April 20, 2021 12:10
Show Gist options
  • Save blha303/101e0db0bf63ea07b1f55862947c9065 to your computer and use it in GitHub Desktop.
Save blha303/101e0db0bf63ea07b1f55862947c9065 to your computer and use it in GitHub Desktop.
Life Is Strange stats server investigation

Contents

In the below documentation, all requests return { "d": something } (except the ones that don't), so everything outside of something will be omitted.

API url: https://lis.os.eidos.com/

Necessary headers for each request (may vary, untested):

  • Accept: application/json, otherwise the API returns XML (obviously if you're fine with that you can omit this)
  • OS-AuthProvider: 6, unsure of the purpose of this
  • OS-AuthTicketData: [string], begins with CAEQ on my copy of the game, CONFIRMED static between copies of the game (at least PC linux)
  • OS-AuthTicketSize: 44, the length of the above string
  • OS-UID: [string], steam user ID, or some other form of identification in standalone versions probably
  • OS-Platform: steam, the user's platform, needs more testing to get options
  • OS-System: linux, ditto

Optional headers:

  • DataServiceVersion: 2.0
  • MaxDataServiceVersion: 2.0
  • OS-Age: 0
  • OS-Build: localBuild
  • OS-GTime: 84
  • OS-Locale: en,XX,XX,
  • OS-MAC: xx-xx-xx-xx-xx-xx
  • OS-OSVersion: 5.0.22.82369, unsure what this is, it's not kernel version
  • OS-PID: LiS-v1.0.0.397609
  • OS-Progress: 0.00000000000000000, lol
  • OS-SID: [uuid]
  • OS-STime: 5713
  • OS-XYZ: 135.95315551757812500,-130.51196289062500000,97.04180908203125000
  • OS-Zone: E0_Menu, the game seems to send ingame location with every request

On game start

The game sends several requests in the opening videos.

  1. /game/os_Ping

    • Likely confirming that the service is up
    • Returns {"os_Ping": 6739}, different number each time
  2. /game/os_GetServiceInfo

  3. /game/os_Ping again

  4. /game/SEM_Login

    • Probably for the Square Enix account login
    • Query params: s_type is 'UID' here; s_value is my steam user ID, a long string of digits
    • Returns { "SEM_Login": { "__metadata": { "type": "game.SEMSubmit" }, "b_confirmed": true, "s_SEMID": "string of numbers", "s_email": "email address", "s_facebookID": "probably facebook user identifier", "s_longTermToken": "string of numbers and letters" } }
  5. It sends the above request twice, unclear why

  6. /game/CreateUserProfile

    • Square Enix login again? Probably making sure that there's an account for the given steam ID for stats collection even if it isn't linked to a SEM account
    • Query params: s_uid is my steam user ID
    • Returns { "CreateUserProfile": true } and a header OS-AuthResponse containing the new OS-AuthTicketData to be used for subsequent requests
  7. /game/GetTodaysInfocast

    • Returns a list of messages to be scrolled at the bottom of the main menu
    • Idling on the menu calls this periodically
    • Query params: b_digital, a boolean, probably whether it's digital or a physical disc; i16_episode, the episode number of the current save game; s_locale, language code e.g 'en'
    • Example response: https://gist.github.com/blha303/d00b1c34728cc21101b7fc198bbf3371
  8. /game/SetUserProfileFriends

    • Sends list of UIDs for user's friends list
    • POST requests, ["UID", ...]
    • Returns {"SetUserProfileFriends": 0 }, friends service probably disabled
  9. /game/GetSeasonPassOfferIdList

Occasionally

  1. /game/AddMetrics

On opening the choices screen (from the main menu or after each episode)

  1. /game/CommunityFactsGetEpisode

  2. /game/GetFriendsProfileStats

    • Apparently would return the same as /game/CommunityFactsGetEpisode with additional info on friend stats, but currently returns nothing
    • Query params: i16_episode, the given episode number; s_uid, the steam/other user id
    • Currently returns {"results": [] }

On finishing an episode

Making me listen to Obstacles again >_<

  1. /game/UpdateUserProfileGameSpecific

Other endpoints: https://gist.github.com/blha303/101e0db0bf63ea07b1f55862947c9065#file-zgenerateddocs-md

Contents

Hello! Today I thought I'd take on the task of reverse engineering the stats screen in Life Is Strange so I could easily request and display the data without needing to start the game. I'm documenting it here for future use, since nobody else seems to have tried this yet.

First, you'll need a copy of Life Is Strange on Steam. Second, you'll need to have it working on Linux. The Linux Steam version of LIS has a startup script that sets the ssl certificate locations, you'll need to modify that to be able to MITM the server connections. The game makes requests to https://lis.os.eidos.com, but doesn't verify the certificate in the game. (requests to Feral Interactive, the company that ported LIS to OSX and Linux, do seem to be verified, but they don't matter much)

    #HAS_CURL=$( command -v curl-config )
    #if [ -n "$...
    #    SSL_CERT_FILE=...
    #else
    #    if ...
    #        SSL_CERT...
    #    elif
    #        SSL_CERT...
    #    elif
    #        SSL_CERT...
    #    fi
    #fi
    export SSL_CERT_FILE="/home/user/Downloads/mitmproxy-ca-cert.pem"
  • Set the second computer as the default gateway on the first computer by running sudo ip route add default via <ip>. Get the ip address of the second computer by running ip addr | grep inet.
  • Close other programs (except Terminal and Steam) to minimize noise
  • Run mitmdump -w output.mitmdump in Terminal, then start Life Is Strange through Steam
  • I had to alt-tab out and into the game to get it to work, you may need to do the same
  • You should see requests to /game/os_Ping start showing up. mitmdump will record all game communication. I loaded up the main menu, clicked on Choices, waited for the stats to show up, then closed the game and killed mitmdump with ctrl-c
  • Run mitmproxy --host -r output.mitmdump to view the requests.

If only the stats server communications weren't encrypted, would have been a lot easier.

I'm not sure how initial authentication works, the game sends an OS-AuthTicketData header with a string of characters, maybe each copy of the game has a unique auth ticket confirmed to be static at least for PC Linux releases. It also requests user location data via an IP info lookup (/game/os_GetServiceInfo) and sends your friends list in the form of a list of user IDs (/game/SetUserProfileFriends), presumably for the friend stats response (/game/GetFriendsProfileStats) but this currently returns nothing. The main thing I'm interested in is /game/CommunityFactsGetEpisode, which takes a query param of i16_episode={episode number} and returns json:

"d" : {
    "results" : [
        {
            "__metadata" : {
                "uri" : "https://lis.os.eidos.com/game/communityfactreturns('BirdDead')",
                "type" : "game.communityfactreturn"
            },
            "f_rate" : "0.638969873663751",
            "i64_totalCount" : 2058,
            "i64_trueCount" : 1315,
            "i16_ep" : 1,
            "s_factID" : "BirdDead"
        },
        {
            "__metadata" : {
                "uri" : "https://lis.os.eidos.com/game/communityfactreturns('BirdSaved')",
                "type" : "game.communityfactreturn"
            },
            "f_rate" : "0.361030126336249",
            "i64_totalCount" : 2058,
            "i64_trueCount" : 743,
            "i16_ep" : 1,
            "s_factID" : "BirdSaved"
        }, ...

Which is exactly what I was looking for ^_^

Contents

These docs aren't entirely accurate. Some things are in the wrong AuthPolicy category, some things say they return nothing when they do return something. But then, since when are any internal docs accurate, right? :P

Policy.OS

  • /game/CalculateCommunityProfileStats
    • Returns Edm.Int32
  • /game/CalculateFriendsProfileStats
    • Returns Edm.Int32
  • /game/UpdateAvailableEpisodeOfferId
    • Query params: s_region, Edm.String; i32_episode, Edm.Int32
    • Returns Edm.String
  • /game/os_GetMonitoring
    • Returns game.ServerStatus
  • /game/os_GetOptions
    • Returns game.Options
  • /game/os_SetOptions
    • Query params: name, Edm.String; value, Edm.String
    • Returns game.Options
  • /game/ClearCache
    • Query params: s_Name, Edm.String
    • Returns Edm.String
  • /game/os_NotificationExpired
    • Query params: s_id, Edm.String; i32_type, Edm.Int32; s_message, Edm.String; s_platform, Edm.String; s_sender, Edm.String
    • Returns nothing
  • /game/DeleteUserProfile
    • Query params: s_uid, Edm.String
    • Returns Edm.Boolean
  • /game/GetUserProfileGlobal
    • Query params: s_uid, Edm.String
    • Returns game.userprofile
  • /game/os_GetUserProfileGlobal
    • Query params: s_uid, Edm.String
    • Returns game.userprofile
  • /game/UpdateUserProfileGlobal
    • Query params: s_uid, Edm.String
    • POST request
    • Returns Edm.Int32
  • /game/os_UpdateUserProfileGlobal
    • Query params: s_uid, Edm.String
    • POST request
    • Returns Edm.Int32
  • /game/GetUserProfilePlatform
    • Query params: s_uid, Edm.String
    • Returns game.userprofile
  • /game/os_GetUserProfilePlatform
    • Query params: s_uid, Edm.String
    • Returns game.userprofile
  • /game/UpdateUserProfilePlatform
    • Query params: s_uid, Edm.String
    • POST request
    • Returns Edm.Int32
  • /game/os_UpdateUserProfilePlatform
    • Query params: s_uid, Edm.String
    • POST request
    • Returns Edm.Int32
  • /game/GetUserProfileFranchise
    • Query params: s_uid, Edm.String
    • Returns game.userprofile
  • /game/os_GetUserProfileFranchise
    • Query params: s_uid, Edm.String
    • Returns game.userprofile
  • /game/UpdateUserProfileFranchise
    • Query params: s_uid, Edm.String
    • POST request
    • Returns Edm.Int32
  • /game/os_UpdateUserProfileFranchise
    • Query params: s_uid, Edm.String
    • POST request
    • Returns Edm.Int32
  • /game/GetUserProfileGameGeneric
    • Query params: s_uid, Edm.String
    • Returns game.userprofile
  • /game/os_GetUserProfileGameGeneric
    • Query params: s_uid, Edm.String
    • Returns game.userprofile
  • /game/UpdateUserProfileGameGeneric
    • Query params: s_uid, Edm.String
    • POST request
    • Returns Edm.Int32
  • /game/os_UpdateUserProfileGameGeneric
    • Query params: s_uid, Edm.String
    • POST request
    • Returns Edm.Int32
  • /game/ValidateSEM
    • Returns Edm.Boolean
  • /game/os_BackupUserProfiles
    • Returns Edm.Int32
  • /game/os_TestCache
    • Query params: s_cacheName, Edm.String
    • Returns Edm.String

Policy.GAMESEE

  • /game/GetSpecialFlag
    • Query params: i32_episode, Edm.Int32
    • Returns game.offlinespecialfeature
  • /game/os_SendNotification
    • Query params: s_sender, Edm.String; i32_type, Edm.Int32; s_platform, Edm.String; i64_lifespan, Edm.Int64
    • POST request
    • Returns nothing
  • /game/os_TranslatePlatformIds
    • POST request
    • Returns Collection(game.PlatformIDInfo)
  • /game/osGetConsumablesInfo
    • Query params: s_uid, Edm.String
    • Returns Edm.String
  • /game/Steam_GetUserInfo
    • Query params: s_uid, Edm.String
    • Returns game.ossteamresponse
  • /game/os_GetLastMetrics
    • Query params: i32_n, Edm.Int32
    • Returns Collection(game.os_metric)
  • /game/os_GetMetric
    • Query params: s_uuid, Edm.String
    • Returns game.os_metric
  • /game/GetUserProfileGameSpecific
    • Query params: s_uid, Edm.String
    • Returns game.userprofile
  • /game/os_GetUserProfileGameSpecific
    • Query params: s_uid, Edm.String
    • Returns game.userprofile
  • /game/GetUserProfileCountry
    • Query params: s_uid, Edm.String
    • Returns Edm.String
  • /game/os_GetUserProfileCountry
    • Query params: s_uid, Edm.String
    • Returns Edm.String
  • /game/UpdateSEMinfo
    • Query params: s_semid, Edm.String
    • POST request
    • Returns Edm.Boolean
  • /game/DeleteSEMinfo
    • Query params: s_semid, Edm.String
    • Returns Edm.Boolean
  • /game/LinkToSEM
    • Query params: s_semid, Edm.String; s_authSystem, Edm.String; s_authId, Edm.String
    • Returns Edm.Boolean
  • /game/UnlinkFromSEM
    • Query params: s_semid, Edm.String; s_authSystem, Edm.String; s_authId, Edm.String
    • Returns Edm.Boolean

Policy.SEE

  • /game/UpdateInfocastMessages
    • Query params: _id, Edm.String; s_Type, Edm.String; s_DateBegin, Edm.String; s_DateEnd, Edm.String; i16_episode, Edm.Int32
    • POST request
    • Returns Edm.Boolean
  • /game/DeleteInfocastMessages
    • Query params: _id, Edm.String
    • Returns Edm.Boolean
  • /game/GetInfocastMessages
    • Returns Collection(game.infocast)
  • /game/EnableSpecialFlag
    • Query params: i32_episode, Edm.Int32; b_enable, Edm.Boolean
    • POST request
    • Returns Edm.Boolean
  • /game/AddInfocastMessages
    • Query params: s_Type, Edm.String; s_DateBegin, Edm.String; s_DateEnd, Edm.String; i16_episode, Edm.Int32
    • POST request
    • Returns Edm.String
  • /game/os_CreateSegment
    • Query params: s_segment, Edm.String
    • POST request
    • Returns Edm.String
  • /game/os_GetSegmentMetadata
    • Query params: s_segment, Edm.String
    • Returns Edm.String
  • /game/os_GetSegmentList
    • Returns Edm.String
  • /game/os_UpdateSegmentMetadata
    • Query params: s_segment, Edm.String
    • POST request
    • Returns Edm.String
  • /game/os_DeleteSegment
    • Query params: s_segment, Edm.String
    • Returns Edm.String
  • /game/os_AddPlayersToSegment
    • Query params: s_segment, Edm.String
    • POST request
    • Returns Edm.String
  • /game/os_GetPlayersFromSegment
    • Query params: s_segment, Edm.String
    • Returns Edm.String
  • /game/os_GetSegmentsFromPlayer
    • Query params: s_uid, Edm.String
    • Returns Edm.String
  • /game/os_RemovePlayersFromSegment
    • Query params: s_segment, Edm.String
    • POST request
    • Returns Edm.String
  • /game/os_AddUsageLog
    • Query params: s_segment, Edm.String; s_log, Edm.String
    • Returns Edm.String
  • /game/os_GetUsageLogs
    • Query params: s_segment, Edm.String
    • Returns Edm.String
  • /game/os_CreateABTest
    • Query params: s_abtest, Edm.String
    • POST request
    • Returns Edm.String
  • /game/os_GetABTestMetadata
    • Query params: s_abtest, Edm.String
    • Returns Edm.String
  • /game/os_GetABTestList
    • Returns Edm.String
  • /game/os_UpdateABTestMetadata
    • Query params: s_abtest, Edm.String
    • POST request
    • Returns Edm.String
  • /game/os_DeleteABTest
    • Query params: s_abtest, Edm.String
    • Returns Edm.String
  • /game/os_AddUsageLogToABTest
    • Query params: s_abtest, Edm.String; s_log, Edm.String
    • Returns Edm.String
  • /game/os_GetUsageLogsFromABTest
    • Query params: s_abtest, Edm.String
    • Returns Edm.String
  • /game/os_AddSegmentToABTest
    • Query params: s_abtest, Edm.String; s_segment, Edm.String
    • Returns Edm.String
  • /game/os_RemoveSegmentFromABTest
    • Query params: s_abtest, Edm.String; s_segment, Edm.String
    • Returns Edm.String
  • /game/os_SetDefaultABTestSegment
    • Query params: s_abtest, Edm.String; s_segment, Edm.String
    • Returns Edm.String
  • /game/os_SetDefaultABTestSegmentAsRandom
    • Query params: s_abtest, Edm.String
    • Returns Edm.String
  • /game/os_GetPlayerSegmentFromABTest
    • Query params: s_abtest, Edm.String; s_uid, Edm.String
    • Returns Edm.String
  • /game/os_CheckABTestConsistency
    • Query params: s_abtest, Edm.String
    • Returns Edm.String

Policy.ANY

  • /game/AddMetric
    • Returns nothing
  • /game/AddMetrics
    • POST request
    • Returns nothing

Policy.PUBLIC

  • /game/GetTodaysInfocast
    • Query params: s_locale, Edm.String; i16_episode, Edm.Int32
    • Returns Collection(game.infocast)
  • /game/os_GetStatus
    • Returns game.ClientInfo
  • /game/os_GetServiceInfo
    • Returns nothing
  • /game/os_GetChangeLog
    • Returns nothing
  • /game/os_GetChangeLogHTML
    • Returns nothing
  • /game/os_Ping
    • Returns Edm.Int64
  • /game/SEM_Login
    • Query params: s_type, Edm.String; s_value, Edm.String
    • Returns game.SEMSubmit
  • /game/SEM_SubmitEmail
    • Query params: s_type, Edm.String; s_value, Edm.String
    • Returns game.SEMSubmit

Policy.GAME

  • /game/UpdateUserProfileGameSpecific
    • Query params: s_uid, Edm.String
    • POST request
    • Returns Edm.Int32
  • /game/SetUserProfileFriends
    • Query params: s_uid, Edm.String
    • POST request
    • Returns Edm.Int32
  • /game/CommunityFactsGetEpisode
    • Query params: i16_episode, Edm.Int32
    • Returns Collection(game.communityfactreturn)
  • /game/CommunityFactsGetAll
    • Returns Collection(game.communityfactreturn)
  • /game/GetFriendsProfileStats
    • Query params: s_uid, Edm.String; i16_episode, Edm.Int32
    • Returns Collection(game.communityfactreturn)
  • /game/GetSeasonPassOfferIdList
    • Query params: s_region, Edm.String
    • Returns Collection(game.offerid)
  • /game/SetUserProfileGameSpecific
    • Query params: s_uid, Edm.String
    • POST request
    • Returns Edm.Int32
  • /game/os_GetPlayer
    • Query params: UID, Edm.String
    • Returns game.os_player
  • /game/os_GetGeoLocation
    • Returns game.GeoLocation
  • /game/SEM_DeleteUser
    • Query params: s_type, Edm.String; s_value, Edm.String
    • Returns game.Result
  • /game/SEM_UnlinkPlayer
    • Query params: s_type, Edm.String; s_value, Edm.String
    • Returns game.Result
  • /game/SEM_GetLinkedAccounts
    • Query params: s_type, Edm.String; s_value, Edm.String
    • Returns Collection(game.SEMLinkedAccount)
  • /game/FB_PostToWall
    • Query params: s_facebookID, Edm.String
    • POST request
    • Returns game.Result
  • /game/FB_PostImageToWall
    • Query params: s_facebookID, Edm.String; s_imageName, Edm.String; s_linkName, Edm.String; s_linkCaption, Edm.String; s_linkDescription, Edm.String
    • POST request
    • Returns game.Result
  • /game/osNewTransaction
    • Query params: s_uid, Edm.String; s_offerIds, Edm.String
    • Returns game.txninfo
  • /game/osCreateCustomTransaction
    • Query params: s_uid, Edm.String
    • POST request
    • Returns game.txninfo
  • /game/osFinalizeTransaction
    • Query params: s_uid, Edm.String; i64_txnId, Edm.Int64
    • Returns game.txninfo
  • /game/osCancelTransaction
    • Query params: s_uid, Edm.String; i64_txnId, Edm.Int64; i32_reason, Edm.Int32
    • Returns game.txninfo
  • /game/osPreTransferTransaction
    • Query params: s_uid, Edm.String; i64_txnId, Edm.Int64
    • POST request
    • Returns game.txninfo
  • /game/osGetIncompleteTransaction
    • Query params: s_uid, Edm.String
    • Returns Collection(game.txninfo)
  • /game/CreateUserProfile
    • Query params: s_uid, Edm.String
    • Returns Edm.Boolean
  • /game/os_CreateUserProfile
    • Query params: s_uid, Edm.String
    • Returns Edm.Int32
  • /game/os_SetUserProfileGameSpecific
    • Query params: s_uid, Edm.String
    • POST request
    • Returns Edm.Int32
  • /game/os_UpdateUserProfileGameSpecific
    • Query params: s_uid, Edm.String
    • POST request
    • Returns Edm.Int32
  • /game/SetUserProfileCountry
    • Query params: s_uid, Edm.String; s_country, Edm.String
    • Returns Edm.Boolean
  • /game/os_SetUserProfileCountry
    • Query params: s_uid, Edm.String; s_country, Edm.String
    • Returns Edm.Boolean
  • /game/PS4_GetFriendList
    • Query params: s_uid, Edm.String
    • Returns Edm.String
#!/usr/bin/env python3
from json import load
with open("/tmp/lisserviceinfo.json") as f: # from os_GetServiceInfo
d = load(f)["d"]
f = d["Metadata"]["Schemas"][0]["EntityContainers"][0]["FunctionImports"]
funcs = {}
for func in f:
if not func["AuthPolicy"] in funcs:
funcs[func["AuthPolicy"]] = []
funcs[func["AuthPolicy"]].append(func)
def format(func):
out = "* `/game/{Name}`\n".format(**func)
if "Parameters" in func:
func["params"] = ["`{Name}`, {Type}".format(**p) for p in func["Parameters"]]
out += " * Query params: " + "; ".join(func["params"]) + "\n"
if func["HttpMethod"] != "GET":
out += " * {HttpMethod} request\n".format(**func)
if "ReturnType" in func:
out += " * Returns {ReturnType}".format(**func)
else:
out += " * Returns nothing"
return out
print("These docs aren't entirely accurate. Some things are in the wrong AuthPolicy category, some things say they return nothing when they do return something. But then, since when are any internal docs accurate, right? :P\n")
for authpolicy, functions in funcs.items():
print("\n### {}".format(authpolicy))
for f in functions:
try:
print(format(f))
except:
print(f)
raise
@JackAshwell11
Copy link

JackAshwell11 commented Apr 20, 2021

Damm, this is interesting. Guessing if the AuthTicketData doesn't change, you could theoretically send requests whenever you want without the game open as long as the headers and endpoint is correct. Might have to test that later.

Well done for your hard work.

P.S. Do you know if this would be against the EULA? It could be counted as reverse engineering.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment