Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/Signal/Registration/RegistrationCoordinatorImpl+Service.swift
1 views
//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import Foundation
import SignalServiceKit

extension RegistrationCoordinatorImpl {

    enum Service {

        enum SVR2AuthCheckResponse {
            case success(RegistrationServiceResponses.SVR2AuthCheckResponse)
            case networkError
            case genericError
        }

        static func makeSVR2AuthCheckRequest(
            e164: E164,
            candidateCredentials: [SVR2AuthCredential],
            signalService: OWSSignalServiceProtocol,
            logger: PrefixedLogger,
        ) async -> SVR2AuthCheckResponse {
            let request = RegistrationRequestFactory.svr2AuthCredentialCheckRequest(
                e164: e164,
                credentials: candidateCredentials,
                logger: logger,
            )
            return await makeRequest(
                { try await signalService.urlSessionForMainSignalService().performRequest(request) },
                handler: self.handleSVR2AuthCheckResponse(statusCode:retryAfterHeader:bodyData:logger:),
                fallbackError: .genericError,
                networkFailureError: .networkError,
                logger: logger,
            )
        }

        private static func handleSVR2AuthCheckResponse(
            statusCode: Int,
            retryAfterHeader: TimeInterval?,
            bodyData: Data?,
            logger: PrefixedLogger,
        ) -> SVR2AuthCheckResponse {
            let statusCode = RegistrationServiceResponses.SVR2AuthCheckResponseCodes(rawValue: statusCode)
            switch statusCode {
            case .success:
                guard let bodyData else {
                    Logger.warn("Got empty KBS auth check response")
                    return .genericError
                }
                guard let response = try? JSONDecoder().decode(RegistrationServiceResponses.SVR2AuthCheckResponse.self, from: bodyData) else {
                    Logger.warn("Unable to parse KBS auth check response from response")
                    return .genericError
                }

                return .success(response)
            case .malformedRequest, .invalidJSON:
                Logger.error("Malformed kbs auth check request")
                return .genericError
            case .none, .unexpectedError:
                return .genericError
            }
        }

        static func makeCreateAccountRequest(
            _ method: RegistrationRequestFactory.VerificationMethod,
            e164: E164,
            authPassword: String,
            accountAttributes: AccountAttributes,
            skipDeviceTransfer: Bool,
            apnRegistrationId: RegistrationRequestFactory.ApnRegistrationId?,
            prekeyBundles: RegistrationPreKeyUploadBundles,
            signalService: OWSSignalServiceProtocol,
            logger: PrefixedLogger,
        ) async -> AccountResponse {
            let request = RegistrationRequestFactory.createAccountRequest(
                verificationMethod: method,
                e164: e164,
                authPassword: authPassword,
                accountAttributes: accountAttributes,
                skipDeviceTransfer: skipDeviceTransfer,
                apnRegistrationId: apnRegistrationId,
                prekeyBundles: prekeyBundles,
                logger: logger,
            )
            return await makeRequest(
                { try await signalService.urlSessionForMainSignalService().performRequest(request) },
                handler: {
                    self.handleCreateAccountResponse(
                        authPassword: authPassword,
                        statusCode: $0,
                        retryAfterHeader: $1,
                        bodyData: $2,
                        logger: $3,
                    )
                },
                fallbackError: .genericError,
                networkFailureError: .networkError,
                logger: logger,
            )
        }

