Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/SignalServiceKit/Network/API/Requests/OWSRequestFactory.swift
1 views
//
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import Foundation
import LibSignalClient

public enum OWSRequestFactory {

    static let textSecureAccountsAPI = "v1/accounts"
    static let textSecureAttributesAPI = "v1/accounts/attributes/"
    static let textSecureMessagesAPI = "v1/messages/"
    static let textSecureKeysAPI = "v2/keys"
    static let textSecureSignedKeysAPI = "v2/keys/signed"
    static let textSecureDirectoryAPI = "v1/directory"
    static let textSecure2FAAPI = "v1/accounts/pin"
    static let textSecureRegistrationLockV2API = "v1/accounts/registration_lock"
    static let textSecureGiftBadgePricesAPI = "v1/subscription/boost/amounts/gift"

    public static let textSecureHTTPTimeOut: TimeInterval = 10

    // MARK: - Other

    static func currencyConversionRequest() -> TSRequest {
        return TSRequest(url: URL(string: "v1/payments/conversions")!, method: "GET", parameters: [:])
    }

    static func getRemoteConfigRequest(eTag: String?) -> TSRequest {
        var request = TSRequest(url: URL(string: "v2/config/")!, method: "GET", parameters: [:])
        if let eTag {
            request.headers["If-None-Match"] = eTag
        }
        return request
    }

    public static func callingRelaysRequest() -> TSRequest {
        return TSRequest(url: URL(string: "v2/calling/relays")!, method: "GET", parameters: [:])
    }

    // MARK: - Auth

    static func authCredentialRequest(from fromRedemptionSeconds: UInt64, to toRedemptionSeconds: UInt64) -> TSRequest {
        owsAssertDebug(fromRedemptionSeconds > 0)
        owsAssertDebug(toRedemptionSeconds > 0)

        let path = "v1/certificate/auth/group?redemptionStartSeconds=\(fromRedemptionSeconds)&redemptionEndSeconds=\(toRedemptionSeconds)"
        return TSRequest(url: URL(string: path)!, method: "GET", parameters: [:])
    }

    public static func paymentsAuthenticationCredentialRequest() -> TSRequest {
        return TSRequest(url: URL(string: "v1/payments/auth")!, method: "GET", parameters: [:])
    }

    static func remoteAttestationAuthRequestForCDSI() -> TSRequest {
        return TSRequest(url: URL(string: "v2/directory/auth")!, method: "GET", parameters: [:])
    }

    static func remoteAttestationAuthRequestForSVR2() -> TSRequest {
        return TSRequest(url: URL(string: "v2/svr/auth")!, method: "GET", parameters: [:])
    }

    static func storageAuthRequest(auth: ChatServiceAuth) -> TSRequest {
        var result = TSRequest(url: URL(string: "v1/storage/auth")!, method: "GET", parameters: [:])
        result.auth = .identified(auth)
        return result
    }

    // MARK: - Challenges

    static func pushChallengeRequest() -> TSRequest {
        return TSRequest(url: URL(string: "v1/challenge/push")!, method: "POST", parameters: [:])
    }

    static func pushChallengeResponse(token: String) -> TSRequest {
        return TSRequest(url: URL(string: "v1/challenge")!, method: "PUT", parameters: ["type": "rateLimitPushChallenge", "challenge": token])
    }

    static func recaptchChallengeResponse(serverToken: String, captchaToken: String) -> TSRequest {
        return TSRequest(url: URL(string: "v1/challenge")!, method: "PUT", parameters: ["type": "captcha", "token": serverToken, "captcha": captchaToken])
    }

    // MARK: - Messages

    static func udSenderCertificateRequest(uuidOnly: Bool) -> TSRequest {
        var path = "v1/certificate/delivery"
        if uuidOnly {
            path += "?includeE164=false"
        }
        return TSRequest(url: URL(string: path)!, method: "GET", parameters: [:])
    }

    static func accountRequest(serviceId: ServiceId) -> TSRequest {
        var request = TSRequest(url: URL(string: "v1/accounts/account/\(serviceId.serviceIdString)")!, method: "HEAD")
        request.auth = .anonymous
        return request
    }

