diff --git a/CHANGELOG.md b/CHANGELOG.md index f89a77f5..1b840e35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## 0.7.0 [unreleased] +### Features +1. [#38](https://github.com/influxdata/influxdb-client-swift/pull/38): Add configuration option for _Proxy_ and _Redirects_ + ## 0.6.0 [2021-07-09] ### API diff --git a/README.md b/README.md index 769ca386..b2a5a139 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ This repository contains the reference Swift client for the InfluxDB 2.0. - [Management API](#management-api) - [Advanced Usage](#advanced-usage) - [Default Tags](#default-tags) + - [Proxy and redirects](#proxy-and-redirects) - [Contributing](#contributing) - [License](#license) @@ -660,6 +661,54 @@ mining,customer=California\ Miner,sensor_id=123-456-789,sensor_state=normal dept mining,customer=California\ Miner,sensor_id=123-456-789,sensor_state=normal pressure=3i ``` +### Proxy and redirects + +> :warning: The `connectionProxyDictionary` cannot be defined on **Linux**. You have to set `HTTPS_PROXY` or `HTTP_PROXY` system environment. + +You can configure the client to tunnel requests through an HTTP proxy by `connectionProxyDictionary` option: + +```swift +var connectionProxyDictionary = [AnyHashable: Any]() +connectionProxyDictionary[kCFNetworkProxiesHTTPEnable as String] = 1 +connectionProxyDictionary[kCFNetworkProxiesHTTPProxy as String] = "localhost" +connectionProxyDictionary[kCFNetworkProxiesHTTPPort as String] = 3128 + +let options: InfluxDBClient.InfluxDBOptions = InfluxDBClient.InfluxDBOptions( + bucket: "my-bucket", + org: "my-org", + precision: .ns, + connectionProxyDictionary: connectionProxyDictionary) + +client = InfluxDBClient(url: "http://localhost:8086", token: "my-token", options: options) +``` +For more info see - [URLSessionConfiguration.connectionProxyDictionary](https://developer.apple.com/documentation/foundation/urlsessionconfiguration/1411499-connectionproxydictionary), [Global Proxy Settings Constants](https://developer.apple.com/documentation/cfnetwork/global_proxy_settings_constants/). + +#### Redirects + +Client automatically follows HTTP redirects. You can disable redirects by an `urlSessionDelegate` configuration: + +```swift +class DisableRedirect: NSObject, URLSessionTaskDelegate { + func urlSession(_ session: URLSession, + task: URLSessionTask, + willPerformHTTPRedirection response: HTTPURLResponse, + newRequest request: URLRequest, + completionHandler: @escaping (URLRequest?) -> Void) { + completionHandler(nil) + } +} + +let options = InfluxDBClient.InfluxDBOptions( + bucket: "my-bucket", + org: "my-org", + urlSessionDelegate: DisableRedirect()) + +client = InfluxDBClient(url: "http://localhost:8086", token: "my-token", options: options) +``` + +For more info see - [URLSessionDelegate](https://developer.apple.com/documentation/foundation/urlsessiondelegate). + + ## Contributing If you would like to contribute code you can do through GitHub by forking the repository and sending a pull request into the `master` branch. diff --git a/Sources/InfluxDBSwift/InfluxDBClient.swift b/Sources/InfluxDBSwift/InfluxDBClient.swift index 7d19de79..098753e2 100644 --- a/Sources/InfluxDBSwift/InfluxDBClient.swift +++ b/Sources/InfluxDBSwift/InfluxDBClient.swift @@ -63,9 +63,14 @@ public class InfluxDBClient { configuration.httpAdditionalHeaders = headers configuration.timeoutIntervalForRequest = self.options.timeoutIntervalForRequest configuration.timeoutIntervalForResource = self.options.timeoutIntervalForResource + configuration.connectionProxyDictionary = self.options.connectionProxyDictionary configuration.protocolClasses = protocolClasses - session = URLSession(configuration: configuration) + if let urlSessionDelegate = self.options.urlSessionDelegate { + session = URLSession(configuration: configuration, delegate: urlSessionDelegate, delegateQueue: nil) + } else { + session = URLSession(configuration: configuration) + } } /// Create a new client for InfluxDB 1.8 compatibility API. @@ -130,6 +135,13 @@ extension InfluxDBClient { /// - SeeAlso: https://docs.influxdata.com/influxdb/v2.0/api/#operation/PostWrite /// - SeeAlso: https://docs.influxdata.com/influxdb/v2.0/api/#operation/PostQuery public let enableGzip: Bool + /// A dictionary containing information about the proxy to use within the HTTP client. + /// - SeeAlso: https://developer.apple.com/documentation/foundation/urlsessionconfiguration/ + /// - SeeAlso: https://developer.apple.com/documentation/cfnetwork/global_proxy_settings_constants/ + public let connectionProxyDictionary: [AnyHashable: Any]? + /// A delegate to handle HTTP session-level events. Useful for disable redirects or custom auth handling. + /// - SeeAlso: https://developer.apple.com/documentation/foundation/urlsessiondelegate + public weak var urlSessionDelegate: URLSessionDelegate? /// Create a new options for client. /// @@ -140,18 +152,24 @@ extension InfluxDBClient { /// - timeoutIntervalForRequest: Timeout interval to use when waiting for additional data. /// - timeoutIntervalForResource: Maximum amount of time that a resource request should be allowed to take. /// - enableGzip: Enable Gzip compression for HTTP requests. + /// - connectionProxyDictionary: Enable Gzip compression for HTTP requests. + /// - urlSessionDelegate: A delegate to handle HTTP session-level events. public init(bucket: String? = nil, org: String? = nil, precision: TimestampPrecision = defaultTimestampPrecision, timeoutIntervalForRequest: TimeInterval = 60, timeoutIntervalForResource: TimeInterval = 60 * 5, - enableGzip: Bool = false) { + enableGzip: Bool = false, + connectionProxyDictionary: [AnyHashable: Any]? = nil, + urlSessionDelegate: URLSessionDelegate? = nil) { self.bucket = bucket self.org = org self.precision = precision self.timeoutIntervalForRequest = timeoutIntervalForRequest self.timeoutIntervalForResource = timeoutIntervalForResource self.enableGzip = enableGzip + self.connectionProxyDictionary = connectionProxyDictionary + self.urlSessionDelegate = urlSessionDelegate } } } diff --git a/Tests/InfluxDBSwiftTests/InfluxDBClientTests.swift b/Tests/InfluxDBSwiftTests/InfluxDBClientTests.swift index 4091ba65..14c2d3c1 100644 --- a/Tests/InfluxDBSwiftTests/InfluxDBClientTests.swift +++ b/Tests/InfluxDBSwiftTests/InfluxDBClientTests.swift @@ -74,6 +74,107 @@ final class InfluxDBClientTests: XCTestCase { XCTAssertEqual(Self.dbURL(), client.url) client.close() } + + func testConfigureProxy() { + #if os(macOS) + var connectionProxyDictionary = [AnyHashable: Any]() + connectionProxyDictionary[kCFNetworkProxiesHTTPEnable as String] = 1 + connectionProxyDictionary[kCFNetworkProxiesHTTPProxy as String] = "localhost" + connectionProxyDictionary[kCFNetworkProxiesHTTPPort as String] = 3128 + + let options: InfluxDBClient.InfluxDBOptions = InfluxDBClient.InfluxDBOptions( + bucket: "my-bucket", + org: "my-org", + precision: .ns, + connectionProxyDictionary: connectionProxyDictionary) + + client = InfluxDBClient(url: "http://localhost:8086", token: "my-token", options: options) + #endif + } + + func testFollowRedirect() { + client = InfluxDBClient( + url: Self.dbURL(), + token: "my-token", + options: InfluxDBClient.InfluxDBOptions(bucket: "my-bucket", org: "my-org"), + protocolClasses: [MockURLProtocol.self]) + + let expectation = self.expectation(description: "Success response from API doesn't arrive") + expectation.expectedFulfillmentCount = 3 + + MockURLProtocol.handler = { request, _ in + XCTAssertEqual("Token my-token", request.allHTTPHeaderFields!["Authorization"]) + + expectation.fulfill() + + // success + if let port = request.url?.port, port == 8088 { + let response = HTTPURLResponse(statusCode: 200) + return (response, "".data(using: .utf8)!) + } + + // redirect + let response = HTTPURLResponse(statusCode: 307, headers: ["location": "http://localhost:8088"]) + return (response, Data()) + } + + client.queryAPI.query(query: "from ...") { _, error in + if let error = error { + XCTFail("Error occurs: \(error)") + } + expectation.fulfill() + } + + waitForExpectations(timeout: 1, handler: nil) + } + + func testDisableRedirect() { + let expectation = self.expectation(description: "Redirect response from API doesn't arrive") + expectation.expectedFulfillmentCount = 2 + + class DisableRedirect: NSObject, URLSessionTaskDelegate { + let expectation: XCTestExpectation + + init(_ expectation: XCTestExpectation) { + self.expectation = expectation + } + + func urlSession(_ session: URLSession, + task: URLSessionTask, + willPerformHTTPRedirection response: HTTPURLResponse, + newRequest request: URLRequest, + completionHandler: @escaping (URLRequest?) -> Void) { + expectation.fulfill() + completionHandler(nil) + } + } + + let options = InfluxDBClient.InfluxDBOptions( + bucket: "my-bucket", + org: "my-org", + urlSessionDelegate: DisableRedirect(expectation)) + + client = InfluxDBClient( + url: Self.dbURL(), + token: "my-token", + options: options, + protocolClasses: [MockURLProtocol.self]) + + MockURLProtocol.handler = { request, _ in + XCTAssertEqual("Token my-token", request.allHTTPHeaderFields!["Authorization"]) + + expectation.fulfill() + + // redirect + let response = HTTPURLResponse(statusCode: 307, headers: ["location": "http://localhost:8088"]) + return (response, Data()) + } + + client.queryAPI.query(query: "from ...") { _, _ in + } + + waitForExpectations(timeout: 1, handler: nil) + } } final class InfluxDBErrorTests: XCTestCase { diff --git a/Tests/InfluxDBSwiftTests/MockHTTP.swift b/Tests/InfluxDBSwiftTests/MockHTTP.swift index 915a8d85..9c49dab8 100644 --- a/Tests/InfluxDBSwiftTests/MockHTTP.swift +++ b/Tests/InfluxDBSwiftTests/MockHTTP.swift @@ -8,6 +8,10 @@ extension HTTPURLResponse { convenience init(statusCode: Int) { self.init(url: MockURLProtocol.url, statusCode: statusCode, httpVersion: "HTTP/1.1", headerFields: [:])! } + + convenience init(statusCode: Int, headers: [String: String]?) { + self.init(url: MockURLProtocol.url, statusCode: statusCode, httpVersion: "HTTP/1.1", headerFields: headers)! + } } class MockURLProtocol: URLProtocol { @@ -31,9 +35,14 @@ class MockURLProtocol: URLProtocol { do { let (response, data) = try handler(request, request.bodyValue) - client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) - client?.urlProtocol(self, didLoad: data) - client?.urlProtocolDidFinishLoading(self) + let locationHeader = response.allHeaderFields["Location"] + if let location = locationHeader as? String, let url = URL(string: location), response.statusCode == 307 { + client?.urlProtocol(self, wasRedirectedTo: URLRequest(url: url), redirectResponse: response) + } else { + client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + client?.urlProtocol(self, didLoad: data) + client?.urlProtocolDidFinishLoading(self) + } } catch { client?.urlProtocol(self, didFailWithError: error) }