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(),
]
}
}