    static func submitMessageRequest(
        serviceId: ServiceId,
        messages: [DeviceMessage],
        timestamp: UInt64,
        isOnline: Bool,
        isUrgent: Bool,
        auth: TSRequest.SealedSenderAuth?,
    ) -> TSRequest {
        // NOTE: messages may be empty; See comments in OWSDeviceManager.
        owsAssertDebug(timestamp > 0)

        let path = "\(self.textSecureMessagesAPI)\(serviceId.serviceIdString)?story=\(auth?.isStory == true ? "true" : "false")"

        // Returns the per-account-message parameters used when submitting a message to
        // the Signal Web Service.
        // See
        // <https://github.com/signalapp/Signal-Server/blob/65da844d70369cb8b44966cfb2d2eb9b925a6ba4/service/src/main/java/org/whispersystems/textsecuregcm/entities/IncomingMessageList.java>.
        let parameters: [String: Any] = [
            "messages": messages.map { $0.requestParameters() },
            "timestamp": timestamp,
            "online": isOnline,
            "urgent": isUrgent,
        ]

        var request = TSRequest(url: URL(string: path)!, method: "PUT", parameters: parameters)
        request.timeoutInterval = sendMessageTimeout(estimatedRequestSize: messages.reduce(into: 0, { $0 += $1.content.count + 50 }) + 100)
        if let auth {
            request.auth = .sealedSender(auth)
        }
        return request
    }

    static func sendMessageTimeout(estimatedRequestSize: Int) -> TimeInterval {
        let bandwidthEstimate: Double = 40_000 // kbit/s
        let transferEstimate = Double(estimatedRequestSize) / (bandwidthEstimate / 8)
        let latencyEstimate: Double = Self.textSecureHTTPTimeOut
        let overallEstimate = latencyEstimate + transferEstimate
        // Limit to 45 seconds (the maximum time allowed by the pinging logic) to
        // support larger messages.
        return min(overallEstimate, 45)
    }

    // MARK: - Registration

    public static func enableRegistrationLockV2Request(token: String, logger: PrefixedLogger) -> TSRequest {
        owsAssertDebug(nil != token.nilIfEmpty)

        let url = URL(string: textSecureRegistrationLockV2API)!
        return TSRequest(
            url: url,
            method: HTTPMethod.put.methodName,
            parameters: [
                "registrationLock": token,
            ],
            logger: logger,
        )
    }

    static func disableRegistrationLockV2Request() -> TSRequest {
        let url = URL(string: textSecureRegistrationLockV2API)!
        return TSRequest(
            url: url,
            method: HTTPMethod.delete.methodName,
            parameters: [:],
        )
    }

    public static func registerForPushRequest(apnsToken: String) -> TSRequest {
        owsAssertDebug(!apnsToken.isEmpty)

        let path = "\(self.textSecureAccountsAPI)/apn"

        return TSRequest(url: URL(string: path)!, method: "PUT", parameters: ["apnRegistrationId": apnsToken])
    }

    static func unregisterAccountRequest() -> TSRequest {
        let path = "\(self.textSecureAccountsAPI)/me"
        return TSRequest(url: URL(string: path)!, method: "DELETE", parameters: [:])
    }

    static let batchIdentityCheckElementsLimit = 1000
    static func batchIdentityCheckRequest(elements: [[String: String]]) -> TSRequest {
        precondition(elements.count <= batchIdentityCheckElementsLimit)
        var request = TSRequest(
            url: .init(string: "v1/profile/identity_check/batch")!,
            method: HTTPMethod.post.methodName,
            parameters: ["elements": elements],
        )
        request.auth = .anonymous
        return request
    }

    // MARK: - Devices

    static func deviceProvisioningCode() -> TSRequest {
        return TSRequest(
            url: URL(string: "v1/devices/provisioning/code")!,
            method: "GET",
            parameters: nil,
        )
    }

    static func provisionDevice(withMessageBody messageBody: Data, ephemeralDeviceId: String) -> TSRequest {
        owsAssertDebug(!messageBody.isEmpty)
        owsAssertDebug(!ephemeralDeviceId.isEmpty)

        return .init(
            url: .init(pathComponents: ["v1", "provisioning", ephemeralDeviceId])!,
            method: "PUT",
            parameters: ["body": messageBody.base64EncodedString()],
        )
    }

