diff --git a/CHANGELOG.md b/CHANGELOG.md index 769e8ac..2b502e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ ### New Features - Add ability to download products if they aren't yet installed +- Show a more helpful message instead of generic JSON error when Frontier API couldn't verify game ownership. + + This error happens intermittently with Steam licenses. ## [0.9.0] - 2023-09-12 diff --git a/src/MinEdLauncher/Api.fs b/src/MinEdLauncher/Api.fs index 4c93e1f..4552d0e 100644 --- a/src/MinEdLauncher/Api.fs +++ b/src/MinEdLauncher/Api.fs @@ -1,6 +1,7 @@ module MinEdLauncher.Api open System +open System.IO open System.IO.Compression open System.Net open System.Net.Http @@ -32,6 +33,7 @@ type AuthResult = | LinkAvailable of Uri | Denied of string | Failed of string +| CouldntConfirmOwnership let private buildUri (host: Uri) (path: string) queryParams = let builder = UriBuilder(host) @@ -169,6 +171,13 @@ let authenticate (runningTime: unit -> double) (token: AuthToken) platform machi match info with | Error m -> return Failed m | Ok (path, query, parseMachineToken) -> + let isHtml (stream: Stream) = + let htmlHeader = " ignore + stream.Seek(0, SeekOrigin.Begin) |> ignore + Encoding.UTF8.GetString(buffer) = htmlHeader + use request = new HttpRequestMessage() request.RequestUri <- buildUri httpClient.BaseAddress path query match platform with @@ -178,54 +187,60 @@ let authenticate (runningTime: unit -> double) (token: AuthToken) platform machi use! response = httpClient.SendAsync(request) use! content = response.Content.ReadAsStreamAsync() - let content = content |> Json.parseStream >>= Json.rootElement - let mapResult f = function - | Ok value -> f value - | Error msg -> Failed msg - let parseError content = - let errorValue = content >>= Json.parseEitherProp "error_enum" "errorCode" >>= Json.toString |> Result.defaultValue "Unknown" - let errorMessage = content >>= Json.parseProp "message" >>= Json.toString |> Result.defaultValue "" - errorValue, errorMessage - return - match response.StatusCode with - | code when int code < 300 -> - let fdevAuthToken = content >>= Json.parseProp "authToken" >>= Json.toString - let machineToken = parseMachineToken content - let registeredName = content >>= Json.parseProp "registeredName" - >>= Json.toString - |> Result.defaultValue $"%s{platform.Name} User" - let errorValue = content >>= Json.parseEitherProp "error_enum" "errorCode" >>= Json.toString - let errorMessage = content >>= Json.parseProp "message" >>= Json.toString - - match fdevAuthToken, machineToken, errorValue, errorMessage with - | Error _, Error _, Ok value, Ok msg -> Failed $"%s{value} - %s{msg}" - | Ok fdevToken, Ok machineToken, _, _ -> - let session = { Token = fdevToken; PlatformToken = token; Name = registeredName; MachineToken = machineToken } - Authorized <| new Connection(httpClient, session, runningTime) - | Error msg, _, _, _ - | _, Error msg, _, _ -> Failed msg - | HttpStatusCode.Found -> - content >>= Json.parseProp "Location" - >>= Json.asUri - |> mapResult RegistrationRequired - | HttpStatusCode.TemporaryRedirect -> - content >>= Json.parseProp "Location" - >>= Json.asUri - |> mapResult LinkAvailable - | HttpStatusCode.BadRequest -> - let errValue, errMessage = content |> parseError - $"Bad Request: %s{errValue} - %s{errMessage}" |> Denied - | HttpStatusCode.Forbidden -> - let errValue, errMessage = content |> parseError - $"Forbidden: %s{errValue} - %s{errMessage}" |> Denied - | HttpStatusCode.Unauthorized -> - let errValue, errMessage = content |> parseError - $"Unauthorized: %s{errValue} - %s{errMessage}" |> Denied - | HttpStatusCode.ServiceUnavailable -> - Failed "Service unavailable" - | code -> - Failed $"%i{int code}: %s{response.ReasonPhrase}" + // API returns an HTML login page when it can't verify game ownership. Sometimes happens with Steam accounts + // API doesn't respond with Content-Type header so check the first few bytes + if isHtml content then + return CouldntConfirmOwnership + else + let content = content |> Json.parseStream >>= Json.rootElement + let mapResult f = function + | Ok value -> f value + | Error msg -> Failed msg + let parseError content = + let errorValue = content >>= Json.parseEitherProp "error_enum" "errorCode" >>= Json.toString |> Result.defaultValue "Unknown" + let errorMessage = content >>= Json.parseProp "message" >>= Json.toString |> Result.defaultValue "" + errorValue, errorMessage + + return + match response.StatusCode with + | code when int code < 300 -> + let fdevAuthToken = content >>= Json.parseProp "authToken" >>= Json.toString + let machineToken = parseMachineToken content + let registeredName = content >>= Json.parseProp "registeredName" + >>= Json.toString + |> Result.defaultValue $"%s{platform.Name} User" + let errorValue = content >>= Json.parseEitherProp "error_enum" "errorCode" >>= Json.toString + let errorMessage = content >>= Json.parseProp "message" >>= Json.toString + + match fdevAuthToken, machineToken, errorValue, errorMessage with + | Error _, Error _, Ok value, Ok msg -> Failed $"%s{value} - %s{msg}" + | Ok fdevToken, Ok machineToken, _, _ -> + let session = { Token = fdevToken; PlatformToken = token; Name = registeredName; MachineToken = machineToken } + Authorized <| new Connection(httpClient, session, runningTime) + | Error msg, _, _, _ + | _, Error msg, _, _ -> Failed msg + | HttpStatusCode.Found -> + content >>= Json.parseProp "Location" + >>= Json.asUri + |> mapResult RegistrationRequired + | HttpStatusCode.TemporaryRedirect -> + content >>= Json.parseProp "Location" + >>= Json.asUri + |> mapResult LinkAvailable + | HttpStatusCode.BadRequest -> + let errValue, errMessage = content |> parseError + $"Bad Request: %s{errValue} - %s{errMessage}" |> Denied + | HttpStatusCode.Forbidden -> + let errValue, errMessage = content |> parseError + $"Forbidden: %s{errValue} - %s{errMessage}" |> Denied + | HttpStatusCode.Unauthorized -> + let errValue, errMessage = content |> parseError + $"Unauthorized: %s{errValue} - %s{errMessage}" |> Denied + | HttpStatusCode.ServiceUnavailable -> + Failed "Service unavailable" + | code -> + Failed $"%i{int code}: %s{response.ReasonPhrase}" } let getAuthorizedProducts platform lang (connection: Connection) = task { diff --git a/src/MinEdLauncher/App.fs b/src/MinEdLauncher/App.fs index 755b014..3a22b2a 100644 --- a/src/MinEdLauncher/App.fs +++ b/src/MinEdLauncher/App.fs @@ -14,6 +14,7 @@ open FsToolkit.ErrorHandling type LoginError = | ActionRequired of string +| CouldntConfirmOwnership of Platform | Failure of string let login launcherVersion runningTime httpClient machineId (platform: Platform) lang = let authenticate disposable = function @@ -27,7 +28,9 @@ let login launcherVersion runningTime httpClient machineId (platform: Platform) | Api.RegistrationRequired uri -> return ActionRequired <| $"Registration is required at %A{uri}" |> Error | Api.LinkAvailable uri -> return ActionRequired <| $"Link available at %A{uri}" |> Error | Api.Denied msg -> return Failure msg |> Error - | Api.Failed msg -> return Failure msg |> Error } + | Api.Failed msg -> return Failure msg |> Error + | Api.CouldntConfirmOwnership -> return CouldntConfirmOwnership platform |> Error + } | Error msg -> Failure msg |> Error |> Task.fromResult match platform with @@ -247,6 +250,20 @@ module AppError = | AuthorizedProducts m -> $"Couldn't get available products: %s{m}" | Login (ActionRequired m) -> $"Unsupported login action required: %s{m}" | Login (Failure m) -> $"Couldn't login: %s{m}" + | Login (CouldntConfirmOwnership platform) -> + let possibleFixes = + [ + $"Ensure you've linked your {platform.Name} account to your Frontier account. https://user.frontierstore.net/user/info" + if platform = Steam then + "Restart Steam" + "Log out and log back in to Steam" + "Restart your computer" + "Wait a minute or two and retry" + "Wait longer" + ] + |> List.map (fun s -> " " + s) + |> String.join Environment.NewLine + $"Frontier was unable to verify that you own the game. This happens intermittently. Possible fixes include:{Environment.NewLine}{possibleFixes}" | NoSelectedProduct -> "No selected project" | InvalidProductState m -> $"Couldn't start selected product: %s{m}"