Path: blob/main/SignalServiceKit/Network/OWSURLSessionProtocol.swift
1 views
//
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
// MARK: - HTTPMethod
public enum HTTPMethod: UInt {
case get
case post
case put
case head
case patch
case delete
public var methodName: String {
switch self {
case .get:
return "GET"
case .post:
return "POST"
case .put:
return "PUT"
case .head:
return "HEAD"
case .patch:
return "PATCH"
case .delete:
return "DELETE"
}
}
public static func method(for method: String?) throws -> HTTPMethod {
switch method {
case "GET":
return .get
case "POST":
return .post
case "PUT":
return .put
case "HEAD":
return .head
case "PATCH":
return .patch
case "DELETE":
return .delete
default:
throw OWSAssertionError("Unknown method: \(String(describing: method))")
}
}
}
extension HTTPMethod: CustomStringConvertible {
public var description: String { methodName }
}
// MARK: - OWSUrlDownloadResponse
public struct OWSUrlDownloadResponse {
public let httpUrlResponse: HTTPURLResponse
public let downloadUrl: URL
public var statusCode: Int {
httpUrlResponse.statusCode
}
public var allHeaderFields: [AnyHashable: Any] {
httpUrlResponse.allHeaderFields
}
}
// MARK: - OWSUrlFrontingInfo
struct OWSUrlFrontingInfo {
let frontingURLWithoutPathPrefix: URL
let frontingURLWithPathPrefix: URL
func isFrontedUrl(_ urlString: String) -> Bool {
urlString.lowercased().hasPrefix(frontingURLWithoutPathPrefix.absoluteString)
}
}
// MARK: - OWSURLSession
// OWSURLSession is typically used for a single REST request.
//
// TODO: If we use OWSURLSession more, we'll need to add support for more features, e.g.:
//
// * Download tasks to memory.
public protocol OWSURLSessionProtocol: AnyObject {
var endpoint: OWSURLSessionEndpoint { get }
// By default OWSURLSession treats 4xx and 5xx responses as errors.
var require2xxOr3xx: Bool { get set }
var allowRedirects: Bool { get set }
var customRedirectHandler: ((URLRequest) -> URLRequest?)? { get set }
static var defaultSecurityPolicy: HttpSecurityPolicy { get }
static var signalServiceSecurityPolicy: HttpSecurityPolicy { get }
static var defaultConfigurationWithCaching: URLSessionConfiguration { get }
static var defaultConfigurationWithoutCaching: URLSessionConfiguration { get }
static var userAgentHeaderKey: String { get }
static var userAgentHeaderValueSignalIos: String { get }
static var acceptLanguageHeaderKey: String { get }
static var acceptLanguageHeaderValue: String { get }
// MARK: Initializer
/// - parameter onFailureCallback: called for any failure on any request made using this session.
init(
endpoint: OWSURLSessionEndpoint,
configuration: URLSessionConfiguration,
maxResponseSize: UInt64?,
canUseSignalProxy: Bool,
onFailureCallback: ((any Error) -> Void)?,
)
// MARK: Tasks
func performRequest(_ rawRequest: TSRequest) async throws -> HTTPResponse
func performUpload(
request: URLRequest,
requestData: Data,
progressBlock: OWSURLSession.ProgressBlock,
) async throws -> HTTPResponse
func performUpload(
request: URLRequest,
fileUrl: URL,
ignoreAppExpiry: Bool,
progressBlock: OWSURLSession.ProgressBlock,
) async throws -> HTTPResponse
func performRequest(
request: URLRequest,
ignoreAppExpiry: Bool,
) async throws -> HTTPResponse
func performDownload(
requestUrl: URL,
resumeData: Data,
progressBlock: OWSURLSession.ProgressBlock,
) async throws -> OWSUrlDownloadResponse
func performDownload(
request: URLRequest,
progressBlock: OWSURLSession.ProgressBlock,
) async throws -> OWSUrlDownloadResponse
func webSocketTask(
requestUrl: URL,
didOpenBlock: @escaping (String?) -> Void,
didCloseBlock: @escaping (Error) -> Void,
) -> URLSessionWebSocketTask
}
extension OWSURLSessionProtocol {
init(
endpoint: OWSURLSessionEndpoint,
configuration: URLSessionConfiguration,
maxResponseSize: UInt64? = nil,
canUseSignalProxy: Bool = false,
) {
self.init(
endpoint: endpoint,
configuration: configuration,
maxResponseSize: maxResponseSize,
canUseSignalProxy: canUseSignalProxy,
onFailureCallback: nil,
)
}
}
// MARK: -
public extension OWSURLSessionProtocol {
// MARK: - Upload Tasks Convenience
func performUpload(
_ urlString: String,
method: HTTPMethod,
headers: HttpHeaders = HttpHeaders(),
requestData: Data,
progressBlock: OWSURLSession.ProgressBlock = { _, _ in },
) async throws -> HTTPResponse {
let request = try self.endpoint.buildRequest(urlString, method: method, headers: headers, body: requestData)
return try await self.performUpload(request: request, requestData: requestData, progressBlock: progressBlock)
}
func performUpload(
_ urlString: String,
method: HTTPMethod,
headers: HttpHeaders = HttpHeaders(),
fileUrl: URL,
progressBlock: OWSURLSession.ProgressBlock = { _, _ in },
) async throws -> HTTPResponse {
let request = try self.endpoint.buildRequest(urlString, method: method, headers: headers)
return try await self.performUpload(
request: request,
fileUrl: fileUrl,
ignoreAppExpiry: false,
progressBlock: progressBlock,
)
}
// MARK: - Data Tasks Convenience
func performRequest(
_ urlString: String,
method: HTTPMethod,
headers: HttpHeaders = HttpHeaders(),
body: Data? = nil,
ignoreAppExpiry: Bool = false,
) async throws -> HTTPResponse {
let request = try self.endpoint.buildRequest(urlString, method: method, headers: headers, body: body)
return try await self.performRequest(request: request, ignoreAppExpiry: ignoreAppExpiry)
}
// MARK: - Download Tasks Convenience
func performDownload(
_ urlString: String,
method: HTTPMethod,
headers: HttpHeaders = HttpHeaders(),
body: Data? = nil,
progressBlock: OWSURLSession.ProgressBlock = { _, _ in },
) async throws -> OWSUrlDownloadResponse {
let request = try self.endpoint.buildRequest(urlString, method: method, headers: headers, body: body)
return try await self.performDownload(request: request, progressBlock: progressBlock)
}
}
// MARK: - MultiPart Task
extension OWSURLSessionProtocol {
public func performMultiPartUpload(
request: URLRequest,
fileUrl inputFileURL: URL,
name: String,
fileName: String,
mimeType: String,
textParts textPartsDictionary: OrderedDictionary<String, String>,
ignoreAppExpiry: Bool = false,
progressBlock: OWSURLSession.ProgressBlock = { _, _ in },
) async throws -> HTTPResponse {
let multipartBodyFileURL = OWSFileSystem.temporaryFileUrl(
fileExtension: nil,
isAvailableWhileDeviceLocked: true,
)
defer {
do {
try OWSFileSystem.deleteFileIfExists(url: multipartBodyFileURL)
} catch {
owsFailDebug("Error: \(error)")
}
}
let boundary = OWSMultipartBody.createMultipartFormBoundary()
// Order of form parts matters.
let textParts = textPartsDictionary.map { key, value in
OWSMultipartTextPart(key: key, value: value)
}
try OWSMultipartBody.write(
inputFile: inputFileURL,
outputFile: multipartBodyFileURL,
name: name,
fileName: fileName,
mimeType: mimeType,
boundary: boundary,
textParts: textParts,
)
let bodyFileSize: UInt64 = try OWSFileSystem.fileSize(of: multipartBodyFileURL)
var request = request
request.httpMethod = HTTPMethod.post.methodName
request.setValue(Self.userAgentHeaderValueSignalIos, forHTTPHeaderField: Self.userAgentHeaderKey)
request.setValue(Self.acceptLanguageHeaderValue, forHTTPHeaderField: Self.acceptLanguageHeaderKey)
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
request.setValue(String(format: "%llu", bodyFileSize), forHTTPHeaderField: "Content-Length")
return try await performUpload(
request: request,
fileUrl: multipartBodyFileURL,
ignoreAppExpiry: ignoreAppExpiry,
progressBlock: progressBlock,
)
}
}