    // MARK: - Donations

    static func setSubscriberID(_ subscriberID: Data) -> TSRequest {
        var result = TSRequest(
            url: .init(pathComponents: ["v1", "subscription", subscriberID.asBase64Url])!,
            method: "PUT",
            parameters: nil,
        )
        result.auth = .anonymous
        result.applyRedactionStrategy(.redactURL())
        return result
    }

    static func deleteSubscriberID(_ subscriberID: Data) -> TSRequest {
        var result = TSRequest(
            url: .init(pathComponents: ["v1", "subscription", subscriberID.asBase64Url])!,
            method: "DELETE",
            parameters: nil,
        )
        result.auth = .anonymous
        result.applyRedactionStrategy(.redactURL())
        return result
    }

    static func subscriptionSetDefaultPaymentMethod(
        subscriberId: Data,
        processor: String,
        paymentMethodId: String,
    ) -> TSRequest {
        var result = TSRequest(
            url: .init(pathComponents: [
                "v1",
                "subscription",
                subscriberId.asBase64Url,
                "default_payment_method",
                processor,
                paymentMethodId,
            ])!,
            method: "POST",
            parameters: nil,
        )
        result.auth = .anonymous
        result.applyRedactionStrategy(.redactURL())
        return result
    }

    static func subscriptionSetDefaultIDEALPaymentMethod(
        subscriberId: Data,
        setupIntentId: String,
    ) -> TSRequest {
        var result = TSRequest(
            url: .init(pathComponents: [
                "v1",
                "subscription",
                subscriberId.asBase64Url,
                "default_payment_method_for_ideal",
                setupIntentId,
            ])!,
            method: "POST",
            parameters: nil,
        )
        result.auth = .anonymous
        result.applyRedactionStrategy(.redactURL())
        return result
    }

    static func subscriptionCreateStripePaymentMethodRequest(subscriberID: Data) -> TSRequest {
        var result = TSRequest(
            url: .init(pathComponents: [
                "v1",
                "subscription",
                subscriberID.asBase64Url,
                "create_payment_method",
            ])!,
            method: "POST",
            parameters: nil,
        )
        result.auth = .anonymous
        result.applyRedactionStrategy(.redactURL())
        return result
    }

    static func subscriptionCreatePaypalPaymentMethodRequest(
        subscriberID: Data,
        returnURL: URL,
        cancelURL: URL,
    ) -> TSRequest {
        var result = TSRequest(
            url: .init(pathComponents: [
                "v1",
                "subscription",
                subscriberID.asBase64Url,
                "create_payment_method",
                "paypal",
            ])!,
            method: "POST",
            parameters: [
                "returnUrl": returnURL.absoluteString,
                "cancelUrl": cancelURL.absoluteString,
            ],
        )
        result.auth = .anonymous
        result.applyRedactionStrategy(.redactURL())
        return result
    }

    static func subscriptionSetSubscriptionLevelRequest(
        subscriberID: Data,
        level: UInt,
        currency: String,
        idempotencyKey: String,
    ) -> TSRequest {
        var result = TSRequest(
            url: .init(pathComponents: [
                "v1",
                "subscription",
                subscriberID.asBase64Url,
                "level",
                String(level),
                currency,
                idempotencyKey,
            ])!,
            method: "PUT",
            parameters: nil,
        )
        result.auth = .anonymous
        result.applyRedactionStrategy(.redactURL())
        return result
    }

    static func subscriptionReceiptCredentialsRequest(
        subscriberID: Data,
        receiptCredentialRequest: ReceiptCredentialRequest,
    ) -> TSRequest {
        var result = TSRequest(
            url: .init(pathComponents: [
                "v1",
                "subscription",
                subscriberID.asBase64Url,
                "receipt_credentials",
            ])!,
            method: "POST",
            parameters: [
                "receiptCredentialRequest": receiptCredentialRequest.serialize().base64EncodedString(),
            ],
        )
        result.auth = .anonymous
        result.applyRedactionStrategy(.redactURL())
        return result
    }