        private static func handleCreateAccountResponse(
            authPassword: String,
            statusCode: Int,
            retryAfterHeader: TimeInterval?,
            bodyData: Data?,
            logger: PrefixedLogger,
        ) -> AccountResponse {
            let statusCode = RegistrationServiceResponses.AccountCreationResponseCodes(rawValue: statusCode)
            switch statusCode {
            case .success:
                guard let bodyData else {
                    Logger.warn("Got empty create account response")
                    return .genericError
                }
                guard let response = try? JSONDecoder().decode(RegistrationServiceResponses.AccountIdentityResponse.self, from: bodyData) else {
                    Logger.warn("Unable to parse Account identity from response")
                    return .genericError
                }
                return .success(AccountIdentity(
                    aci: response.aci,
                    pni: response.pni,
                    e164: response.e164,
                    hasPreviouslyUsedSVR: response.hasPreviouslyUsedSVR,
                    authPassword: authPassword,
                ))

            case .deviceTransferPossible:
                return .deviceTransferPossible

            case .regRecoveryPasswordRejected:
                Logger.warn("Reg recovery password rejected when creating account.")
                return .rejectedVerificationMethod

            case .reglockFailed:
                guard let bodyData else {
                    Logger.warn("Got empty create account response")
                    return .genericError
                }
                guard
                    let response = try? JSONDecoder().decode(
                        RegistrationServiceResponses.RegistrationLockFailureResponse.self,
                        from: bodyData,
                    )
                else {
                    Logger.warn("Unable to parse ReglockFailure from response")
                    return .genericError
                }
                return .reglockFailure(response)

            case .retry:
                return .retryAfter(retryAfterHeader)

            case .unauthorized:
                Logger.warn("Got unauthorized response for create account")
                return .rejectedVerificationMethod

            case .invalidArgument:
                Logger.warn("Got invalid argument response for create account")
                return .genericError

            case .malformedRequest:
                Logger.warn("Got malformed request response for create account")
                return .genericError

            case .none, .unexpectedError:
                return .genericError
            }
        }

        static func makeChangeNumberRequest(
            _ method: RegistrationRequestFactory.VerificationMethod,
            e164: E164,
            reglockToken: String?,
            authPassword: String,
            pniChangeNumberParameters: PniDistribution.Parameters,
            networkManager: any NetworkManagerProtocol,
            logger: PrefixedLogger,
        ) async -> AccountResponse {
            let request = RegistrationRequestFactory.changeNumberRequest(
                verificationMethod: method,
                e164: e164,
                reglockToken: reglockToken,
                pniChangeNumberParameters: pniChangeNumberParameters,
                logger: logger,
            )
            return await makeRequest(
                { try await networkManager.asyncRequest(request) },
                handler: {
                    return self.handleChangeNumberResponse(authPassword: authPassword, statusCode: $0, retryAfterHeader: $1, bodyData: $2, logger: $3)
                },
                fallbackError: .genericError,
                networkFailureError: .networkError,
                logger: logger,
            )
        }

        private static func handleChangeNumberResponse(
            authPassword: String,
            statusCode: Int,
            retryAfterHeader: TimeInterval?,
            bodyData: Data?,
            logger: PrefixedLogger,
        ) -> AccountResponse {
            let statusCode = RegistrationServiceResponses.ChangeNumberResponseCodes(rawValue: statusCode)
            switch statusCode {
            case .success:
                guard let bodyData else {
                    Logger.warn("Got empty create account response")
                    return .genericError
                }
                guard let response = try? JSONDecoder().decode(RegistrationServiceResponses.AccountIdentityResponse.self, from: bodyData) else {
                    Logger.warn("Unable to parse Account identity from response")
                    return .genericError
                }
                return .success(AccountIdentity(
                    aci: response.aci,
                    pni: response.pni,
                    e164: response.e164,
                    hasPreviouslyUsedSVR: response.hasPreviouslyUsedSVR,
                    authPassword: authPassword,
                ))

            case .reglockFailed:
                guard let bodyData else {
                    Logger.warn("Got empty create account response")
                    return .genericError
                }
                guard
                    let response = try? JSONDecoder().decode(
                        RegistrationServiceResponses.RegistrationLockFailureResponse.self,
                        from: bodyData,
                    )
                else {
                    Logger.warn("Unable to parse ReglockFailure from response")
                    return .genericError
                }
                return .reglockFailure(response)

            case .retry:
                return .retryAfter(retryAfterHeader)

            case .unauthorized, .regRecoveryPasswordRejected:
                return .rejectedVerificationMethod

            case .malformedRequest:
                Logger.error("Got malformed request for change number")
                return .genericError

            case .invalidArgument:
                Logger.error("Got invalid argument for change number")
                return .genericError

            case .mismatchedDevicesToNotify, .mismatchedDevicesToNotifyRegistrationIds:
                // TODO[PNP]: What should be done about this category of error?
                Logger.error("Got mismatched device list information for change number")
                return .genericError

            case .none, .unexpectedError:
                return .genericError
            }
        }

