Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
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,
        )
    }
}