    static func subscriptionRedeemReceiptCredential(
        receiptCredentialPresentation: Data,
        displayBadgesOnProfile: Bool,
    ) -> TSRequest {
        return TSRequest(
            url: .init(pathComponents: [
                "v1",
                "donation",
                "redeem-receipt",
            ])!,
            method: "POST",
            parameters: [
                "receiptCredentialPresentation": receiptCredentialPresentation.base64EncodedString(),
                "visible": displayBadgesOnProfile,
                "primary": false,
            ],
        )
    }

    static func boostReceiptCredentials(
        paymentIntentID: String,
        paymentProcessor: DonationPaymentProcessor,
        receiptCredentialRequest: ReceiptCredentialRequest,
    ) -> TSRequest {
        var result = TSRequest(
            url: .init(pathComponents: [
                "v1",
                "subscription",
                "boost",
                "receipt_credentials",
            ])!,
            method: "POST",
            parameters: [
                "paymentIntentId": paymentIntentID,
                "receiptCredentialRequest": receiptCredentialRequest.serialize().base64EncodedString(),
                "processor": paymentProcessor.rawValue,
            ],
        )
        result.auth = .anonymous
        return result
    }

    public static func bankMandateRequest(bankTransferType: StripePaymentMethod.BankTransfer) -> TSRequest {
        var result = TSRequest(
            url: .init(pathComponents: [
                "v1",
                "subscription",
                "bank_mandate",
                bankTransferType.rawValue,
            ])!,
            method: "GET",
            parameters: nil,
        )
        result.headers[HttpHeaders.acceptLanguageHeaderKey] = HttpHeaders.acceptLanguageHeaderValue
        result.auth = .anonymous
        return result
    }

    // MARK: - Keys

    static func preKeyRequestParameters(_ preKeyRecord: LibSignalClient.PreKeyRecord) -> [String: Any] {
        [
            "keyId": preKeyRecord.id,
            "publicKey": try! preKeyRecord.publicKey().serialize().base64EncodedStringWithoutPadding(),
        ]
    }

    static func signedPreKeyRequestParameters(_ signedPreKeyRecord: LibSignalClient.SignedPreKeyRecord) -> [String: Any] {
        [
            "keyId": signedPreKeyRecord.id,
            "publicKey": try! signedPreKeyRecord.publicKey().serialize().base64EncodedStringWithoutPadding(),
            "signature": signedPreKeyRecord.signature.base64EncodedStringWithoutPadding(),
        ]
    }

    static func pqPreKeyRequestParameters(_ pqPreKeyRecord: LibSignalClient.KyberPreKeyRecord) -> [String: Any] {
        [
            "keyId": pqPreKeyRecord.id,
            "publicKey": try! pqPreKeyRecord.publicKey().serialize().base64EncodedStringWithoutPadding(),
            "signature": pqPreKeyRecord.signature.base64EncodedStringWithoutPadding(),
        ]
    }

    static func availablePreKeysCountRequest(for identity: OWSIdentity) -> TSRequest {
        var path = self.textSecureKeysAPI
        if let queryParam = queryParam(for: identity) {
            path += "?" + queryParam
        }
        return TSRequest(url: URL(string: path)!, method: "GET", parameters: [:])
    }

    static func recipientPreKeyRequest(serviceId: ServiceId, deviceId: String, auth: TSRequest.SealedSenderAuth?) -> TSRequest {
        let path = "\(self.textSecureKeysAPI)/\(serviceId.serviceIdString)/\(deviceId)"

        var request = TSRequest(url: URL(string: path)!, method: "GET", parameters: [:])
        if let auth {
            request.auth = .sealedSender(auth)
        }
        return request
    }

