diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a93cdd016..1137531955 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,27 @@ appRunner: () => runApp(MyApp()), ); ``` +- Add proxy support ([#2192](https://github.com/getsentry/sentry-dart/pull/2192)) + - Configure a `SentryProxy` object and set it on `SentryFlutter.init` + ```dart + import 'package:flutter/widgets.dart'; + import 'package:sentry_flutter/sentry_flutter.dart'; + + Future main() async { + await SentryFlutter.init( + (options) { + options.dsn = 'https://example@sentry.io/add-your-dsn-here'; + options.proxy = SentryProxy( + type: SenryProxyType.http, + host: 'localhost', + port: 8080, + ); + }, + // Init your App. + appRunner: () => runApp(MyApp()), + ); + } + ``` ### Improvements @@ -72,7 +93,7 @@ SentryFlutter.init((options) => - This allows viewing the correct dart formatted raw stacktrace in the Sentry UI - Support `ignoredExceptionsForType` ([#2150](https://github.com/getsentry/sentry-dart/pull/2150)) - Filter out exception types by calling `SentryOptions.addExceptionFilterForType(Type exceptionType)` - + ### Fixes - Disable sff & frame delay detection on web, linux and windows ([#2182](https://github.com/getsentry/sentry-dart/pull/2182)) diff --git a/dart/lib/sentry.dart b/dart/lib/sentry.dart index a43fc08fc6..e9cae9d666 100644 --- a/dart/lib/sentry.dart +++ b/dart/lib/sentry.dart @@ -56,3 +56,5 @@ export 'src/sentry_span_operations.dart'; export 'src/utils.dart'; // spotlight debugging export 'src/spotlight.dart'; +// proxy +export 'src/protocol/sentry_proxy.dart'; diff --git a/dart/lib/src/http_client/client_provider.dart b/dart/lib/src/http_client/client_provider.dart new file mode 100644 index 0000000000..201a559601 --- /dev/null +++ b/dart/lib/src/http_client/client_provider.dart @@ -0,0 +1,16 @@ +import 'package:meta/meta.dart'; +import 'package:http/http.dart'; + +import '../sentry_options.dart'; + +@internal +ClientProvider getClientProvider() { + return ClientProvider(); +} + +@internal +class ClientProvider { + Client getClient(SentryOptions options) { + return Client(); + } +} diff --git a/dart/lib/src/http_client/io_client_provider.dart b/dart/lib/src/http_client/io_client_provider.dart new file mode 100644 index 0000000000..b8ec366d06 --- /dev/null +++ b/dart/lib/src/http_client/io_client_provider.dart @@ -0,0 +1,67 @@ +import 'dart:io'; + +import 'package:http/http.dart'; +import 'package:http/io_client.dart'; +import 'package:meta/meta.dart'; + +import '../protocol.dart'; +import '../protocol/sentry_proxy.dart'; +import '../sentry_options.dart'; +import 'client_provider.dart'; + +@internal +ClientProvider getClientProvider() { + return IoClientProvider( + () { + return HttpClient(); + }, + (user, pass) { + return HttpClientBasicCredentials(user, pass); + }, + ); +} + +@internal +class IoClientProvider implements ClientProvider { + final HttpClient Function() _httpClient; + final HttpClientCredentials Function(String, String) _httpClientCredentials; + + IoClientProvider(this._httpClient, this._httpClientCredentials); + + @override + Client getClient(SentryOptions options) { + final proxy = options.proxy; + if (proxy == null) { + return Client(); + } + final pac = proxy.toPacString(); + if (proxy.type == SentryProxyType.socks) { + options.logger( + SentryLevel.warning, + "Setting proxy '$pac' is not supported.", + ); + return Client(); + } + options.logger( + SentryLevel.info, + "Setting proxy '$pac'", + ); + final httpClient = _httpClient(); + httpClient.findProxy = (url) => pac; + + final host = proxy.host; + final port = proxy.port; + final user = proxy.user; + final pass = proxy.pass; + + if (host != null && port != null && user != null && pass != null) { + httpClient.addProxyCredentials( + host, + port, + '', + _httpClientCredentials(user, pass), + ); + } + return IOClient(httpClient); + } +} diff --git a/dart/lib/src/protocol/sentry_proxy.dart b/dart/lib/src/protocol/sentry_proxy.dart new file mode 100644 index 0000000000..7dcaefef22 --- /dev/null +++ b/dart/lib/src/protocol/sentry_proxy.dart @@ -0,0 +1,62 @@ +class SentryProxy { + final SentryProxyType type; + final String? host; + final int? port; + final String? user; + final String? pass; + + SentryProxy({required this.type, this.host, this.port, this.user, this.pass}); + + String toPacString() { + String type = 'DIRECT'; + switch (this.type) { + case SentryProxyType.direct: + return 'DIRECT'; + case SentryProxyType.http: + type = 'PROXY'; + break; + case SentryProxyType.socks: + type = 'SOCKS'; + break; + } + if (host != null && port != null) { + return '$type $host:$port'; + } else if (host != null) { + return '$type $host'; + } else { + return 'DIRECT'; + } + } + + /// Produces a [Map] that can be serialized to JSON. + Map toJson() { + return { + if (host != null) 'host': host, + if (port != null) 'port': port, + 'type': type.toString().split('.').last.toUpperCase(), + if (user != null) 'user': user, + if (pass != null) 'pass': pass, + }; + } + + SentryProxy copyWith({ + String? host, + int? port, + SentryProxyType? type, + String? user, + String? pass, + }) => + SentryProxy( + host: host ?? this.host, + port: port ?? this.port, + type: type ?? this.type, + user: user ?? this.user, + pass: pass ?? this.pass, + ); +} + +enum SentryProxyType { + direct, + http, + socks; +} diff --git a/dart/lib/src/sentry_options.dart b/dart/lib/src/sentry_options.dart index 26cda852c4..2b1771a2b5 100644 --- a/dart/lib/src/sentry_options.dart +++ b/dart/lib/src/sentry_options.dart @@ -496,6 +496,19 @@ class SentryOptions { /// ``` Spotlight spotlight = Spotlight(enabled: false); + /// Configure a proxy to use for SDK API calls. + /// + /// On io platforms without native SDKs (dart, linux, windows), this will use + /// an 'IOClient' with inner 'HTTPClient' for http communication. + /// A http proxy will be set in returned for 'HttpClient.findProxy' in the + /// form 'PROXY :'. + /// When setting 'user' and 'pass', the 'HttpClient.addProxyCredentials' + /// method will be called with empty 'realm'. + /// + /// On Android & iOS, the proxy settings are handled by the native SDK. + /// iOS only supports http proxies, while macOS also supports socks. + SentryProxy? proxy; + SentryOptions({this.dsn, PlatformChecker? checker}) { if (checker != null) { platformChecker = checker; diff --git a/dart/lib/src/transport/http_transport.dart b/dart/lib/src/transport/http_transport.dart index 90dd8949ce..68950948a2 100644 --- a/dart/lib/src/transport/http_transport.dart +++ b/dart/lib/src/transport/http_transport.dart @@ -11,6 +11,8 @@ import '../sentry_options.dart'; import '../sentry_envelope.dart'; import 'transport.dart'; import 'rate_limiter.dart'; +import '../http_client/client_provider.dart' + if (dart.library.io) '../http_client/io_client_provider.dart'; /// A transport is in charge of sending the event to the Sentry server. class HttpTransport implements Transport { @@ -22,9 +24,8 @@ class HttpTransport implements Transport { factory HttpTransport(SentryOptions options, RateLimiter rateLimiter) { if (options.httpClient is NoOpClient) { - options.httpClient = Client(); + options.httpClient = getClientProvider().getClient(options); } - return HttpTransport._(options, rateLimiter); } diff --git a/dart/lib/src/transport/spotlight_http_transport.dart b/dart/lib/src/transport/spotlight_http_transport.dart index f51e77d478..7567039d82 100644 --- a/dart/lib/src/transport/spotlight_http_transport.dart +++ b/dart/lib/src/transport/spotlight_http_transport.dart @@ -4,6 +4,8 @@ import 'http_transport_request_handler.dart'; import '../../sentry.dart'; import '../noop_client.dart'; +import '../http_client/client_provider.dart' + if (dart.library.io) '../http_client/io_client_provider.dart'; /// Spotlight HTTP transport decorator that sends Sentry envelopes to both Sentry and Spotlight. class SpotlightHttpTransport extends Transport { @@ -13,7 +15,7 @@ class SpotlightHttpTransport extends Transport { factory SpotlightHttpTransport(SentryOptions options, Transport transport) { if (options.httpClient is NoOpClient) { - options.httpClient = Client(); + options.httpClient = getClientProvider().getClient(options); } return SpotlightHttpTransport._(options, transport); } diff --git a/dart/test/http_client/io_client_provider_test.dart b/dart/test/http_client/io_client_provider_test.dart new file mode 100644 index 0000000000..0672e84c08 --- /dev/null +++ b/dart/test/http_client/io_client_provider_test.dart @@ -0,0 +1,267 @@ +@TestOn('vm') +library dart_test; + +import 'dart:io'; + +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/http_client/io_client_provider.dart'; +import 'package:test/test.dart'; + +import '../mocks.dart'; + +void main() { + group('getClient', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + test('http proxy should call findProxyResult', () async { + fixture.options.proxy = SentryProxy( + type: SentryProxyType.http, + host: 'localhost', + port: 8080, + ); + + final sut = fixture.getSut(); + sut.getClient(fixture.options); + + expect(fixture.mockHttpClient.findProxyResult, + equals(fixture.options.proxy?.toPacString())); + }); + + test('direct proxy should call findProxyResult', () async { + fixture.options.proxy = SentryProxy(type: SentryProxyType.direct); + + final sut = fixture.getSut(); + sut.getClient(fixture.options); + + expect(fixture.mockHttpClient.findProxyResult, + equals(fixture.options.proxy?.toPacString())); + }); + + test('socks proxy should not call findProxyResult', () async { + fixture.options.proxy = SentryProxy( + type: SentryProxyType.socks, host: 'localhost', port: 8080); + + final sut = fixture.getSut(); + sut.getClient(fixture.options); + + expect(fixture.mockHttpClient.findProxyResult, isNull); + }); + + test('authenticated proxy http should call addProxyCredentials', () async { + fixture.options.proxy = SentryProxy( + type: SentryProxyType.http, + host: 'localhost', + port: 8080, + user: 'admin', + pass: '0000', + ); + + final sut = fixture.getSut(); + + sut.getClient(fixture.options); + + expect(fixture.mockHttpClient.addProxyCredentialsHost, + fixture.options.proxy?.host); + expect(fixture.mockHttpClient.addProxyCredentialsPort, + fixture.options.proxy?.port); + expect(fixture.mockHttpClient.addProxyCredentialsRealm, ''); + expect(fixture.mockUser, fixture.options.proxy?.user); + expect(fixture.mockPass, fixture.options.proxy?.pass); + expect(fixture.mockHttpClient.addProxyCredentialsBasic, isNotNull); + }); + }); +} + +class Fixture { + final options = SentryOptions(dsn: fakeDsn); + final mockHttpClient = MockHttpClient(); + + String? mockUser; + String? mockPass; + + IoClientProvider getSut() { + return IoClientProvider( + () { + return mockHttpClient; + }, + (user, pass) { + mockUser = user; + mockPass = pass; + return HttpClientBasicCredentials(user, pass); + }, + ); + } +} + +class MockHttpClient implements HttpClient { + @override + bool autoUncompress = false; + + @override + Duration? connectionTimeout; + + @override + Duration idleTimeout = Duration(seconds: 1); + + @override + int? maxConnectionsPerHost; + + @override + String? userAgent; + + @override + void addCredentials( + Uri url, String realm, HttpClientCredentials credentials) { + // TODO: implement addCredentials + } + + String? addProxyCredentialsHost; + int? addProxyCredentialsPort; + String? addProxyCredentialsRealm; + HttpClientBasicCredentials? addProxyCredentialsBasic; + + @override + void addProxyCredentials( + String host, int port, String realm, HttpClientCredentials credentials) { + addProxyCredentialsHost = host; + addProxyCredentialsPort = port; + addProxyCredentialsRealm = realm; + if (credentials is HttpClientBasicCredentials) { + addProxyCredentialsBasic = credentials; + } + } + + @override + set authenticate( + Future Function(Uri url, String scheme, String? realm)? f) { + // TODO: implement authenticate + } + + @override + set authenticateProxy( + Future Function( + String host, int port, String scheme, String? realm)? + f) { + // TODO: implement authenticateProxy + } + + @override + set badCertificateCallback( + bool Function(X509Certificate cert, String host, int port)? callback) { + // TODO: implement badCertificateCallback + } + + @override + void close({bool force = false}) { + // TODO: implement close + } + + @override + set connectionFactory( + Future> Function( + Uri url, String? proxyHost, int? proxyPort)? + f) { + // TODO: implement connectionFactory + } + + @override + Future delete(String host, int port, String path) { + // TODO: implement delete + throw UnimplementedError(); + } + + @override + Future deleteUrl(Uri url) { + // TODO: implement deleteUrl + throw UnimplementedError(); + } + + String? findProxyResult; + + @override + set findProxy(String Function(Uri url)? f) { + findProxyResult = f!(Uri(scheme: "http", host: "localhost", port: 8080)); + } + + @override + Future get(String host, int port, String path) { + // TODO: implement get + throw UnimplementedError(); + } + + @override + Future getUrl(Uri url) { + // TODO: implement getUrl + throw UnimplementedError(); + } + + @override + Future head(String host, int port, String path) { + // TODO: implement head + throw UnimplementedError(); + } + + @override + Future headUrl(Uri url) { + // TODO: implement headUrl + throw UnimplementedError(); + } + + @override + set keyLog(Function(String line)? callback) { + // TODO: implement keyLog + } + + @override + Future open( + String method, String host, int port, String path) { + // TODO: implement open + throw UnimplementedError(); + } + + @override + Future openUrl(String method, Uri url) { + // TODO: implement openUrl + throw UnimplementedError(); + } + + @override + Future patch(String host, int port, String path) { + // TODO: implement patch + throw UnimplementedError(); + } + + @override + Future patchUrl(Uri url) { + // TODO: implement patchUrl + throw UnimplementedError(); + } + + @override + Future post(String host, int port, String path) { + // TODO: implement post + throw UnimplementedError(); + } + + @override + Future postUrl(Uri url) { + // TODO: implement postUrl + throw UnimplementedError(); + } + + @override + Future put(String host, int port, String path) { + // TODO: implement put + throw UnimplementedError(); + } + + @override + Future putUrl(Uri url) { + // TODO: implement putUrl + throw UnimplementedError(); + } +} diff --git a/dart/test/protocol/sentry_proxy_test.dart b/dart/test/protocol/sentry_proxy_test.dart new file mode 100644 index 0000000000..795dc1793d --- /dev/null +++ b/dart/test/protocol/sentry_proxy_test.dart @@ -0,0 +1,102 @@ +import 'package:collection/collection.dart'; +import 'package:sentry/sentry.dart'; +import 'package:test/test.dart'; + +void main() { + final proxy = SentryProxy( + host: 'localhost', + port: 8080, + type: SentryProxyType.http, + user: 'admin', + pass: '0000', + ); + + final proxyJson = { + 'host': 'localhost', + 'port': 8080, + 'type': 'HTTP', + 'user': 'admin', + 'pass': '0000', + }; + + group('toPacString', () { + test('returns "DIRECT" for ProxyType.direct', () { + SentryProxy proxy = SentryProxy(type: SentryProxyType.direct); + expect(proxy.toPacString(), equals('DIRECT')); + }); + + test('returns "PROXY host:port" for ProxyType.http with host and port', () { + SentryProxy proxy = SentryProxy( + type: SentryProxyType.http, host: 'localhost', port: 8080); + expect(proxy.toPacString(), equals('PROXY localhost:8080')); + }); + + test('returns "PROXY host" for ProxyType.http with host only', () { + SentryProxy proxy = + SentryProxy(type: SentryProxyType.http, host: 'localhost'); + expect(proxy.toPacString(), equals('PROXY localhost')); + }); + + test('returns "SOCKS host:port" for ProxyType.socks with host and port', + () { + SentryProxy proxy = SentryProxy( + type: SentryProxyType.socks, host: 'localhost', port: 8080); + expect(proxy.toPacString(), equals('SOCKS localhost:8080')); + }); + + test('returns "SOCKS host" for ProxyType.socks with host only', () { + SentryProxy proxy = + SentryProxy(type: SentryProxyType.socks, host: 'localhost'); + expect(proxy.toPacString(), equals('SOCKS localhost')); + }); + + test('falls back to "DIRECT" if http is missing host', () { + SentryProxy proxy = SentryProxy(type: SentryProxyType.http); + expect(proxy.toPacString(), equals('DIRECT')); + }); + + test('falls back to "DIRECT" if socks is missing host', () { + SentryProxy proxy = SentryProxy(type: SentryProxyType.socks); + expect(proxy.toPacString(), equals('DIRECT')); + }); + }); + + group('json', () { + test('toJson', () { + final json = proxy.toJson(); + + expect( + DeepCollectionEquality().equals(proxyJson, json), + true, + ); + }); + }); + + group('copyWith', () { + test('copyWith keeps unchanged', () { + final data = proxy; + + final copy = data.copyWith(); + + expect(data.toJson(), copy.toJson()); + }); + + test('copyWith takes new values', () { + final data = proxy; + + final copy = data.copyWith( + host: 'localhost-2', + port: 9001, + type: SentryProxyType.socks, + user: 'user', + pass: '1234', + ); + + expect('localhost-2', copy.host); + expect(9001, copy.port); + expect(SentryProxyType.socks, copy.type); + expect('user', copy.user); + expect('1234', copy.pass); + }); + }); +} diff --git a/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutter.kt b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutter.kt index c06a8b0dc2..123778e9be 100644 --- a/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutter.kt +++ b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutter.kt @@ -1,9 +1,12 @@ package io.sentry.flutter +import android.util.Log import io.sentry.SentryLevel +import io.sentry.SentryOptions.Proxy import io.sentry.android.core.BuildConfig import io.sentry.android.core.SentryAndroidOptions import io.sentry.protocol.SdkVersion +import java.net.Proxy.Type import java.util.Locale class SentryFlutter( @@ -119,6 +122,30 @@ class SentryFlutter( data.getIfNotNull("readTimeoutMillis") { options.readTimeoutMillis = it } + data.getIfNotNull>("proxy") { proxyJson -> + options.proxy = + Proxy() + .apply { + host = proxyJson["host"] as? String + port = + (proxyJson["port"] as? Int) + ?.let { + "$it" + } + (proxyJson["type"] as? String) + ?.let { + type = + try { + Type.valueOf(it.toUpperCase(Locale.ROOT)) + } catch (_: IllegalArgumentException) { + Log.w("Sentry", "Could not parse `type` from proxy json: $proxyJson") + null + } + } + user = proxyJson["user"] as? String + pass = proxyJson["pass"] as? String + } + } } } diff --git a/flutter/android/src/test/kotlin/io/sentry/flutter/SentryFlutterTest.kt b/flutter/android/src/test/kotlin/io/sentry/flutter/SentryFlutterTest.kt index 724559bb76..9fa9183f33 100644 --- a/flutter/android/src/test/kotlin/io/sentry/flutter/SentryFlutterTest.kt +++ b/flutter/android/src/test/kotlin/io/sentry/flutter/SentryFlutterTest.kt @@ -6,6 +6,7 @@ import io.sentry.android.core.SentryAndroidOptions import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test +import java.net.Proxy class SentryFlutterTest { private lateinit var fixture: Fixture @@ -60,6 +61,12 @@ class SentryFlutterTest { assertEquals(9006, fixture.options.connectionTimeoutMillis) assertEquals(9007, fixture.options.readTimeoutMillis) + + assertEquals("localhost", fixture.options.proxy?.host) + assertEquals("8080", fixture.options.proxy?.port) + assertEquals(Proxy.Type.HTTP, fixture.options.proxy?.type) + assertEquals("admin", fixture.options.proxy?.user) + assertEquals("0000", fixture.options.proxy?.pass) } @Test @@ -127,12 +134,19 @@ class Fixture { "enableAutoPerformanceTracing" to true, "connectionTimeoutMillis" to 9006, "readTimeoutMillis" to 9007, + "proxy" to + mapOf( + "host" to "localhost", + "port" to 8080, + "type" to "http", // lowercase to check enum mapping + "user" to "admin", + "pass" to "0000", + ), ) - fun getSut(): SentryFlutter { - return SentryFlutter( + fun getSut(): SentryFlutter = + SentryFlutter( androidSdk = "sentry.java.android.flutter", nativeSdk = "fixture-nativeSdk", ) - } } diff --git a/flutter/example/ios/RunnerTests/SentryFlutterTests.swift b/flutter/example/ios/RunnerTests/SentryFlutterTests.swift index 057b2363b5..4873388f2f 100644 --- a/flutter/example/ios/RunnerTests/SentryFlutterTests.swift +++ b/flutter/example/ios/RunnerTests/SentryFlutterTests.swift @@ -9,6 +9,8 @@ import XCTest import sentry_flutter import Sentry +// swiftlint:disable function_body_length line_length + final class SentryFlutterTests: XCTestCase { private var fixture: Fixture! @@ -43,7 +45,14 @@ final class SentryFlutterTests: XCTestCase { "maxAttachmentSize": NSNumber(value: 9004), "captureFailedRequests": false, "enableAppHangTracking": false, - "appHangTimeoutIntervalMillis": NSNumber(value: 10000) + "appHangTimeoutIntervalMillis": NSNumber(value: 10000), + "proxy": [ + "host": "localhost", + "port": NSNumber(value: 8080), + "type": "hTtP", // mixed case to check enum mapping + "user": "admin", + "pass": "0000" + ] ] ) @@ -68,6 +77,41 @@ final class SentryFlutterTests: XCTestCase { XCTAssertEqual(false, fixture.options.enableCaptureFailedRequests) XCTAssertEqual(false, fixture.options.enableAppHangTracking) XCTAssertEqual(10, fixture.options.appHangTimeoutInterval) + + XCTAssertNotNil(fixture.options.urlSession) + XCTAssertEqual(true, fixture.options.urlSession?.configuration.connectionProxyDictionary?[kCFNetworkProxiesHTTPEnable as String] as? Bool) + XCTAssertEqual("localhost", fixture.options.urlSession?.configuration.connectionProxyDictionary?[kCFNetworkProxiesHTTPProxy as String] as? String) + XCTAssertEqual(8080, fixture.options.urlSession?.configuration.connectionProxyDictionary?[kCFNetworkProxiesHTTPPort as String] as? Int) + XCTAssertEqual("admin", fixture.options.urlSession?.configuration.connectionProxyDictionary?[kCFProxyUsernameKey as String] as? String) + XCTAssertEqual("0000", fixture.options.urlSession?.configuration.connectionProxyDictionary?[kCFProxyPasswordKey as String] as? String) + } + + func testUpdateSocksProxy() { + let sut = fixture.getSut() + + sut.update( + options: fixture.options, + with: [ + "proxy": [ + "host": "localhost", + "port": 8080, + "type": "sOcKs", // mixed case to check enum mapping + "user": "admin", + "pass": "0000" + ] + ] + ) + + #if os(macOS) + XCTAssertNotNil(fixture.options.urlSession) + XCTAssertEqual(true, fixture.options.urlSession?.configuration.connectionProxyDictionary?[kCFNetworkProxiesSOCKSEnable as String] as? Bool) + XCTAssertEqual("localhost", fixture.options.urlSession?.configuration.connectionProxyDictionary?[kCFNetworkProxiesSOCKSProxy as String] as? String) + XCTAssertEqual(8080, fixture.options.urlSession?.configuration.connectionProxyDictionary?[kCFNetworkProxiesSOCKSPort as String] as? Int) + XCTAssertEqual("admin", fixture.options.urlSession?.configuration.connectionProxyDictionary?[kCFProxyUsernameKey as String] as? String) + XCTAssertEqual("0000", fixture.options.urlSession?.configuration.connectionProxyDictionary?[kCFProxyPasswordKey as String] as? String) + #else + XCTAssertNil(fixture.options.urlSession) + #endif } } @@ -81,3 +125,5 @@ extension SentryFlutterTests { } } } + +// swiftlint:enable function_body_length line_length diff --git a/flutter/ios/Classes/SentryFlutter.swift b/flutter/ios/Classes/SentryFlutter.swift index b26bcfc30d..769f595fba 100644 --- a/flutter/ios/Classes/SentryFlutter.swift +++ b/flutter/ios/Classes/SentryFlutter.swift @@ -70,6 +70,41 @@ public final class SentryFlutter { if let appHangTimeoutIntervalMillis = data["appHangTimeoutIntervalMillis"] as? NSNumber { options.appHangTimeoutInterval = appHangTimeoutIntervalMillis.doubleValue / 1000 } + if let proxy = data["proxy"] as? [String: Any] { + guard let host = proxy["host"] as? String, + let port = proxy["port"] as? Int, + let type = proxy["type"] as? String else { + print("Could not read proxy data") + return + } + + var connectionProxyDictionary: [String: Any] = [:] + if type.lowercased() == "http" { + connectionProxyDictionary[kCFNetworkProxiesHTTPEnable as String] = true + connectionProxyDictionary[kCFNetworkProxiesHTTPProxy as String] = host + connectionProxyDictionary[kCFNetworkProxiesHTTPPort as String] = port + } else if type.lowercased() == "socks" { + #if os(macOS) + connectionProxyDictionary[kCFNetworkProxiesSOCKSEnable as String] = true + connectionProxyDictionary[kCFNetworkProxiesSOCKSProxy as String] = host + connectionProxyDictionary[kCFNetworkProxiesSOCKSPort as String] = port + #else + return + #endif + } else { + return + } + + if let user = proxy["user"] as? String, let pass = proxy["pass"] { + connectionProxyDictionary[kCFProxyUsernameKey as String] = user + connectionProxyDictionary[kCFProxyPasswordKey as String] = pass + } + + let configuration = URLSessionConfiguration.default + configuration.connectionProxyDictionary = connectionProxyDictionary + + options.urlSession = URLSession(configuration: configuration) + } } private func logLevelFrom(diagnosticLevel: String) -> SentryLevel { diff --git a/flutter/lib/src/native/sentry_native_channel.dart b/flutter/lib/src/native/sentry_native_channel.dart index 0a3b97820d..623111bb9e 100644 --- a/flutter/lib/src/native/sentry_native_channel.dart +++ b/flutter/lib/src/native/sentry_native_channel.dart @@ -66,6 +66,7 @@ class SentryNativeChannel 'readTimeoutMillis': options.readTimeout.inMilliseconds, 'appHangTimeoutIntervalMillis': options.appHangTimeoutInterval.inMilliseconds, + if (options.proxy != null) 'proxy': options.proxy?.toJson(), }); } diff --git a/flutter/test/integrations/init_native_sdk_test.dart b/flutter/test/integrations/init_native_sdk_test.dart index 0dcf3af502..d3faa93346 100644 --- a/flutter/test/integrations/init_native_sdk_test.dart +++ b/flutter/test/integrations/init_native_sdk_test.dart @@ -104,7 +104,14 @@ void main() { ..enableAppHangTracking = false ..connectionTimeout = Duration(milliseconds: 9001) ..readTimeout = Duration(milliseconds: 9002) - ..appHangTimeoutInterval = Duration(milliseconds: 9003); + ..appHangTimeoutInterval = Duration(milliseconds: 9003) + ..proxy = SentryProxy( + host: "localhost", + port: 8080, + type: SentryProxyType.http, + user: 'admin', + pass: '0000', + ); fixture.options.sdk.addIntegration('foo'); fixture.options.sdk.addPackage('bar', '1'); @@ -149,6 +156,13 @@ void main() { 'connectionTimeoutMillis': 9001, 'readTimeoutMillis': 9002, 'appHangTimeoutIntervalMillis': 9003, + 'proxy': { + 'host': 'localhost', + 'port': 8080, + 'type': 'HTTP', + 'user': 'admin', + 'pass': '0000', + } }); }); }