You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
session-ios/SessionUtilitiesKit/Networking/HTTP.swift

219 lines
11 KiB
Swift

import Foundation
import PromiseKit
public enum HTTP {
private static let seedNodeURLSession = URLSession(configuration: .ephemeral, delegate: seedNodeURLSessionDelegate, delegateQueue: nil)
private static let seedNodeURLSessionDelegate = SeedNodeURLSessionDelegateImplementation()
private static let snodeURLSession = URLSession(configuration: .ephemeral, delegate: snodeURLSessionDelegate, delegateQueue: nil)
private static let snodeURLSessionDelegate = SnodeURLSessionDelegateImplementation()
// MARK: Certificates
3 years ago
private static let storageSeed1Cert: SecCertificate = {
let path = Bundle.main.path(forResource: "storage-seed-1", ofType: "der")!
3 years ago
let data = try! Data(contentsOf: URL(fileURLWithPath: path))
return SecCertificateCreateWithData(nil, data as CFData)!
}()
3 years ago
private static let storageSeed3Cert: SecCertificate = {
let path = Bundle.main.path(forResource: "storage-seed-3", ofType: "der")!
3 years ago
let data = try! Data(contentsOf: URL(fileURLWithPath: path))
return SecCertificateCreateWithData(nil, data as CFData)!
}()
3 years ago
private static let publicLokiFoundationCert: SecCertificate = {
let path = Bundle.main.path(forResource: "public-loki-foundation", ofType: "der")!
3 years ago
let data = try! Data(contentsOf: URL(fileURLWithPath: path))
return SecCertificateCreateWithData(nil, data as CFData)!
}()
// MARK: Settings
public static let timeout: TimeInterval = 10
// MARK: Seed Node URL Session Delegate Implementation
private final class SeedNodeURLSessionDelegateImplementation : NSObject, URLSessionDelegate {
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
3 years ago
guard let trust = challenge.protectionSpace.serverTrust else {
return completionHandler(.cancelAuthenticationChallenge, nil)
}
// Mark the seed node certificates as trusted
let certificates = [ storageSeed1Cert, storageSeed3Cert, publicLokiFoundationCert ]
guard SecTrustSetAnchorCertificates(trust, certificates as CFArray) == errSecSuccess else {
return completionHandler(.cancelAuthenticationChallenge, nil)
}
3 years ago
// Check that the presented certificate is one of the seed node certificates
3 years ago
var result: SecTrustResultType = .invalid
guard SecTrustEvaluate(trust, &result) == errSecSuccess else {
return completionHandler(.cancelAuthenticationChallenge, nil)
}
switch result {
3 years ago
case .proceed, .unspecified:
// Unspecified indicates that evaluation reached an (implicitly trusted) anchor certificate without
// any evaluation failures, but never encountered any explicitly stated user-trust preference. This
// is the most common return value. The Keychain Access utility refers to this value as the "Use System
// Policy," which is the default user setting.
return completionHandler(.useCredential, URLCredential(trust: trust))
3 years ago
default: return completionHandler(.cancelAuthenticationChallenge, nil)
}
}
}
// MARK: Snode URL Session Delegate Implementation
private final class SnodeURLSessionDelegateImplementation : NSObject, URLSessionDelegate {
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
// Snode to snode communication uses self-signed certificates but clients can safely ignore this
completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!))
}
}
// MARK: - Main
Work on the PromiseKit refactor # Conflicts: # Session.xcodeproj/project.pbxproj # Session/Conversations/ConversationVC+Interaction.swift # Session/Home/Message Requests/MessageRequestsViewModel.swift # Session/Notifications/AppNotifications.swift # Session/Notifications/PushRegistrationManager.swift # Session/Notifications/SyncPushTokensJob.swift # Session/Notifications/UserNotificationsAdaptee.swift # Session/Settings/BlockedContactsViewModel.swift # Session/Settings/NukeDataModal.swift # Session/Settings/SettingsViewModel.swift # Session/Utilities/BackgroundPoller.swift # SessionMessagingKit/Database/Models/ClosedGroup.swift # SessionMessagingKit/File Server/FileServerAPI.swift # SessionMessagingKit/Open Groups/OpenGroupAPI.swift # SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift # SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift # SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift # SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift # SessionMessagingKit/Sending & Receiving/MessageSender.swift # SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift # SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift # SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift # SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift # SessionMessagingKit/Utilities/ProfileManager.swift # SessionSnodeKit/Networking/SnodeAPI.swift # SessionSnodeKit/OnionRequestAPI.swift # SessionUtilitiesKit/Networking/HTTP.swift
2 years ago
public static func executeLegacy(
_ method: HTTPMethod,
_ url: String,
timeout: TimeInterval = HTTP.defaultTimeout,
useSeedNodeURLSession: Bool = false
) -> Promise<Data> {
return executeLegacy(method, url, body: nil, timeout: timeout, useSeedNodeURLSession: useSeedNodeURLSession)
}
Work on the PromiseKit refactor # Conflicts: # Session.xcodeproj/project.pbxproj # Session/Conversations/ConversationVC+Interaction.swift # Session/Home/Message Requests/MessageRequestsViewModel.swift # Session/Notifications/AppNotifications.swift # Session/Notifications/PushRegistrationManager.swift # Session/Notifications/SyncPushTokensJob.swift # Session/Notifications/UserNotificationsAdaptee.swift # Session/Settings/BlockedContactsViewModel.swift # Session/Settings/NukeDataModal.swift # Session/Settings/SettingsViewModel.swift # Session/Utilities/BackgroundPoller.swift # SessionMessagingKit/Database/Models/ClosedGroup.swift # SessionMessagingKit/File Server/FileServerAPI.swift # SessionMessagingKit/Open Groups/OpenGroupAPI.swift # SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift # SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift # SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift # SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift # SessionMessagingKit/Sending & Receiving/MessageSender.swift # SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift # SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift # SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift # SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift # SessionMessagingKit/Utilities/ProfileManager.swift # SessionSnodeKit/Networking/SnodeAPI.swift # SessionSnodeKit/OnionRequestAPI.swift # SessionUtilitiesKit/Networking/HTTP.swift
2 years ago
public static func executeLegacy(
_ method: HTTPMethod,
_ url: String,
body: Data?,
timeout: TimeInterval = HTTP.defaultTimeout,
useSeedNodeURLSession: Bool = false
) -> Promise<Data> {
var request = URLRequest(url: URL(string: url)!)
request.httpMethod = verb.rawValue
request.httpBody = body
request.timeoutInterval = timeout
request.allHTTPHeaderFields?.removeValue(forKey: "User-Agent")
request.setValue("WhatsApp", forHTTPHeaderField: "User-Agent") // Set a fake value
request.setValue("en-us", forHTTPHeaderField: "Accept-Language") // Set a fake value
let (promise, seal) = Promise<Data>.pending()
let urlSession = useSeedNodeURLSession ? seedNodeURLSession : snodeURLSession
let task = urlSession.dataTask(with: request) { data, response, error in
guard let data = data, let response = response as? HTTPURLResponse else {
if let error = error {
SNLog("\(verb.rawValue) request to \(url) failed due to error: \(error).")
} else {
SNLog("\(verb.rawValue) request to \(url) failed.")
}
// Override the actual error so that we can correctly catch failed requests in sendOnionRequest(invoking:on:with:)
switch (error as? NSError)?.code {
case NSURLErrorTimedOut: return seal.reject(Error.timeout)
default: return seal.reject(Error.httpRequestFailed(statusCode: 0, data: nil))
}
}
if let error = error {
SNLog("\(verb.rawValue) request to \(url) failed due to error: \(error).")
// Override the actual error so that we can correctly catch failed requests in sendOnionRequest(invoking:on:with:)
return seal.reject(Error.httpRequestFailed(statusCode: 0, data: data))
}
let statusCode = UInt(response.statusCode)
guard 200...299 ~= statusCode else {
var json: JSON? = nil
if let processedJson: JSON = try? JSONSerialization.jsonObject(with: data, options: [ .fragmentsAllowed ]) as? JSON {
json = processedJson
}
else if let result: String = String(data: data, encoding: .utf8) {
json = [ "result": result ]
}
let jsonDescription: String = (json?.prettifiedDescription ?? "no debugging info provided")
SNLog("\(verb.rawValue) request to \(url) failed with status code: \(statusCode) (\(jsonDescription)).")
return seal.reject(Error.httpRequestFailed(statusCode: statusCode, data: data))
}
seal.fulfill(data)
}
task.resume()
return promise
}
Work on the PromiseKit refactor # Conflicts: # Session.xcodeproj/project.pbxproj # Session/Conversations/ConversationVC+Interaction.swift # Session/Home/Message Requests/MessageRequestsViewModel.swift # Session/Notifications/AppNotifications.swift # Session/Notifications/PushRegistrationManager.swift # Session/Notifications/SyncPushTokensJob.swift # Session/Notifications/UserNotificationsAdaptee.swift # Session/Settings/BlockedContactsViewModel.swift # Session/Settings/NukeDataModal.swift # Session/Settings/SettingsViewModel.swift # Session/Utilities/BackgroundPoller.swift # SessionMessagingKit/Database/Models/ClosedGroup.swift # SessionMessagingKit/File Server/FileServerAPI.swift # SessionMessagingKit/Open Groups/OpenGroupAPI.swift # SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift # SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+UnsendRequests.swift # SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift # SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift # SessionMessagingKit/Sending & Receiving/MessageSender.swift # SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift # SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift # SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift # SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift # SessionMessagingKit/Utilities/ProfileManager.swift # SessionSnodeKit/Networking/SnodeAPI.swift # SessionSnodeKit/OnionRequestAPI.swift # SessionUtilitiesKit/Networking/HTTP.swift
2 years ago
// MARK: - Execution
public static func execute(
_ method: HTTPMethod,
_ url: String,
timeout: TimeInterval = HTTP.defaultTimeout,
useSeedNodeURLSession: Bool = false
) -> AnyPublisher<Data, Error> {
return execute(
method,
url,
body: nil,
timeout: timeout,
useSeedNodeURLSession: useSeedNodeURLSession
)
}
public static func execute(
_ method: HTTPMethod,
_ url: String,
body: Data?, // TODO: Default Value?
timeout: TimeInterval = HTTP.defaultTimeout,
useSeedNodeURLSession: Bool = false
) -> AnyPublisher<Data, Error> {
guard let url: URL = URL(string: url) else {
return Fail<Data, Error>(error: HTTPError.invalidURL)
.eraseToAnyPublisher()
}
let urlSession: URLSession = (useSeedNodeURLSession ? seedNodeURLSession : snodeURLSession)
var request = URLRequest(url: url)
request.httpMethod = method.rawValue
request.httpBody = body
request.timeoutInterval = timeout
request.allHTTPHeaderFields?.removeValue(forKey: "User-Agent")
request.setValue("WhatsApp", forHTTPHeaderField: "User-Agent") // Set a fake value
request.setValue("en-us", forHTTPHeaderField: "Accept-Language") // Set a fake value
return urlSession
.dataTaskPublisher(for: request)
.mapError { error in
SNLog("\(method.rawValue) request to \(url) failed due to error: \(error).")
// Override the actual error so that we can correctly catch failed requests
// in sendOnionRequest(invoking:on:with:)
switch (error as NSError).code {
case NSURLErrorTimedOut: return HTTPError.timeout
default: return HTTPError.httpRequestFailed(statusCode: 0, data: nil)
}
}
.flatMap { data, response in
guard let response = response as? HTTPURLResponse else {
SNLog("\(method.rawValue) request to \(url) failed.")
return Fail<Data, Error>(error: HTTPError.httpRequestFailed(statusCode: 0, data: data))
.eraseToAnyPublisher()
}
let statusCode = UInt(response.statusCode)
// TODO: Remove all the JSON handling?
guard 200...299 ~= statusCode else {
var json: JSON? = nil
if let processedJson: JSON = try? JSONSerialization.jsonObject(with: data, options: [ .fragmentsAllowed ]) as? JSON {
json = processedJson
}
else if let result: String = String(data: data, encoding: .utf8) {
json = [ "result": result ]
}
let jsonDescription: String = (json?.prettifiedDescription ?? "no debugging info provided")
SNLog("\(method.rawValue) request to \(url) failed with status code: \(statusCode) (\(jsonDescription)).")
return Fail<Data, Error>(error: HTTPError.httpRequestFailed(statusCode: statusCode, data: data))
.eraseToAnyPublisher()
}
return Just(data)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
}