        static func makeEnableReglockRequest(
            reglockToken: String,
            auth: ChatServiceAuth,
            networkManager: any NetworkManagerProtocol,
            logger: PrefixedLogger,
        ) async throws {
            try await Retry.performWithBackoff(
                maxAttempts: RegistrationCoordinatorImpl.Constants.networkErrorRetries + 1,
                isRetryable: { $0.isNetworkFailureOrTimeout },
            ) {
                var request = OWSRequestFactory.enableRegistrationLockV2Request(token: reglockToken, logger: logger)
                request.auth = .identified(auth)
                _ = try await networkManager.asyncRequest(request)
            }
        }

        static func makeUpdateAccountAttributesRequest(
            _ attributes: AccountAttributes,
            auth: ChatServiceAuth,
            networkManager: any NetworkManagerProtocol,
            logger: PrefixedLogger,
        ) async throws {
            try await Retry.performWithBackoff(
                maxAttempts: RegistrationCoordinatorImpl.Constants.networkErrorRetries + 1,
                isRetryable: { $0.isNetworkFailureOrTimeout },
            ) {
                let request = RegistrationRequestFactory.updatePrimaryDeviceAccountAttributesRequest(
                    attributes,
                    auth: auth,
                    logger: logger,
                )
                let response = try await networkManager.asyncRequest(request)
                guard response.responseStatusCode >= 200, response.responseStatusCode < 300 else {
                    // Errors are undifferentiated; the only actual error we can get is an unauthenticated
                    // one and there isn't any way to handle that as different from a, say server 500.
                    throw OWSAssertionError("Got unexpected response code from update attributes request: \(response.responseStatusCode).")
                }
            }
        }

        enum WhoAmIResponse {
            case success(WhoAmIRequestFactory.Responses.WhoAmI)
            case networkError
            case genericError
        }

        static func makeWhoAmIRequest(
            auth: ChatServiceAuth,
            networkManager: any NetworkManagerProtocol,
        ) async -> WhoAmIResponse {
            do {
                return try await Retry.performWithBackoff(
                    maxAttempts: RegistrationCoordinatorImpl.Constants.networkErrorRetries + 1,
                    isRetryable: { $0.isNetworkFailureOrTimeout },
                ) {
                    let request = WhoAmIRequestFactory.whoAmIRequest(auth: auth)
                    let response = try await networkManager.asyncRequest(request)
                    guard response.responseStatusCode >= 200, response.responseStatusCode < 300 else {
                        return .genericError
                    }
                    guard let bodyData = response.responseBodyData else {
                        Logger.error("Got empty whoami response")
                        return .genericError
                    }
                    guard let response = try? JSONDecoder().decode(WhoAmIRequestFactory.Responses.WhoAmI.self, from: bodyData) else {
                        Logger.error("Unable to parse whoami response from response")
                        return .genericError
                    }
                    return .success(response)
                }
            } catch {
                return error.isNetworkFailureOrTimeout ? .networkError : .genericError
            }
        }

        private static func makeRequest<ResponseType>(
            _ makeRequest: () async throws -> HTTPResponse,
            handler: (_ statusCode: Int, _ retryAfterHeader: TimeInterval?, _ bodyData: Data?, _ logger: PrefixedLogger) -> ResponseType,
            fallbackError: ResponseType,
            networkFailureError: ResponseType,
            logger: PrefixedLogger,
        ) async -> ResponseType {
            do {
                let response = try await makeRequest()
                return handler(
                    response.responseStatusCode,
                    response.headers.retryAfterTimeInterval,
                    response.responseBodyData,
                    logger,
                )
            } catch where error.isNetworkFailureOrTimeout {
                return networkFailureError
            } catch let error as OWSHTTPError {
                return handler(
                    error.responseStatusCode,
                    error.responseHeaders?.retryAfterTimeInterval,
                    error.httpResponseData,
                    logger,
                )
            } catch {
                return fallbackError
            }
        }
    }
}