Signal-iOS/SignalServiceKit/src/Network/API/RESTNetworkManager.swift
2021-09-20 17:46:09 -03:00

254 lines
11 KiB
Swift

//
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
import Foundation
import AFNetworking
public extension RESTNetworkManager {
func makePromise(request: TSRequest) -> Promise<HTTPResponse> {
let (promise, future) = Promise<HTTPResponse>.pending()
self.makeRequest(request,
completionQueue: DispatchQueue.global(),
success: { (response: HTTPResponse) in
future.resolve(response)
},
failure: { (error: OWSHTTPErrorWrapper) in
future.reject(error.error)
})
return promise
}
}
// MARK: -
// TODO: Use OWSURLSession instead.
@objc
public class RESTSessionManager: NSObject {
private let sessionManager: AFHTTPSessionManager
@objc
public let createdDate = Date()
@objc
public override required init() {
assertOnQueue(NetworkManagerQueue())
// TODO: Use OWSUrlSession instead.
self.sessionManager = Self.signalService.sessionManagerForMainSignalService()
self.sessionManager.completionQueue = .global()
}
@objc
public func performRequest(_ request: TSRequest,
canUseAuth: Bool,
success: @escaping RESTNetworkManagerSuccess,
failure: @escaping RESTNetworkManagerFailure) {
assertOnQueue(NetworkManagerQueue())
owsAssertDebug(!FeatureFlags.deprecateREST)
guard let rawRequestUrl = request.url else {
owsFailDebug("Missing requestUrl.")
failure(OWSHTTPErrorWrapper(error: .invalidRequest(requestUrl: URL(string: "")!)))
return
}
guard !appExpiry.isExpired else {
owsFailDebug("App is expired.")
failure(OWSHTTPErrorWrapper(error: .invalidAppState(requestUrl: rawRequestUrl)))
return
}
// Use new serializers. This will clear all request headers.
//
// NOTE: that we send JSON and receive a binary Blob.
// NOTE: We could enable HTTPShouldUsePipelining here.
sessionManager.requestSerializer = {
if let requestBody = request.httpBody, request.value(forHTTPHeaderField: "Content-Type") == kSenderKeySendRequestBodyContentType {
return OWSSenderKeyBodyRequestSerializer(senderKeyBody: requestBody)
} else {
return AFJSONRequestSerializer()
}
}()
sessionManager.responseSerializer = AFHTTPResponseSerializer()
let httpHeaders = OWSHttpHeaders()
// Apply the default headers for this session manager.
let defaultHeaders = sessionManager.requestSerializer.httpRequestHeaders
httpHeaders.addHeaderMap(defaultHeaders, overwriteOnConflict: false)
// Clear all default headers.
for headerField in defaultHeaders.keys {
sessionManager.requestSerializer.setValue(nil, forHTTPHeaderField: headerField)
}
// Disable default cookie handling for all requests.
sessionManager.requestSerializer.httpShouldHandleCookies = false
if signalService.isCensorshipCircumventionActive {
httpHeaders.addHeader("Host", value: TSConstants.censorshipReflectorHost, overwriteOnConflict: true)
}
// Set User-Agent header.
httpHeaders.addHeader(OWSURLSession.kUserAgentHeader,
value: OWSURLSession.signalIosUserAgent,
overwriteOnConflict: true)
if signalService.isCensorshipCircumventionActive {
httpHeaders.addHeader("Host", value: TSConstants.censorshipReflectorHost, overwriteOnConflict: true)
}
// Then apply any custom headers for the request
httpHeaders.addHeaderMap(request.allHTTPHeaderFields, overwriteOnConflict: true)
if canUseAuth,
request.shouldHaveAuthorizationHeaders {
owsAssertDebug(nil != request.authUsername?.nilIfEmpty)
owsAssertDebug(nil != request.authPassword?.nilIfEmpty)
sessionManager.requestSerializer.setAuthorizationHeaderFieldWithUsername(request.authUsername ?? "",
password: request.authPassword ?? "")
}
// Most of TSNetwork requests are destined for the Signal Service.
// When we are domain fronting, we have to target a different host and add a path prefix.
// For common Signal-Service requests the host/path-prefix logic is handled by the
// sessionManager.
//
// However, for CDS requests, we need to:
// With CC enabled, use the service fronting Hostname but a custom path-prefix
// With CC disabled, use the custom directory host, and no path-prefix
func buildRequestURL() -> URL? {
if signalService.isCensorshipCircumventionActive,
let customCensorshipCircumventionPrefix = request.customCensorshipCircumventionPrefix?.nilIfEmpty {
// All fronted requests go through the same host
let customBaseUrl: URL = signalService.domainFrontBaseURL.appendingPathComponent(customCensorshipCircumventionPrefix)
guard let requestUrl = OWSURLSession.buildUrl(urlString: rawRequestUrl.absoluteString,
baseUrl: customBaseUrl) else {
owsFailDebug("Could not apply baseUrl.")
return nil
}
return requestUrl
} else if let customHost = request.customHost?.nilIfEmpty {
guard let customBaseUrl = URL(string: customHost) else {
owsFailDebug("Invalid customHost.")
return nil
}
guard let requestUrl = OWSURLSession.buildUrl(urlString: rawRequestUrl.absoluteString,
baseUrl: customBaseUrl) else {
owsFailDebug("Could not apply baseUrl.")
return nil
}
return requestUrl
} else {
// requests for the signal-service (with or without censorship circumvention)
return rawRequestUrl
}
}
guard let requestUrl = buildRequestURL(),
let requestUrlString = requestUrl.absoluteString.nilIfEmpty else {
owsFailDebug("Missing or invalid requestUrl.")
failure(OWSHTTPErrorWrapper(error: .invalidRequest(requestUrl: rawRequestUrl)))
return
}
// Honor the request's headers.
for (key, value) in httpHeaders.headers {
sessionManager.requestSerializer.setValue(value, forHTTPHeaderField: key)
}
func parseResponse(task: URLSessionDataTask?, error: Error? = nil) -> HTTPURLResponse? {
if let response = task?.response as? HTTPURLResponse {
return response
}
if let error = error,
let response = (error as NSError).afFailingHTTPURLResponse {
return response
}
return nil
}
func parseResponseHeaders(task: URLSessionDataTask?) -> OWSHttpHeaders {
let parsedHeaders = OWSHttpHeaders()
guard let response = parseResponse(task: task) else {
return parsedHeaders
}
for (key, value) in response.allHeaderFields {
guard let key = key as? String,
let value = value as? String else {
owsFailDebug("Invalid header: \(key), \(value)")
continue
}
parsedHeaders.addHeader(key, value: value, overwriteOnConflict: false)
}
return parsedHeaders
}
let afSuccess = { (task: URLSessionDataTask?, responseObject: Any?) in
guard let httpUrlResponse = parseResponse(task: task) else {
if DebugFlags.internalLogging,
let response = task?.response {
Logger.warn("Invalid response: \(response)")
}
owsFailDebug("Invalid response")
failure(OWSHTTPErrorWrapper(error: .invalidResponse(requestUrl: requestUrl)))
return
}
var responseData: Data?
if let responseObject = responseObject {
if let data = responseObject as? Data {
responseData = data
} else {
owsFailDebug("Invalid response: \(type(of: responseObject))")
}
}
let response = HTTPResponseImpl.build(requestUrl: requestUrl,
httpUrlResponse: httpUrlResponse,
bodyData: responseData)
success(response)
}
let afFailure = { (task: URLSessionDataTask?, error: Error) in
let responseHeaders = parseResponseHeaders(task: task)
// TODO: Can we extract a response body?
let responseData: Data? = nil
let responseStatus: Int
if let response = parseResponse(task: task) {
responseStatus = response.statusCode
} else {
responseStatus = 0
}
let error = HTTPUtils.preprocessMainServiceHTTPError(request: request,
requestUrl: requestUrl,
responseStatus: responseStatus,
responseHeaders: responseHeaders,
responseError: error,
responseData: responseData)
failure(OWSHTTPErrorWrapper(error: error))
}
switch request.httpMethod {
case "GET":
sessionManager.get(requestUrlString,
parameters: request.parameters,
progress: nil,
success: afSuccess,
failure: afFailure)
case "POST":
sessionManager.post(requestUrlString,
parameters: request.parameters,
progress: nil,
success: afSuccess,
failure: afFailure)
case "PUT":
sessionManager.put(requestUrlString,
parameters: request.parameters,
success: afSuccess,
failure: afFailure)
case "DELETE":
sessionManager.delete(requestUrlString,
parameters: request.parameters,
success: afSuccess,
failure: afFailure)
default:
owsFailDebug("Invalid request.")
failure(OWSHTTPErrorWrapper(error: .invalidRequest(requestUrl: rawRequestUrl)))
}
}
}