    /// If a username and password are both provided, those are used for the request's
    /// Authentication header. Otherwise, the default header is used (whatever's on
    /// TSAccountManager).
    static func registerPrekeysRequest(
        identity: OWSIdentity,
        signedPreKeyRecord: LibSignalClient.SignedPreKeyRecord?,
        prekeyRecords: [LibSignalClient.PreKeyRecord]?,
        pqLastResortPreKeyRecord: LibSignalClient.KyberPreKeyRecord?,
        pqPreKeyRecords: [LibSignalClient.KyberPreKeyRecord]?,
        auth: ChatServiceAuth,
    ) -> TSRequest {
        var path = textSecureKeysAPI
        if let queryParam = queryParam(for: identity) {
            path = path.appending("?\(queryParam)")
        }

        var parameters = [String: Any]()

        if let signedPreKeyRecord {
            parameters["signedPreKey"] = signedPreKeyRequestParameters(signedPreKeyRecord)
        }
        if let prekeyRecords {
            parameters["preKeys"] = prekeyRecords.map { self.preKeyRequestParameters($0) }
        }
        if let pqLastResortPreKeyRecord {
            parameters["pqLastResortPreKey"] = pqPreKeyRequestParameters(pqLastResortPreKeyRecord)
        }
        if let pqPreKeyRecords {
            parameters["pqPreKeys"] = pqPreKeyRecords.map { self.pqPreKeyRequestParameters($0) }
        }

        var request = TSRequest(
            url: URL(string: path)!,
            method: "PUT",
            parameters: parameters,
        )
        request.auth = .identified(auth)
        request.timeoutInterval = 45
        return request
    }

    static func queryParam(for identity: OWSIdentity) -> String? {
        switch identity {
        case .aci:
            return nil
        case .pni:
            return "identity=pni"
        }
    }

    // MARK: - Profiles

    static func getUnversionedProfileRequest(serviceId: ServiceId, auth: TSRequest.Auth) -> TSRequest {
        let path = "v1/profile/\(serviceId.serviceIdString)"
        var request = TSRequest(url: URL(string: path)!, method: "GET", parameters: [:])
        request.auth = auth
        return request
    }

    static func getVersionedProfileRequest(
        aci: Aci,
        profileKeyVersion: String,
        credentialRequest: Data?,
        auth: TSRequest.Auth,
    ) -> TSRequest {
        var components = [String]()
        components.append(aci.serviceIdString)
        components.append(profileKeyVersion)
        if let credentialRequest, !credentialRequest.isEmpty {
            components.append(credentialRequest.hexadecimalString + "?credentialType=expiringProfileKey")
        }

        let path = "v1/profile/\(components.joined(separator: "/"))"

        var request = TSRequest(url: URL(string: path)!, method: "GET", parameters: [:])
        request.auth = auth
        return request
    }

    public static func setVersionedProfileRequest(
        name: ProfileValue?,
        bio: ProfileValue?,
        bioEmoji: ProfileValue?,
        hasAvatar: Bool,
        sameAvatar: Bool,
        paymentAddress: ProfileValue?,
        phoneNumberSharing: ProfileValue,
        visibleBadgeIds: [String],
        version: String,
        commitment: Data,
        auth: ChatServiceAuth,
    ) -> TSRequest {
        var parameters: [String: Any] = [
            "avatar": hasAvatar,
            "sameAvatar": sameAvatar,
            "badgeIds": visibleBadgeIds,
            "commitment": commitment.base64EncodedString(),
            "phoneNumberSharing": phoneNumberSharing.encryptedBase64Value,
            "version": version,
        ]
        if let name {
            parameters["name"] = name.encryptedBase64Value
        }
        if let bio {
            parameters["about"] = bio.encryptedBase64Value
        }
        if let bioEmoji {
            parameters["aboutEmoji"] = bioEmoji.encryptedBase64Value
        }
        if let paymentAddress {
            parameters["paymentAddress"] = paymentAddress.encryptedBase64Value
        }
        var request = TSRequest(url: URL(string: "v1/profile/")!, method: "PUT", parameters: parameters)
        request.auth = .identified(auth)
        return request
    }
}

// MARK: -

extension DeviceMessage {
    /// Returns the per-device-message parameters when sending a message.
    ///
    /// See <https://github.com/signalapp/Signal-Server/blob/ab26a65/service/src/main/java/org/whispersystems/textsecuregcm/entities/IncomingMessage.java>.
    func requestParameters() -> NSDictionary {
        return [
            "type": type.rawValue,
            "destinationDeviceId": destinationDeviceId.uint32Value,
            "destinationRegistrationId": Int32(bitPattern: destinationRegistrationId),
            "content": content.base64EncodedString(),
        ]
    }
}