From 5eaeeff83878924ca98e5513621d92e1cae094c1 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 25 Feb 2019 14:19:29 -0500 Subject: [PATCH 1/3] Use content proxy to configure all proxied content requests. --- Signal/src/network/GiphyAPI.swift | 6 ++ .../Interactions/OWSLinkPreview.swift | 14 +-- .../src/Network/ContentProxy.swift | 100 +++++++++++++++++- .../Network/ProxiedContentDownloader.swift | 80 +++----------- 4 files changed, 128 insertions(+), 72 deletions(-) diff --git a/Signal/src/network/GiphyAPI.swift b/Signal/src/network/GiphyAPI.swift index 03e082949..462fc59e1 100644 --- a/Signal/src/network/GiphyAPI.swift +++ b/Signal/src/network/GiphyAPI.swift @@ -310,6 +310,12 @@ extension GiphyError: LocalizedError { } let urlString = "/v1/gifs/search?api_key=\(kGiphyApiKey)&offset=\(kGiphyPageOffset)&limit=\(kGiphyPageSize)&q=\(queryEncoded)" + guard ContentProxy.configureSessionManager(sessionManager: sessionManager, forUrl: urlString) else { + owsFailDebug("Could not configure query: \(query).") + failure(nil) + return + } + sessionManager.get(urlString, parameters: [String: AnyObject](), progress: nil, diff --git a/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift b/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift index 01d416d77..556505ccc 100644 --- a/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift +++ b/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift @@ -589,10 +589,10 @@ public class OWSLinkPreview: MTLModel { } } - class func downloadLink(url: String, + class func downloadLink(url urlString: String, remainingRetries: UInt = 3) -> Promise { - Logger.verbose("url: \(url)") + Logger.verbose("url: \(urlString)") let sessionConfiguration = ContentProxy.sessionConfiguration() @@ -605,13 +605,13 @@ public class OWSLinkPreview: MTLModel { sessionManager.requestSerializer = AFHTTPRequestSerializer() sessionManager.responseSerializer = AFHTTPResponseSerializer() - // Remove all headers from the request. - for headerField in sessionManager.requestSerializer.httpRequestHeaders.keys { - sessionManager.requestSerializer.setValue(nil, forHTTPHeaderField: headerField) + guard ContentProxy.configureSessionManager(sessionManager: sessionManager, forUrl: urlString) else { + owsFailDebug("Could not configure url: \(urlString).") + return Promise(error: LinkPreviewError.assertionFailure) } let (promise, resolver) = Promise.pending() - sessionManager.get(url, + sessionManager.get(urlString, parameters: [String: AnyObject](), progress: nil, success: { task, value in @@ -654,7 +654,7 @@ public class OWSLinkPreview: MTLModel { resolver.reject(LinkPreviewError.couldNotDownload) return } - OWSLinkPreview.downloadLink(url: url, remainingRetries: remainingRetries - 1) + OWSLinkPreview.downloadLink(url: urlString, remainingRetries: remainingRetries - 1) .done(on: DispatchQueue.global()) { (data) in resolver.fulfill(data) }.catch(on: DispatchQueue.global()) { (error) in diff --git a/SignalServiceKit/src/Network/ContentProxy.swift b/SignalServiceKit/src/Network/ContentProxy.swift index 098a9e557..d76b37213 100644 --- a/SignalServiceKit/src/Network/ContentProxy.swift +++ b/SignalServiceKit/src/Network/ContentProxy.swift @@ -51,4 +51,102 @@ public class ContentProxy: NSObject { sessionManager.responseSerializer = AFJSONResponseSerializer() return sessionManager } -} + + static let userAgent = "Signal iOS (+https://signal.org/download)" + + public class func configureProxiedRequest(request: inout URLRequest) -> Bool { + request.addValue(userAgent, forHTTPHeaderField: "User-Agent") + + padRequestSize(request: &request) + + guard let url = request.url, + let scheme = url.scheme, + scheme.lowercased() == "https" else { + return false + } + return true + } + + @objc + public class func configureSessionManager(sessionManager: AFHTTPSessionManager, + forUrl urlString: String) -> Bool { + + guard let url = URL(string: urlString, relativeTo: sessionManager.baseURL) else { + owsFailDebug("Invalid URL query: \(urlString).") + return false + } + + var request = URLRequest(url: url) + + guard configureProxiedRequest(request: &request) else { + owsFailDebug("Invalid URL query: \(urlString).") + return false + } + + // Remove all headers from the request. + for headerField in sessionManager.requestSerializer.httpRequestHeaders.keys { + sessionManager.requestSerializer.setValue(nil, forHTTPHeaderField: headerField) + } + // Honor the request's headers. + if let allHTTPHeaderFields = request.allHTTPHeaderFields { + for (headerField, headerValue) in allHTTPHeaderFields { + sessionManager.requestSerializer.setValue(headerValue, forHTTPHeaderField: headerField) + } + } + return true + } + + public class func padRequestSize(request: inout URLRequest) { + guard let sizeEstimate: UInt = estimateRequestSize(request: request) else { + owsFailDebug("Could not estimate request size.") + return + } + // We pad the estimated size to an even multiple of paddingQuantum (plus the + // extra ": " and "\r\n"). The exact size doesn't matter so long as the + // padding is consistent. + let paddingQuantum: UInt = 1024 + let paddingSize = paddingQuantum - (sizeEstimate % paddingQuantum) + let padding = String(repeating: ".", count: Int(paddingSize)) + request.addValue(padding, forHTTPHeaderField: "X-SignalPadding") + } + + private class func estimateRequestSize(request: URLRequest) -> UInt? { + // iOS doesn't offer an exact way to measure request sizes on the wire, + // but we can reliably estimate request sizes using the "knowns", e.g. + // HTTP method, path, querystring, headers. The "unknowns" should be + // consistent between requests. + + guard let url = request.url?.absoluteString else { + owsFailDebug("Request missing URL.") + return nil + } + guard let components = URLComponents(string: url) else { + owsFailDebug("Request has invalid URL.") + return nil + } + + var result: Int = 0 + + if let httpMethod = request.httpMethod { + result += httpMethod.count + } + result += components.percentEncodedPath.count + if let percentEncodedQuery = components.percentEncodedQuery { + result += percentEncodedQuery.count + } + if let allHTTPHeaderFields = request.allHTTPHeaderFields { + for (key, value) in allHTTPHeaderFields { + // Each header has 4 extra bytes: + // + // * Two for the key/value separator ": " + // * Two for "\r\n", the line break in the HTTP protocol spec. + result += key.count + value.count + 4 + } + } else { + owsFailDebug("Request has no headers.") + } + if let httpBody = request.httpBody { + result += httpBody.count + } + return UInt(result) + }} diff --git a/SignalServiceKit/src/Network/ProxiedContentDownloader.swift b/SignalServiceKit/src/Network/ProxiedContentDownloader.swift index 3d13e3830..5b6d2f23e 100644 --- a/SignalServiceKit/src/Network/ProxiedContentDownloader.swift +++ b/SignalServiceKit/src/Network/ProxiedContentDownloader.swift @@ -657,8 +657,6 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio return } - let userAgent = "Signal iOS (+https://signal.org/download)" - if assetRequest.state == .waiting { // If asset request hasn't yet determined the resource size, // try to do so now, by requesting a small initial segment. @@ -671,8 +669,14 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio request.httpShouldUsePipelining = true let rangeHeaderValue = "bytes=\(segmentStart)-\(segmentStart + segmentLength - 1)" request.addValue(rangeHeaderValue, forHTTPHeaderField: "Range") - request.addValue(userAgent, forHTTPHeaderField: "User-Agent") - padRequestSize(request: &request) + + guard ContentProxy.configureProxiedRequest(request: &request) else { + assetRequest.state = .failed + assetRequestDidFail(assetRequest: assetRequest) + processRequestQueueSync() + return + } + let task = downloadSession.dataTask(with: request, completionHandler: { data, response, error -> Void in self.handleAssetSizeResponse(assetRequest: assetRequest, data: data, response: response, error: error) }) @@ -692,8 +696,14 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio request.httpShouldUsePipelining = true let rangeHeaderValue = "bytes=\(assetSegment.segmentStart)-\(assetSegment.segmentStart + assetSegment.segmentLength - 1)" request.addValue(rangeHeaderValue, forHTTPHeaderField: "Range") - request.addValue(userAgent, forHTTPHeaderField: "User-Agent") - padRequestSize(request: &request) + + guard ContentProxy.configureProxiedRequest(request: &request) else { + assetRequest.state = .failed + assetRequestDidFail(assetRequest: assetRequest) + processRequestQueueSync() + return + } + let task: URLSessionDataTask = downloadSession.dataTask(with: request) task.assetRequest = assetRequest task.assetSegment = assetSegment @@ -705,64 +715,6 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio processRequestQueueSync() } - private func padRequestSize(request: inout URLRequest) { - guard let sizeEstimate: UInt = estimateRequestSize(request: request) else { - owsFailDebug("Could not estimate request size.") - return - } - // We pad the estimated size to an even multiple of paddingQuantum (plus the - // extra ": " and "\r\n"). The exact size doesn't matter so long as the - // padding is consistent. - let paddingQuantum: UInt = 1024 - let paddingSize = paddingQuantum - (sizeEstimate % paddingQuantum) - let padding = String(repeating: ".", count: Int(paddingSize)) - request.addValue(padding, forHTTPHeaderField: "X-SignalPadding") - } - - private func estimateRequestSize(request: URLRequest) -> UInt? { - // iOS doesn't offer an exact way to measure request sizes on the wire, - // but we can reliably estimate request sizes using the "knowns", e.g. - // HTTP method, path, querystring, headers. The "unknowns" should be - // consistent between requests. - - guard let url = request.url?.absoluteString else { - owsFailDebug("Request missing URL.") - return nil - } - guard let components = URLComponents(string: url) else { - owsFailDebug("Request has invalid URL.") - return nil - } - - var result: Int = 0 - - if let httpMethod = request.httpMethod { - result += httpMethod.count - } - result += components.percentEncodedPath.count - if let percentEncodedQuery = components.percentEncodedQuery { - result += percentEncodedQuery.count - } - if let allHTTPHeaderFields = request.allHTTPHeaderFields { - if allHTTPHeaderFields.count != 2 { - owsFailDebug("Request has unexpected number of headers.") - } - for (key, value) in allHTTPHeaderFields { - // Each header has 4 extra bytes: - // - // * Two for the key/value separator ": " - // * Two for "\r\n", the line break in the HTTP protocol spec. - result += key.count + value.count + 4 - } - } else { - owsFailDebug("Request has no headers.") - } - if let httpBody = request.httpBody { - result += httpBody.count - } - return UInt(result) - } - private func handleAssetSizeResponse(assetRequest: ProxiedContentAssetRequest, data: Data?, response: URLResponse?, error: Error?) { guard error == nil else { assetRequest.state = .failed From ad90a8e0c483bcad0864dbfebbed534c049f400e Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 25 Feb 2019 14:19:51 -0500 Subject: [PATCH 2/3] Use content proxy to configure all proxied content requests. --- SignalServiceKit/src/Network/ContentProxy.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SignalServiceKit/src/Network/ContentProxy.swift b/SignalServiceKit/src/Network/ContentProxy.swift index d76b37213..576fd876c 100644 --- a/SignalServiceKit/src/Network/ContentProxy.swift +++ b/SignalServiceKit/src/Network/ContentProxy.swift @@ -17,7 +17,7 @@ public class ContentProxy: NSObject { let proxyHost = "contentproxy.signal.org" let proxyPort = 443 configuration.connectionProxyDictionary = [ - "HTTPEnable": 1, + "HTTPEnable": 0, "HTTPProxy": proxyHost, "HTTPPort": proxyPort, "HTTPSEnable": 1, From fff93f8bb2661c37035e15973d96fd94fb0a6f8e Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 25 Feb 2019 15:08:19 -0500 Subject: [PATCH 3/3] Use content proxy to configure all proxied content requests. --- SignalServiceKit/src/Network/ContentProxy.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/SignalServiceKit/src/Network/ContentProxy.swift b/SignalServiceKit/src/Network/ContentProxy.swift index 576fd876c..6dd135b79 100644 --- a/SignalServiceKit/src/Network/ContentProxy.swift +++ b/SignalServiceKit/src/Network/ContentProxy.swift @@ -17,7 +17,7 @@ public class ContentProxy: NSObject { let proxyHost = "contentproxy.signal.org" let proxyPort = 443 configuration.connectionProxyDictionary = [ - "HTTPEnable": 0, + "HTTPEnable": 1, "HTTPProxy": proxyHost, "HTTPPort": proxyPort, "HTTPSEnable": 1, @@ -67,6 +67,11 @@ public class ContentProxy: NSObject { return true } + // This mutates the session manager state, so its the caller's obligation to avoid conflicts by: + // + // * Using a new session manager for each request. + // * Pooling session managers. + // * Using a single session manager on a single queue. @objc public class func configureSessionManager(sessionManager: AFHTTPSessionManager, forUrl urlString: String) -> Bool {