417 lines
17 KiB
Swift
417 lines
17 KiB
Swift
//
|
|
// 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,
|
|
schedulers: Schedulers
|
|
) -> Guarantee<SVR2AuthCheckResponse> {
|
|
let request = RegistrationRequestFactory.svr2AuthCredentialCheckRequest(
|
|
e164: e164,
|
|
credentials: candidateCredentials
|
|
)
|
|
return makeRequest(
|
|
request,
|
|
signalService: signalService,
|
|
schedulers: schedulers,
|
|
handler: self.handleSVR2AuthCheckResponse(statusCode:retryAfterHeader:bodyData:),
|
|
fallbackError: .genericError,
|
|
networkFailureError: .networkError
|
|
)
|
|
}
|
|
|
|
private static func handleSVR2AuthCheckResponse(
|
|
statusCode: Int,
|
|
retryAfterHeader: String?,
|
|
bodyData: Data?
|
|
) -> 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,
|
|
schedulers: Schedulers
|
|
) -> Guarantee<AccountResponse> {
|
|
let request = RegistrationRequestFactory.createAccountRequest(
|
|
verificationMethod: method,
|
|
e164: e164,
|
|
authPassword: authPassword,
|
|
accountAttributes: accountAttributes,
|
|
skipDeviceTransfer: skipDeviceTransfer,
|
|
apnRegistrationId: apnRegistrationId,
|
|
prekeyBundles: prekeyBundles
|
|
)
|
|
return makeRequest(
|
|
request,
|
|
signalService: signalService,
|
|
schedulers: schedulers,
|
|
handler: {
|
|
self.handleCreateAccountResponse(
|
|
authPassword: authPassword,
|
|
statusCode: $0,
|
|
retryAfterHeader: $1,
|
|
bodyData: $2
|
|
)
|
|
},
|
|
fallbackError: .genericError,
|
|
networkFailureError: .networkError
|
|
)
|
|
}
|
|
|
|
private static func handleCreateAccountResponse(
|
|
authPassword: String,
|
|
statusCode: Int,
|
|
retryAfterHeader: String?,
|
|
bodyData: Data?
|
|
) -> 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:
|
|
let retryAfter: TimeInterval
|
|
if
|
|
let retryAfterHeader,
|
|
let retryAfterTime = TimeInterval(retryAfterHeader)
|
|
{
|
|
retryAfter = retryAfterTime
|
|
} else {
|
|
Logger.warn("Missing retry-after header from server; falling back to default.")
|
|
retryAfter = Constants.defaultRetryTime
|
|
}
|
|
return .retryAfter(retryAfter)
|
|
|
|
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,
|
|
signalService: OWSSignalServiceProtocol,
|
|
schedulers: Schedulers
|
|
) -> Guarantee<AccountResponse> {
|
|
let request = RegistrationRequestFactory.changeNumberRequest(
|
|
verificationMethod: method,
|
|
e164: e164,
|
|
reglockToken: reglockToken,
|
|
pniChangeNumberParameters: pniChangeNumberParameters
|
|
)
|
|
return makeRequest(
|
|
request,
|
|
signalService: signalService,
|
|
schedulers: schedulers,
|
|
handler: {
|
|
return self.handleChangeNumberResponse(authPassword: authPassword, statusCode: $0, retryAfterHeader: $1, bodyData: $2)
|
|
},
|
|
fallbackError: .genericError,
|
|
networkFailureError: .networkError
|
|
)
|
|
}
|
|
|
|
private static func handleChangeNumberResponse(
|
|
authPassword: String,
|
|
statusCode: Int,
|
|
retryAfterHeader: String?,
|
|
bodyData: Data?
|
|
) -> 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:
|
|
let retryAfter: TimeInterval
|
|
if
|
|
let retryAfterHeader,
|
|
let retryAfterTime = TimeInterval(retryAfterHeader)
|
|
{
|
|
retryAfter = retryAfterTime
|
|
} else {
|
|
Logger.warn("Missing retry-after header from server; falling back to default.")
|
|
retryAfter = Constants.defaultRetryTime
|
|
}
|
|
return .retryAfter(retryAfter)
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
public static func makeEnableReglockRequest(
|
|
reglockToken: String,
|
|
auth: ChatServiceAuth,
|
|
signalService: OWSSignalServiceProtocol,
|
|
schedulers: Schedulers,
|
|
retriesLeft: Int = RegistrationCoordinatorImpl.Constants.networkErrorRetries
|
|
) -> Promise<Void> {
|
|
let request = OWSRequestFactory.enableRegistrationLockV2Request(token: reglockToken)
|
|
request.setAuth(auth)
|
|
return signalService.urlSessionForMainSignalService().promiseForTSRequest(request).asVoid()
|
|
.recover(on: schedulers.sync) { error in
|
|
if error.isNetworkFailureOrTimeout, retriesLeft > 0 {
|
|
return makeEnableReglockRequest(
|
|
reglockToken: reglockToken,
|
|
auth: auth,
|
|
signalService: signalService,
|
|
schedulers: schedulers,
|
|
retriesLeft: retriesLeft - 1
|
|
)
|
|
}
|
|
return .init(error: error)
|
|
}
|
|
}
|
|
|
|
/// Returns nil error if success.
|
|
public static func makeUpdateAccountAttributesRequest(
|
|
_ attributes: AccountAttributes,
|
|
auth: ChatServiceAuth,
|
|
signalService: OWSSignalServiceProtocol,
|
|
schedulers: Schedulers,
|
|
retriesLeft: Int = RegistrationCoordinatorImpl.Constants.networkErrorRetries
|
|
) -> Guarantee<Error?> {
|
|
let request = RegistrationRequestFactory.updatePrimaryDeviceAccountAttributesRequest(
|
|
attributes,
|
|
auth: auth
|
|
)
|
|
return signalService.urlSessionForMainSignalService().promiseForTSRequest(request)
|
|
.map(on: schedulers.sync) { response in
|
|
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.
|
|
return OWSAssertionError("Got unexpected response code from update attributes request: \(response.responseStatusCode).")
|
|
}
|
|
return nil
|
|
}
|
|
.recover(on: schedulers.sync) { error in
|
|
if error.isNetworkFailureOrTimeout, retriesLeft > 0 {
|
|
return makeUpdateAccountAttributesRequest(
|
|
attributes,
|
|
auth: auth,
|
|
signalService: signalService,
|
|
schedulers: schedulers,
|
|
retriesLeft: retriesLeft - 1
|
|
)
|
|
}
|
|
return .value(error)
|
|
}
|
|
}
|
|
|
|
enum WhoAmIResponse {
|
|
case success(WhoAmIRequestFactory.Responses.WhoAmI)
|
|
case networkError
|
|
case genericError
|
|
}
|
|
|
|
public static func makeWhoAmIRequest(
|
|
auth: ChatServiceAuth,
|
|
signalService: OWSSignalServiceProtocol,
|
|
schedulers: Schedulers,
|
|
retriesLeft: Int = RegistrationCoordinatorImpl.Constants.networkErrorRetries
|
|
) -> Guarantee<WhoAmIResponse> {
|
|
let request = WhoAmIRequestFactory.whoAmIRequest(auth: auth)
|
|
return signalService.urlSessionForMainSignalService().promiseForTSRequest(request)
|
|
.map(on: schedulers.sync) { response in
|
|
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)
|
|
}
|
|
.recover(on: schedulers.sync) { error -> Guarantee<WhoAmIResponse> in
|
|
if error.isNetworkFailureOrTimeout, retriesLeft > 0 {
|
|
return makeWhoAmIRequest(
|
|
auth: auth,
|
|
signalService: signalService,
|
|
schedulers: schedulers,
|
|
retriesLeft: retriesLeft - 1
|
|
)
|
|
}
|
|
return .value(error.isNetworkFailureOrTimeout ? .networkError : .genericError)
|
|
}
|
|
}
|
|
|
|
private static func makeRequest<ResponseType>(
|
|
_ request: TSRequest,
|
|
signalService: OWSSignalServiceProtocol,
|
|
schedulers: Schedulers,
|
|
handler: @escaping (_ statusCode: Int, _ retryAfterHeader: String?, _ bodyData: Data?) -> ResponseType,
|
|
fallbackError: ResponseType,
|
|
networkFailureError: ResponseType
|
|
) -> Guarantee<ResponseType> {
|
|
return signalService.urlSessionForMainSignalService().promiseForTSRequest(request)
|
|
.map(on: schedulers.global()) { (response: HTTPResponse) -> ResponseType in
|
|
return handler(
|
|
response.responseStatusCode,
|
|
response.responseHeaders[Constants.retryAfterHeader],
|
|
response.responseBodyData
|
|
)
|
|
}
|
|
.recover(on: schedulers.global()) { (error: Error) -> Guarantee<ResponseType> in
|
|
if error.isNetworkFailureOrTimeout {
|
|
return .value(networkFailureError)
|
|
}
|
|
guard let error = error as? OWSHTTPError else {
|
|
return .value(fallbackError)
|
|
}
|
|
let response = handler(
|
|
error.responseStatusCode,
|
|
error.responseHeaders?.value(forHeader: Constants.retryAfterHeader),
|
|
error.httpResponseData
|
|
)
|
|
return .value(response)
|
|
}
|
|
}
|
|
|
|
enum Constants {
|
|
static let defaultRetryTime: TimeInterval = 3
|
|
|
|
static let retryAfterHeader = "retry-after"
|
|
}
|
|
}
|
|
}
|